Files
daily-notification-plugin/docs/integration/ACTION_PLAN_INTEGRATION_FIXES.md
Jose Olarte III 6ad7ff5fe1 docs: reorganize docs into subdirs and fix links
- 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
2026-03-06 19:51:13 +08:00

7.2 KiB
Raw Blame History

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:

  1. Reset works: Editing/re-saving a daily reminder (even with the same time) reliably re-schedules and the alarm fires.
  2. Text persists: Custom title/body persist across the first fire and rollover (next day); no silent fallback to generic text.
  3. 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 ~206226) 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 CHECK that loads existingSchedule and compares existingSchedule.nextRunAt with triggerAtMillis (60s tolerance), and return@runBlocking on duplicate.
  • Wrap: Put that block inside if (!skipPendingIntentIdempotence) { ... } and add an else that 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:

  1. At the start of scheduleNextNotification(), read from WorkManager input:
    boolean preserveStaticReminder = getInputData().getBoolean("is_static_reminder", false);
  2. When choosing scheduleId: if preserveStaticReminder && notificationId != null && !notificationId.isEmpty(), set scheduleId = notificationId. Otherwise keep existing logic (daily_* → use as scheduleId, else daily_rollover_ + timestamp).
  3. Replace the existing scheduleExactNotification(...) call with:
    • isStaticReminder = preserveStaticReminder
    • reminderId = preserveStaticReminder ? scheduleId : null
    • scheduleId = the chosen scheduleId (stable for static reminders).
  4. (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() (or DailyNotificationDatabase.getDatabase(context) as used elsewhere in plugin).
    • db.scheduleDao().setEnabled(reminderId, false) and db.scheduleDao().updateRunTimes(reminderId, null, null).
    • ScheduleDao already has setEnabled and updateRunTimes (see DatabaseSchema.kt).
  • On success: call.resolve(). On exception: log and call.reject("cancelDailyReminder failed: ...").

Verification: From the app, call cancelDailyReminder({ reminderId: "daily_notification" }) (or your apps id); it should resolve and the alarm for that id should be gone.


Verification Checklist (plugin)

After implementing the three items above:

  1. Reset test: Schedule reminder 23 minutes from now → Edit and re-save without changing time → Confirm it still fires. Logs: "Skipping DB idempotence (skipPendingIntentIdempotence=true)...".
  2. 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.
  3. 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 TimeSafariNativeFetcher placeholder with real content fetch and token persistence if using native fetcher for daily content.

References

  • NotifyReceiver: DB idempotence at ~206226; skipPendingIntentIdempotence at ~159204.
  • DailyNotificationWorker: scheduleNextNotification() ~512594; pass preserveStaticReminder and stable scheduleId into scheduleExactNotification.
  • DailyNotificationPlugin: add cancelDailyReminder after scheduleDailyReminder; use NotifyReceiver.cancelNotification and ScheduleDao setEnabled / 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 plugins Schedule entity uses different field names, adjust the updateRunTimes call accordingly (signature is id, lastRunAt, nextRunAt).
  • Native fetcher and token persistence are app responsibilities; the plugin only needs to preserve static reminder semantics and provide cancel-by-id.