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.
19 KiB
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 usesreminder_<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
UNIMPLEMENTEDwhen 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 and apply any required registration or build fixes so
scheduleDualNotificationis exposed and called.
- Confirm the app depends on this plugin (or an up-to-date fork) and that
- 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:)inios/Plugin/DailyNotificationPlugin.swift(lines 767–771) with real cron parsing. - Reference: Android's
calculateNextRunTime(schedule: String)inDailyNotificationPlugin.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 asDate/TimeIntervaland use that for:BGAppRefreshTaskRequest.earliestBeginDateUNCalendarNotificationTrigger(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 withinrelationship.contentTimeout; otherwise showuserNotification.title/userNotification.body(perfallbackBehavior: "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 adual_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:- Read the persisted dual config. If none (no dual schedule or legacy flow), skip.
- 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 useuserNotification.title/userNotification.body. - Replace the pending dual notification: Remove the pending request with identifier
dualNotificationRequestIdentifier, then add a newUNNotificationRequestwith the same identifier, the same trigger (recompute fromuserNotification.schedulein 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 withincontentTimeout, 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:
cancelDualScheduleis indefinitions.tsand the web implementation, but there is no@objc func cancelDualSchedule(_ call: CAPPluginCall)in the iOS plugin and noCAPPluginMethod(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
fetchTaskIdentifieror 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).
- Cancels the BGAppRefreshTaskRequest for the dual content fetch (e.g. cancel the task with
- When implementing or refactoring the dual user notification in
scheduleUserNotification(config:)(or the path used byscheduleDualNotification), use a stable, dedicated identifier for the dual notification (e.g."dual_daily_notification"or"new_activity") socancelDualSchedulecan 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.
- Add
- Result: Turning off "New Activity" in the app and calling
DailyNotification.cancelDualSchedule()will no longer getUNIMPLEMENTEDand 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 asscheduleDualNotification). - 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
updateDualScheduleConfigon iOS (and Android) for clear semantics: "change time" → callupdateDualScheduleConfig(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 callupdateDualScheduleConfig(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 callsscheduleDualNotificationagain (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:
pauseDualScheduleandresumeDualScheduleremain optional; they are indefinitions.tsbut not required for the current app flow.
1.6 Android parity
- cancelDualSchedule / updateDualScheduleConfig: Implemented; Android now exposes both methods and uses
FetchWorker.WORK_NAME_DUALso 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.jsondependency points to the right plugin (path, git, or npm).- Run
npx cap sync iosand confirmDailyNotificationPlugin(and its Swift files) are in the app'sios/App/or Pods. - Clean build and run on device/simulator; confirm in Xcode that the plugin's
scheduleDualNotificationis 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
buildDualScheduleConfigor 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.vuetreatscode === "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
cancelDualScheduleon iOS, the app can:- Keep handling
UNIMPLEMENTEDfor 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."
- Keep handling
- 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 forDailyNotification?.cancelDualSchedule). Once the plugin implements and registerscancelDualScheduleon 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). SeeeditNewActivityNotification()inAccountViewView.vue(~1504–1520). - Recommended: Once the plugin implements
updateDualScheduleConfig(see 1.5), the app should callupdateDualScheduleConfig({ config })when the user saves a new time from the Edit dialog, withconfig = buildDualScheduleConfig({ notifyTime: timeText }). That makes the intent explicit ("update existing schedule") and avoids relying on replace semantics insidescheduleDualNotification. The app can keep a fallback toscheduleDualNotificationwhenupdateDualScheduleConfigis 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–1548editNewActivityNotification),src/services/notifications/dualScheduleConfig.ts,src/services/notifications/reminderIds.ts(Daily Reminder vs New Activity IDs),src/services/notifications/NativeNotificationService.ts(Daily Reminder usesscheduleDailyReminder/cancelDailyReminder).