- Keep only index, getting-started, invariants, performance, troubleshooting, and file-organization-summary in docs/ root - Add docs/architecture/ (storage, database interfaces, native fetcher) - Add docs/deployment/ (deployment-guide, DEPLOYMENT_CHECKLIST) - Add docs/compliance/ (accessibility, legal, observability) - Move integration guides and host-app docs to docs/integration/ - Move design/planning and prefetch docs to docs/design/ - Move Android consuming-app and comparison docs to docs/platform/android/ - Move DEPLOYMENT_SUMMARY and TODO-CLASSIFICATION to docs/progress/ - Archive deprecated platform-capability-reference to docs/_archive/ - Point platform-capability links to alarms/01-platform-capability-reference.md - Update docs/00-INDEX.md with new sections and paths - Fix cross-references in README, deployment, progress, design, testing, and test-app docs - Remove one-off COMMIT_MESSAGE.txt
7.2 KiB
Action Plan: Plugin + Consuming App Integration Fixes
Source: Comparison output from Cursor session (daily-notification-plugin ↔ Time Safari / crowd-funder-for-time-pwa).
Bugs addressed: (A) Re-setting a notification doesn't fire; (B) Notification text always defaults to fallback values.
Objective
Implement plugin-side and app-side changes so that:
- Reset works: Editing/re-saving a daily reminder (even with the same time) reliably re-schedules and the alarm fires.
- Text persists: Custom title/body persist across the first fire and rollover (next day); no silent fallback to generic text.
- Cancel works on Android: App can call
cancelDailyReminder({ reminderId })and the plugin performs per-id cancellation (parity with iOS).
Plugin-Side Implementation (this repo)
1. Bug A: Skip DB idempotence when caller requests reset
File: android/src/main/java/com/timesafari/dailynotification/NotifyReceiver.kt
Problem: scheduleExactNotification() already skips PendingIntent idempotence when skipPendingIntentIdempotence=true, but the DB-level idempotence check (lines ~206–226) still runs. On "re-set same time," the DB still has the same nextRunAt, so the check returns early and no alarm is scheduled.
Change: Wrap the entire DB idempotence block so it runs only when !skipPendingIntentIdempotence. When skipPendingIntentIdempotence=true, log and skip the DB check.
- Locate: The block starting with
// DB-LEVEL IDEMPOTENCE CHECKthat loadsexistingScheduleand comparesexistingSchedule.nextRunAtwithtriggerAtMillis(60s tolerance), andreturn@runBlockingon duplicate. - Wrap: Put that block inside
if (!skipPendingIntentIdempotence) { ... }and add anelsethat logs:
"Skipping DB idempotence (skipPendingIntentIdempotence=true) for scheduleId=$stableScheduleId".
Verification: After editing a reminder without changing time, logs should show both "Skipping PendingIntent idempotence..." and "Skipping DB idempotence (skipPendingIntentIdempotence=true)...", and the alarm should fire.
2. Bug B: Preserve static reminder on rollover
File: android/src/main/java/com/timesafari/dailynotification/DailyNotificationWorker.java
Problem: In scheduleNextNotification(), the call to NotifyReceiver.scheduleExactNotification() uses hardcoded false for isStaticReminder and null for reminderId. So the next occurrence is treated as non-static and content is loaded from storage/default → fallback text.
Change:
- At the start of
scheduleNextNotification(), read from WorkManager input:
boolean preserveStaticReminder = getInputData().getBoolean("is_static_reminder", false); - When choosing
scheduleId: ifpreserveStaticReminder && notificationId != null && !notificationId.isEmpty(), setscheduleId = notificationId. Otherwise keep existing logic (daily_*→ use as scheduleId, elsedaily_rollover_+ timestamp). - Replace the existing
scheduleExactNotification(...)call with:isStaticReminder=preserveStaticReminderreminderId=preserveStaticReminder ? scheduleId : nullscheduleId= the chosenscheduleId(stable for static reminders).
- (Optional but useful) Add log before scheduling:
Log.d("DN|ROLLOVER", "next=" + nextScheduledTime + " scheduleId=" + scheduleId + " static=" + preserveStaticReminder);
Verification: Set a custom title/body, let it fire once, then confirm the next scheduled run still uses the same text; logs should show DN|ROLLOVER ... scheduleId=daily_timesafari_reminder static=true.
3. Integration: Add Android cancelDailyReminder
File: android/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.kt
Problem: The app calls DailyNotification.cancelDailyReminder({ reminderId }). iOS implements this; Android only has cancelAllNotifications() and scheduleDailyReminder() alias. On Android the call fails (method missing / not implemented), so "turn off" and "reset" flows cannot rely on explicit cancel.
Change: Add a new @PluginMethod fun cancelDailyReminder(call: PluginCall) (e.g. immediately after scheduleDailyReminder()).
- Parse ID:
reminderId = call.getString("reminderId") ?: call.getString("id") ?: call.getString("reminder_id") ?: call.getString("scheduleId"). Reject if null/blank. - Cancel alarm:
NotifyReceiver.cancelNotification(context, scheduleId = reminderId). - DB cleanup (best-effort): In a try/catch,
runBlocking:db = getDatabase()(orDailyNotificationDatabase.getDatabase(context)as used elsewhere in plugin).db.scheduleDao().setEnabled(reminderId, false)anddb.scheduleDao().updateRunTimes(reminderId, null, null).- ScheduleDao already has
setEnabledandupdateRunTimes(seeDatabaseSchema.kt).
- On success:
call.resolve(). On exception: log andcall.reject("cancelDailyReminder failed: ...").
Verification: From the app, call cancelDailyReminder({ reminderId: "daily_notification" }) (or your app’s id); it should resolve and the alarm for that id should be gone.
Verification Checklist (plugin)
After implementing the three items above:
- Reset test: Schedule reminder 2–3 minutes from now → Edit and re-save without changing time → Confirm it still fires. Logs: "Skipping DB idempotence (skipPendingIntentIdempotence=true)...".
- Rollover test: Set custom title/body → Let it fire once → Confirm next scheduled notification keeps the same title/body. Logs:
DN|ROLLOVER ... static=true scheduleId=daily_timesafari_reminder. - Cancel test: Call
cancelDailyReminder({ reminderId })from app or test harness; no error and alarm cleared.
Consuming App Work
App-side changes are described in a separate document intended for the crowd-funder-for-time-pwa (Time Safari) repo: CONSUMING_APP_CURSOR_BRIEF.md. That document is written so you can paste it into Cursor in the app repo to implement:
- Gate cancel in
editReminderNotification()so Android skips pre-cancel (schedule path already cancels internally). - Replace
TimeSafariNativeFetcherplaceholder with real content fetch and token persistence if using native fetcher for daily content.
References
- NotifyReceiver: DB idempotence at ~206–226; skipPendingIntentIdempotence at ~159–204.
- DailyNotificationWorker:
scheduleNextNotification()~512–594; passpreserveStaticReminderand stablescheduleIdintoscheduleExactNotification. - DailyNotificationPlugin: add
cancelDailyReminderafterscheduleDailyReminder; useNotifyReceiver.cancelNotificationand ScheduleDaosetEnabled/updateRunTimes. - DatabaseSchema.kt: ScheduleDao
getById,upsert,setEnabled,updateRunTimes.
Assumptions & Limits
- App uses a stable reminder id (e.g.
daily_timesafari_reminder); plugin preserves that id for static reminders on rollover. - DAO method names are as in DatabaseSchema.kt; if the plugin’s Schedule entity uses different field names, adjust the
updateRunTimescall accordingly (signature isid, lastRunAt, nextRunAt). - Native fetcher and token persistence are app responsibilities; the plugin only needs to preserve static reminder semantics and provide cancel-by-id.