# 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 ~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 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 app’s 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 2–3 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 ~206–226; skipPendingIntentIdempotence at ~159–204. - DailyNotificationWorker: `scheduleNextNotification()` ~512–594; 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 plugin’s 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.