doc: add completion plan for scheduleDualNotification (iOS/Android)

- 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
This commit is contained in:
Jose Olarte III
2026-03-18 17:41:49 +08:00
parent 4a1d476528
commit 7a1e58a4b6

View File

@@ -0,0 +1,135 @@
# Completion Plan: scheduleDualNotification (Plugin + Consuming App)
**Purpose:** Checklist of what needs to be done to complete the dual-schedule (New Activity) implementation in the plugin and in the consuming app. For review before making changes.
**Related:** Consuming app feedback doc `plugin-feedback-ios-scheduleDualNotification.md`; plugin `src/definitions.ts` (`DualScheduleConfiguration`, `scheduleDualNotification`, `cancelDualSchedule`).
---
## 0. Context: Two notification types in the consuming app
The consuming app (crowd-funder-for-time-pwa) has **two independent notification types**, both using a user-set time from the app DB and firing once a day at that time:
| Type | Content source | Plugin API | Cancel API |
|------|----------------|------------|------------|
| **Daily Reminder** | User-set text from app DB | `scheduleDailyReminder(id, title, body, time, …)` | `cancelDailyReminder({ reminderId })` |
| **New Activity** | API-fetched content (native fetcher) | `scheduleDualNotification({ config })` | `cancelDualSchedule()` |
- Both can be **on at the same time**; the app turns each on/off and sets its time independently.
- **Isolation requirement:** Cancelling one must not affect the other. So:
- `cancelDualSchedule()` must cancel **only** the dual schedule (content fetch + user notification for New Activity). It must **not** remove Daily Reminder notifications (iOS uses `reminder_<id>` e.g. `reminder_daily_timesafari_reminder`).
- `cancelDailyReminder({ reminderId })` must cancel **only** that reminder; it must **not** cancel the dual content-fetch task or New Activity user notification.
The completion plan below assumes this separation. Any new plugin code (e.g. iOS `cancelDualSchedule`, or dual user-notification identifiers) must preserve it.
---
## 1. Plugin (daily-notification-plugin) — iOS
### 1.1 Fix UNIMPLEMENTED (bridge / integration)
- **Ensure the native method is actually invoked.** Capacitor returns `UNIMPLEMENTED` when the bridge doesn't call the native handler. In the **consuming app**:
- Confirm the app depends on this plugin (or an up-to-date fork) and that `npx cap sync ios` / build includes the plugin's native code.
- If they use Capacitor 6, check the [Capacitor 6 plugin registration / UNIMPLEMENTED issues](https://github.com/ionic-team/capacitor-docs/issues/325) and apply any required registration or build fixes so `scheduleDualNotification` is exposed and called.
- No code changes are required in the plugin for this; the handler and registration already exist.
### 1.2 Cron parsing (align with Android)
- **Replace the stub `calculateNextRunTime(from:)`** in `ios/Plugin/DailyNotificationPlugin.swift` (lines 767771) with real cron parsing.
- **Reference:** Android's `calculateNextRunTime(schedule: String)` in `DailyNotificationPlugin.kt` (lines 23362378): supports `"minute hour * * *"`, uses device timezone, returns next occurrence (today or tomorrow).
- **Behavior:** For a given cron string (e.g. `"25 18 * * *"`), compute the next run as `Date`/`TimeInterval` and use that for:
- `BGAppRefreshTaskRequest.earliestBeginDate`
- `UNCalendarNotificationTrigger` (or equivalent) for the user notification so it fires at the correct local time daily.
- Right now the implementation ignores the cron and always uses 86400 seconds, so schedules are wrong.
### 1.3 Use `relationship` (contentTimeout + fallbackBehavior)
- **Content fetch:** Already runs in `handleBackgroundFetch` and can use the native fetcher; it stores content (e.g. via state actor / storage). No change needed for "run fetch at contentFetch.schedule."
- **User notification at userNotification.schedule:** Currently `scheduleUserNotification(config:)` (lines 741764) builds a notification from **config only** (title/body) and does not:
- Read cached content from the fetch,
- Apply `relationship.contentTimeout` (wait up to N ms for content),
- Apply `relationship.fallbackBehavior` (`show_default` vs skip vs retry).
- **Required:** When the user-notification time fires (or when the notification is prepared), resolve title/body by:
- Preferring cached content from the content fetch if it exists and is within `contentTimeout`;
- Else using `userNotification.title` / `userNotification.body` when `fallbackBehavior === "show_default"`.
- **Parsing:** Pass `relationship` from `scheduleDualNotification` into the code path that schedules and/or shows the user notification so timeout and fallback behavior are respected.
### 1.4 Implement and register `cancelDualSchedule()` on iOS
- **Current state:** `cancelDualSchedule` is in `definitions.ts` and the web implementation, but there is **no** `@objc func cancelDualSchedule(_ call: CAPPluginCall)` in the iOS plugin and **no** `CAPPluginMethod(name: "cancelDualSchedule", ...)` in the plugin's method list (around 21952199).
- **Required:**
- Add `cancelDualSchedule(_ call: CAPPluginCall)` that:
- Cancels the BGAppRefreshTaskRequest for the dual content fetch (e.g. cancel the task with `fetchTaskIdentifier` or the identifier used for the dual schedule).
- Cancels **only** the pending user notification(s) created for the dual (New Activity) schedule — e.g. by a **dedicated request identifier** (see below). Must **not** remove Daily Reminder notifications (those use identifier `reminder_<id>` e.g. `reminder_daily_timesafari_reminder`).
- When implementing or refactoring the dual user notification in `scheduleUserNotification(config:)` (or the path used by `scheduleDualNotification`), use a **stable, dedicated identifier** for the dual notification (e.g. `"dual_daily_notification"` or `"new_activity"`) so `cancelDualSchedule` can remove only that request. Currently the code uses `"daily-notification-\(Date().timeIntervalSince1970)"`, which is unique per call and not suitable for targeted cancellation.
- Append `CAPPluginMethod(name: "cancelDualSchedule", returnType: CAPPluginReturnPromise)` in the same method list.
- **Result:** Turning off "New Activity" in the app and calling `DailyNotification.cancelDualSchedule()` will no longer get `UNIMPLEMENTED` and will clear only the dual schedule, leaving Daily Reminder untouched.
### 1.5 Implement `updateDualScheduleConfig` for Edit time (recommended)
- **Use case:** The consuming app has an **Edit** button for New Activity that lets the user **change the time** of the notification. That flow is exactly what `updateDualScheduleConfig(config: DualScheduleConfiguration)` is for: "update the existing dual schedule with new config" (same config shape as `scheduleDualNotification`).
- **Current app behavior:** Edit is implemented by calling `scheduleNewActivityDualNotification(timeText)` again (i.e. `scheduleDualNotification({ config })` with the new time). That can create duplicate pending notifications if the plugin does not replace the existing dual schedule (e.g. iOS currently uses a unique identifier per call: `daily-notification-<timestamp>`).
- **Recommendation:** Implement **`updateDualScheduleConfig`** on iOS (and Android) for clear semantics: "change time" → call `updateDualScheduleConfig(newConfig)`. Implementation can be cancel existing dual schedule then schedule with new config (same as cancel + scheduleDualNotification under the hood). The consuming app should then call `updateDualScheduleConfig(buildDualScheduleConfig({ notifyTime }))` when the user saves a new time from the Edit dialog, instead of (or with fallback to) `scheduleDualNotification`.
- **Replace semantics:** Whether or not the app uses `updateDualScheduleConfig`, the plugin must ensure that when a dual schedule already exists and the app calls `scheduleDualNotification` again (e.g. on Edit), the result is **replace** not **add** — no duplicate content-fetch tasks or user notifications. Using a stable dual notification identifier (see 1.4) and replacing the existing request when scheduling achieves this.
- **Other optional methods:** `pauseDualSchedule` and `resumeDualSchedule` remain optional; they are in `definitions.ts` but not required for the current app flow.
### 1.6 Android parity check
- **cancelDualSchedule:** Android does not expose a dedicated `cancelDualSchedule`; it has `cancelAllNotifications` and WorkManager tag cancellation. For parity with the app's "turn off New Activity" flow, consider adding a `cancelDualSchedule` plugin method on Android that cancels **only** the dual-schedule work (e.g. the same tags used by `scheduleDualNotification`) and **not** the Daily Reminder schedule. That way turning off New Activity does not affect the user's Daily Reminder. Otherwise the app's call to `cancelDualSchedule()` may get `UNIMPLEMENTED` on Android too if the TS layer forwards it to native.
---
## 2. Consuming app (crowd-funder-for-time-pwa)
### 2.1 Ensure plugin is linked and built (fix UNIMPLEMENTED)
- **Verify** the app's iOS project is using the plugin from this repo (or a release that includes the iOS implementation):
- `package.json` dependency points to the right plugin (path, git, or npm).
- Run `npx cap sync ios` and confirm `DailyNotificationPlugin` (and its Swift files) are in the app's `ios/App/` or Pods.
- Clean build and run on device/simulator; confirm in Xcode that the plugin's `scheduleDualNotification` is registered and that a breakpoint in the Swift handler is hit when turning on New Activity.
- If the app is on **Capacitor 6**, follow any documented steps for plugin registration so native methods are not reported as unimplemented.
- No change to `buildDualScheduleConfig` or call order is needed; the config shape and sequence (configureNativeFetcher → updateStarredPlans → scheduleDualNotification) already match the plugin's expectations.
### 2.2 Error handling (optional but useful)
- **Current:** `AccountViewView.vue` treats `code === "UNIMPLEMENTED"` with a "not yet available on this device" message and any other error as "Could not schedule… try again."
- **Improvement:** Once the plugin implements and registers `cancelDualSchedule` on iOS, the app can:
- Keep handling `UNIMPLEMENTED` for older builds or platforms where the method is still missing.
- Optionally surface more specific errors (e.g. `code === "SCHEDULING_FAILED"` or message strings from the plugin) so the user gets clearer feedback when scheduling fails for a reason other than "not implemented."
- No change is strictly required for completion; the current flow is valid.
### 2.3 Turn-off flow
- The app already calls `DailyNotification.cancelDualSchedule()` when the user turns off New Activity (with a guard for `DailyNotification?.cancelDualSchedule`). Once the plugin implements and registers `cancelDualSchedule` on iOS (and optionally on Android), this will work without any app code change.
### 2.4 Edit time flow (New Activity)
- **Current:** When the user taps **Edit** and picks a new time, the app calls `scheduleNewActivityDualNotification(timeText)` (i.e. `scheduleDualNotification({ config })` again). See `editNewActivityNotification()` in `AccountViewView.vue` (~15041520).
- **Recommended:** Once the plugin implements `updateDualScheduleConfig` (see 1.5), the app should call **`updateDualScheduleConfig({ config })`** when the user saves a new time from the Edit dialog, with `config = buildDualScheduleConfig({ notifyTime: timeText })`. That makes the intent explicit ("update existing schedule") and avoids relying on replace semantics inside `scheduleDualNotification`. The app can keep a fallback to `scheduleDualNotification` when `updateDualScheduleConfig` is not available (e.g. older plugin version).
---
## 3. Summary table
| Where | What | Status / action |
|-------|------|------------------|
| **Plugin iOS** | `scheduleDualNotification` handler + registration | Done; fix bridge/build in app if still UNIMPLEMENTED. |
| **Plugin iOS** | Cron parsing in `calculateNextRunTime(from:)` | Replace stub with real parsing (match Android semantics). |
| **Plugin iOS** | Use `relationship.contentTimeout` and `fallbackBehavior` when showing user notification | Implement: prefer cached content, else default title/body. |
| **Plugin iOS** | `cancelDualSchedule()` implementation + registration | Add handler and method registration; cancel BG task + dual user notifications only (use dedicated dual notification identifier; do not affect Daily Reminder). |
| **Plugin iOS** | `updateDualScheduleConfig(config)` | Implement for Edit-time use case; update existing dual schedule (e.g. cancel then schedule with new config). Use stable dual identifier so no duplicates. |
| **Plugin Android** | `cancelDualSchedule()` (if app calls it) | Add if not present; cancel only dual-schedule work, not Daily Reminder. |
| **Plugin Android** | `updateDualScheduleConfig(config)` | Implement for Edit-time use case; same semantics as iOS. |
| **Plugin both** | Replace semantics for dual schedule | When a dual schedule exists, calling `scheduleDualNotification` again or `updateDualScheduleConfig` must replace it, not add a second (no duplicate notifications). |
| **Plugin both** | Isolation of Daily Reminder vs New Activity | cancelDualSchedule must not touch reminder_* / Daily Reminder; cancelDailyReminder must not touch dual schedule. |
| **Consuming app** | Plugin linked and built for iOS | Verify dependency, `cap sync`, and build so native `scheduleDualNotification` is called. |
| **Consuming app** | Edit time: use `updateDualScheduleConfig` | In `editNewActivityNotification()`, call `updateDualScheduleConfig(buildDualScheduleConfig({ notifyTime }))` when user saves new time; fallback to `scheduleDualNotification` if unavailable. |
| **Consuming app** | Error handling / UX | Optional: refine messages once plugin returns specific error codes. |
---
## 4. References
- **Plugin:** `ios/Plugin/DailyNotificationPlugin.swift` (scheduleDualNotification ~350379, scheduleBackgroundFetch/scheduleUserNotification ~731770, calculateNextRunTime ~767771, method list ~21952199), `ios/Plugin/DailyNotificationScheduleHelper.swift` (~98106), `src/definitions.ts` (DualScheduleConfiguration, cancelDualSchedule).
- **Android reference:** `android/.../DailyNotificationPlugin.kt` (scheduleDualNotification ~13691420, calculateNextRunTime ~23362378).
- **Consuming app:** `doc/plugin-feedback-ios-scheduleDualNotification.md`, `src/views/AccountViewView.vue` (~12371245, ~12591300, ~15011548 `editNewActivityNotification`), `src/services/notifications/dualScheduleConfig.ts`, `src/services/notifications/reminderIds.ts` (Daily Reminder vs New Activity IDs), `src/services/notifications/NativeNotificationService.ts` (Daily Reminder uses `scheduleDailyReminder` / `cancelDailyReminder`).