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.
174 lines
19 KiB
Markdown
174 lines
19 KiB
Markdown
# 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.
|
||
|
||
**Status:** Cron parsing, stable dual ID, cancelDualSchedule, and updateDualScheduleConfig are implemented on iOS and Android. The **relationship** (contentTimeout / fallbackBehavior) is implemented: dual config is persisted; on iOS the pending dual notification is updated when the fetch completes; on Android the Worker resolves config + cache at fire time and shows the resolved title/body.
|
||
|
||
**Related:** Consuming app feedback doc `plugin-feedback-ios-scheduleDualNotification.md`; plugin `src/definitions.ts` (`DualScheduleConfiguration`, `scheduleDualNotification`, `cancelDualSchedule`).
|
||
|
||
**For app-side implementation (crowd-funder-for-time-pwa):** All plugin work below is **done**. Use this doc in the app repo to implement app changes. Require plugin **v2.1.0+** (or current local plugin with dual schedule + relationship). Focus on **§2** and the **Consuming app** rows in **§3**. Key tasks: (1) Verify plugin is linked and built so `scheduleDualNotification` is not UNIMPLEMENTED (§2.1); (2) In Edit-time flow, call `updateDualScheduleConfig(buildDualScheduleConfig({ notifyTime }))` when user saves new time, with fallback to `scheduleDualNotification` (§2.4). App paths: `src/views/AccountViewView.vue` (e.g. `editNewActivityNotification()` ~1504–1520), `src/services/notifications/dualScheduleConfig.ts`, `src/services/notifications/NativeNotificationService.ts`.
|
||
|
||
---
|
||
|
||
## 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 767–771) with real cron parsing.
|
||
- **Reference:** Android's `calculateNextRunTime(schedule: String)` in `DailyNotificationPlugin.kt` (lines 2336–2378): 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) — implemented
|
||
|
||
- **Intent:** When the user notification fires at `userNotification.schedule`, show **API-derived content** if the fetch completed and is within `relationship.contentTimeout`; otherwise show `userNotification.title` / `userNotification.body` (per `fallbackBehavior: "show_default"`).
|
||
- **Implemented:** Dual config (userNotification + relationship) is persisted when scheduling/updating. On **iOS**, after the content fetch completes in `handleBackgroundFetch`, the plugin replaces the pending dual notification with resolved title/body (from cache if within contentTimeout, else default). On **Android**, when the Worker runs for a `dual_notify_*` schedule, it loads the persisted config and content cache and resolves title/body at fire time, then displays one notification with that content. See **§1.3a** for implementation details (retained for reference).
|
||
|
||
### 1.3a Implementation plan: relationship (contentTimeout / fallbackBehavior)
|
||
|
||
Implement when ready so the New Activity notification can show API content when the fetch succeeds in time, or default text otherwise.
|
||
|
||
**Prerequisite: persist dual config (both platforms)**
|
||
|
||
When `scheduleDualNotification` or `updateDualScheduleConfig` runs, persist enough of the config for later use:
|
||
|
||
- **userNotification:** `schedule` (cron), `title`, `body` (and any other fields needed to build the notification).
|
||
- **relationship:** `contentTimeout`, `fallbackBehavior`.
|
||
|
||
So when we later resolve content (after fetch or at fire time), we have the default text and the rules. No new API surface; store what we already receive.
|
||
|
||
- **iOS:** e.g. a single key in UserDefaults (or alongside `native_fetcher_config`), e.g. `dual_schedule_config`, with this structure (e.g. JSON).
|
||
- **Android:** e.g. SharedPreferences or a keyed config; the code that runs at notification time (or after fetch) must be able to read it.
|
||
|
||
**iOS: update the pending notification when the fetch completes**
|
||
|
||
- When the content fetch runs (e.g. in `handleBackgroundFetch`), we already store the result. After a successful fetch:
|
||
1. **Read the persisted dual config.** If none (no dual schedule or legacy flow), skip.
|
||
2. **Resolve content:** Load the content just stored (or latest from cache) and its timestamp. If content exists and `(now - contentTimestamp) <= relationship.contentTimeout`, use that title/body; else use `userNotification.title` / `userNotification.body`.
|
||
3. **Replace the pending dual notification:** Remove the pending request with identifier `dualNotificationRequestIdentifier`, then add a new `UNNotificationRequest` with the same identifier, the same trigger (recompute from `userNotification.schedule` in stored config), and the resolved title/body.
|
||
|
||
- **Edge cases:** If the fetch completes after the notification time (next trigger already in the past), do not replace. If the fetch fails, leave the existing pending notification as-is (it already has default title/body).
|
||
|
||
**Android: resolve content when the notification is about to fire**
|
||
|
||
- On Android the “notification” is an alarm that fires at notify time and then runs code (e.g. `NotifyReceiver` / `DailyNotificationReceiver`) to display the notification. We cannot change the alarm’s “content” after the fact the same way as on iOS; we decide what to show when the alarm fires.
|
||
- **Persist dual config** when scheduling (same as above), keyed so the receiver can find it (e.g. by schedule id or a single “current dual config” key).
|
||
- **When the receiver runs** for a dual schedule (e.g. for `dual_notify_*` or the known dual schedule id): load the persisted dual config, load the latest content from the content cache and its timestamp, apply relationship (use cache if within `contentTimeout`, else default), then show one notification with that resolved title/body.
|
||
- The receiver must be dual-aware: for dual schedules it resolves title/body from config + cache + relationship instead of using fixed payload from the alarm.
|
||
|
||
**Summary**
|
||
|
||
| Step | iOS | Android |
|
||
|------|-----|---------|
|
||
| 1. Persist dual config | Store `userNotification` + `relationship` when scheduling/updating dual (e.g. UserDefaults). | Same; store when scheduling dual (e.g. SharedPreferences), keyed for the receiver. |
|
||
| 2. Where relationship is applied | In **handleBackgroundFetch** after storing content: resolve cache vs default, then **replace** the pending dual notification (same id, same trigger, new title/body). | In the **receiver** at notify time: load config + cache, resolve cache vs default, then **show** the notification with that title/body. |
|
||
| 3. Edge cases | Do not replace if next trigger is in the past; if fetch fails, leave existing default notification. | Receiver runs at fire time; “too old” handled by contentTimeout; if no config, fall back to alarm payload. |
|
||
|
||
### 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 2195–2199).
|
||
- **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
|
||
|
||
- **cancelDualSchedule / updateDualScheduleConfig:** Implemented; Android now exposes both methods and uses `FetchWorker.WORK_NAME_DUAL` so only dual fetch work is cancelled. For **relationship** (contentTimeout / fallbackBehavior), see §1.3a (resolve at fire time in receiver).
|
||
|
||
---
|
||
|
||
## 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` (~1504–1520).
|
||
- **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:)` | Done; real cron parsing (match Android semantics). |
|
||
| **Plugin iOS** | Use `relationship.contentTimeout` and `fallbackBehavior` | **Done;** persist dual config; in handleBackgroundFetch replace pending notification with resolved content. |
|
||
| **Plugin iOS** | `cancelDualSchedule()` implementation + registration | Done; cancel BG task + dual user notification only; stable identifier. |
|
||
| **Plugin iOS** | `updateDualScheduleConfig(config)` | Done; cancel then schedule with new config. |
|
||
| **Plugin Android** | `cancelDualSchedule()` | Done; cancel dual schedules + WorkManager WORK_NAME_DUAL only. |
|
||
| **Plugin Android** | `updateDualScheduleConfig(config)` | Done; cancel then schedule with new config. |
|
||
| **Plugin Android** | Use `relationship` (contentTimeout / fallbackBehavior) | **Done;** persist dual config; in Worker at fire time (dual_notify_*) resolve config + cache and show resolved title/body. |
|
||
| **Plugin both** | Replace semantics for dual schedule | Done; stable dual identifier, replace before add. |
|
||
| **Plugin both** | Isolation of Daily Reminder vs New Activity | Done; cancelDualSchedule does not touch reminder_*. |
|
||
| **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 ~350–379, scheduleBackgroundFetch/scheduleUserNotification ~731–770, calculateNextRunTime ~767–771, method list ~2195–2199), `ios/Plugin/DailyNotificationScheduleHelper.swift` (~98–106), `src/definitions.ts` (DualScheduleConfiguration, cancelDualSchedule).
|
||
- **Android reference:** `android/.../DailyNotificationPlugin.kt` (scheduleDualNotification ~1369–1420, calculateNextRunTime ~2336–2378).
|
||
- **Consuming app:** `doc/plugin-feedback-ios-scheduleDualNotification.md`, `src/views/AccountViewView.vue` (~1237–1245, ~1259–1300, ~1501–1548 `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`).
|