Expose bridge-safe pending, delivered, and settings snapshots from
UNUserNotificationCenter so apps can inspect local notification state
without native date objects or large payloads.
- Add getDeliveredNotifications and getNotificationSettings on iOS
- Harden getPendingNotifications with triggerTimestamp/triggerDateIso
- Always resolve with serializable values; omit userInfo from delivered
- Register new Capacitor methods and TypeScript definitions/web stubs
Clarify that the native Daily Notification Plugin does not
authenticate users or call notification-wakeup-service directly.
Authentication belongs in the host app; the plugin only schedules
local notifications and orchestrates host-provided content fetch.
Add docs/security-boundaries.md with responsibility matrix, auth
flow diagram, SPI boundary notes, and a warning against adding backend
auth logic to the plugin layer.
Use predictive_<epochMillis> for reminders (next-occurrence ms), testAlarm
fire time, and scheduleNotifications; persist predictiveEpochMillis in
UserDefaults for cancel/update alignment.
clearAllNotifications removes only predictive_* pending and delivered
entries. scheduleNotifications is additive (no global clear) and skips
past timestamps.
Expose clearAllNotifications and scheduleNotifications on DailyNotification.
clearAllNotifications clears pending and delivered center notifications.
scheduleNotifications replaces pending requests from epoch-ms timestamps
with deterministic predictive_* IDs and DNP-BATCH logging.
Replace reminder_* and random test alarm IDs with predictive_<Int>
so UNUserNotificationCenter replaces pending requests instead of
stacking. Daily reminders key off local HH:mm; testAlarm uses the
scheduled fire second. Dual notification id unchanged.
Dual native prefetch used to map an empty NotificationContent list to
synthetic JSON and still arm the chained notify alarm, which led hosts
such as TimeSafari to show marketing copy via show_default even when the
API had no rows.
Persist {"skipNotification":true} for an empty native result, skip
DualScheduleNotifyScheduler for that successful cycle while still
enqueueing dual fetch recovery, and teach DualScheduleHelper to return
no content for fresh skip payloads and for stale cache when
fallbackBehavior is skip. Add Robolectric tests for DualScheduleHelper
and the skip payload helper.
Add jwtTokens / jwtTokenPoolJson to the TypeScript API, parse and validate
(max 128) on Android and iOS, persist jwtTokenPool with native_fetcher_config
when persistToken is true (Android), and extend NativeNotificationContentFetcher
with a four-argument configure overload delegating to the existing three-arg
default. iOS stores the pool in UserDefaults JSON and uses primary jwt or first
pool entry in the plugin background fetch path. Bump version to 2.2.0. Update
TestNativeFetcher to exercise the new configure overload.
- Schedule dual content fetch with WorkManager initialDelay to the next
contentFetch cron; reschedule from prefs after success and on boot when
dual_fetch_* exists (DualScheduleFetchRecovery + ReactivationManager).
- When contentFetch has no URL, call NativeNotificationContentFetcher with
FetchContext (prefetch + next notify time); else keep HTTP/mock behavior.
- Add content_cache.cacheScope (dual|daily|legacy), Room v4 migration,
getLatestByScope; DualScheduleHelper reads dual only; daily fetch paths
write daily; NotifyReceiver prefers daily/legacy for legacy cache reads.
- Extract ScheduleCronUtils.calculateNextRunTimeMillis for shared cron math.
- Document in README/CHANGELOG; bump package to 2.1.5.
Add ANDROID_DUAL_SCHEDULE_NATIVE_FETCH_AND_CACHE_SCOPE.md describing the
pre-implementation plan: WorkManager initial delay for dual prefetch,
NativeNotificationContentFetcher when URL is absent, and cacheScope on
ContentCache to separate dual vs daily reminder cache rows.
parseUserNotificationConfig used getBoolean/getString for title, body, sound,
vibration, and priority; missing keys threw JSONException though TS marks them
optional. Add optBooleanOrNull/optStringOrNull (same pattern as optIntOrNull) and
defer to existing NotifyReceiver/DualScheduleHelper defaults.
Document in README; extend CHANGELOG [2.1.3].
JSONObject.getInt threw when timeout/retryAttempts/retryDelay were omitted, but
TS ContentFetchConfig marks them optional. Use optIntOrNull so null passes
through and FetchWorker keeps its existing defaults.
Document omitted-field behavior in README under scheduleDualNotification.
handleDisplayNotification already reads schedule_id after getInputData().
Inner branches redeclared String scheduleId, which javac rejects in the
same method scope. Drop the redundant lines; behavior unchanged.
NotificationContent.title and .body are String?; assigning them to
non-optional String caused Swift build errors. Use ?? with the same
defaults as the config fallback so both branches yield non-optional
title/body.
Add "For app-side implementation" paragraph so the completion plan can
be used in the app repo: focus §2/§3, plugin v2.1.0+, link/build check,
Edit flow with updateDualScheduleConfig, and key app file paths.
- Checklist for completing dual-schedule (New Activity) on plugin and app
- Context: two notification types (Daily Reminder vs New Activity), isolation
- iOS: cron parsing, relationship, cancelDualSchedule, updateDualScheduleConfig
- Android: cancelDualSchedule; updateDualScheduleConfig for Edit time
- Consuming app: link/build verification, Edit flow use updateDualScheduleConfig
- Replace semantics and refs to plugin and app code
- Android: move plugin source to org/timesafari/dailynotification, update
namespace, manifest package, and all package/imports; change intent actions
to org.timesafari.daily.NOTIFICATION and DISMISS
- iOS: update bundle IDs, BGTask identifiers, subsystem labels, and queue
names in Plugin and Xcode projects
- Capacitor: update plugin class registration and appIds in configs
- Test apps (android-test-app, daily-notification-test, ios-test-app):
applicationId/bundleId, manifests, ProGuard, scripts, and docs
- Docs: bulk update references; add CONSUMING_APP_MIGRATION_COM_TO_ORG.md
for consuming app migration
BREAKING CHANGE: Consuming apps must update plugin class to
org.timesafari.dailynotification.DailyNotificationPlugin, manifest
receivers/actions, and iOS BGTask identifiers per migration doc.
Remove the guard that opened system Settings and rejected when exact alarms
were not granted. Scheduling now proceeds using inexact/windowed fallback;
consuming apps can handle UX (e.g. optional hint or openExactAlarmSettings()).
- Bug 1: When the firing run used schedule_id daily_rollover_*, resolve the
canonical notify schedule (first enabled with rolloverIntervalMinutes > 0)
and use it to read the interval so the next run is current + interval
instead of +24h. Add ScheduleHelper.getCanonicalRolloverScheduleBlocking().
- Bug 2: For ROLLOVER_ON_FIRE, do not skip scheduling when an existing
PendingIntent is found for the same schedule id: cancel the existing alarm
and set the new trigger time so the rollover chain (e.g. 21:10 → 21:20)
is updated instead of treated as duplicate.
Align package.json and all plugin version references (Android entity
strings and file headers, TypeScript definitions/observability/web)
with 1.3.0 for the rolloverIntervalMinutes release.
Add optional rolloverIntervalMinutes to scheduleDailyNotification so the
next occurrence can be scheduled N minutes after the current trigger
(e.g. 10 minutes) instead of 24 hours. Value is persisted and used on
rollover and after reboot.
- TypeScript: NotificationOptions.rolloverIntervalMinutes?: number
- Android: Schedule.rolloverIntervalMinutes in Room (migration 2→3);
Plugin and ScheduleHelper persist it; Worker uses it in rollover and
updates nextRunAt; ReactivationManager uses it in boot recovery
- iOS: NotificationContent.rolloverIntervalMinutes (Codable); Plugin
passes it into content; Scheduler uses it in calculateNextScheduledTime
and copies to nextContent on rollover
When absent or ≤0, behavior unchanged (24h). App can clear by calling
scheduleDailyNotification without the parameter.
Update package.json, iOS podspec, and Android plugin-version references
after fix for duplicate fallback notifications (cancel fetch-related
WorkManager jobs when scheduling daily notification).
Prevents a second notification (UUID alarm) with fallback or placeholder text by
cancelling pending prefetch/fetch work when the user schedules or reschedules.
cleanupExistingNotificationSchedules only cancels alarms for DB schedule IDs;
alarms from DailyNotificationFetchWorker use a UUID and were never cancelled.
Add ScheduleHelper.cancelFetchRelatedWorkManagerJobs() to cancel only the
prefetch and daily_notification_fetch tags (not display, dismiss, or maintenance).
Call it after cleanup and before scheduleDailyNotification. Future fetched-content
flows can use distinct WorkManager tags and will not be affected by this path.
- Receiver: stop reading Room on main thread; pass schedule_id to Worker
so title/body are resolved on a background thread (fixes
db_fallback_failed / "Cannot access database on the main thread").
- Worker: use stable schedule_id for rollover so one alarm per reminder
and reschedule cancels it; resolve user title/body by schedule_id when
Intent lacks them; skip prefetch for static reminders to avoid a
second alarm.
- ScheduleHelper: persist NotificationContentEntity for scheduleId when
scheduling daily notification so rollover and post-reboot show user
text.
Refs: plugin-feedback-android-rollover-double-fire-and-user-content
Boot recovery was skipping reschedule when it found an "existing" PendingIntent.
AlarmManager alarms are not guaranteed to persist across reboot; on devices that
clear them, the skip caused the next notification (initial or rollover) to never
fire until the app was opened. Pass skipPendingIntentIdempotence = true for all
BOOT_RECOVERY call sites (BootReceiver, ReactivationManager.rescheduleAlarmForBoot)
so the alarm is always re-registered after reboot. Setting the same PendingIntent
again replaces any existing alarm, so no duplicate alarms.
After device restart, PendingIntent extras (title, body, is_static_reminder) can be
missing when the alarm fires, so the worker took the Room/JIT path and showed
fallback text instead of the user's message.
- DailyNotificationReceiver: when intent has notification_id but missing title/body,
load NotificationContentEntity from Room and pass title/body into Worker input
with is_static_reminder=true.
- ReactivationManager: add getTitleBodyForSchedule(); use persisted title/body in
rescheduleAlarm and rescheduleAlarmForBoot (and inner boot helper) instead of
hardcoded "Daily Notification" / "Your daily update is ready".
- BootReceiver: use ReactivationManager.getTitleBodyForSchedule() when building
UserNotificationConfig for notify schedules after boot.
- DailyNotificationWorker: when content from Room has both title and body, skip
performJITFreshnessCheck so user text is not overwritten by fetcher placeholder.
Ref: plugin-feedback-android-post-reboot-fallback-text (crowd-funder-for-time-pwa)