From 3c262c9eeb158d7d1fad1302ea73905628ac36bb Mon Sep 17 00:00:00 2001 From: Jose Olarte III Date: Tue, 17 Mar 2026 21:05:06 +0800 Subject: [PATCH] docs: add plugin feedback doc for iOS scheduleDualNotification Add plugin-feedback-ios-scheduleDualNotification.md for the daily-notification-plugin repo: config shape from the app, expected behavior, and acceptance criteria so iOS can implement or fix scheduleDualNotification (currently returns UNIMPLEMENTED). --- ...n-feedback-ios-scheduleDualNotification.md | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 doc/plugin-feedback-ios-scheduleDualNotification.md diff --git a/doc/plugin-feedback-ios-scheduleDualNotification.md b/doc/plugin-feedback-ios-scheduleDualNotification.md new file mode 100644 index 0000000000..0fa73dddfa --- /dev/null +++ b/doc/plugin-feedback-ios-scheduleDualNotification.md @@ -0,0 +1,115 @@ +# Plugin Feedback: Implement scheduleDualNotification on iOS + +**Target repo:** daily-notification-plugin (iOS native layer) +**Purpose:** Document for implementing or fixing `scheduleDualNotification` on iOS so the consuming app (TimeSafari / crowd-funder) can enable “New Activity” notifications. +**Consuming app doc:** `doc/notification-new-activity-lay-of-the-land.md` + +--- + +## Current behavior + +- The **consuming app** calls `DailyNotification.scheduleDualNotification({ config })` from TypeScript when the user turns on “New Activity Notification” and picks a time (native iOS). +- On **iOS**, the plugin rejects with **`code: "UNIMPLEMENTED"`** (observed in Xcode: `[AccountViewView] scheduleNewActivityDualNotification failed: {"code":"UNIMPLEMENTED"}`). +- On **Android**, the same call is expected to work (dual schedule: content fetch + user notification). + +The app has already: + +- Called `configureNativeFetcher({ apiBaseUrl, activeDid, jwtToken })` so the plugin can use the native fetcher for API-driven content. +- Called `updateStarredPlans({ planIds })` so the fetcher knows which plans to query. +- Built a `config` object that matches the plugin’s `DualScheduleConfiguration` (see below). + +So the missing piece on iOS is a **working implementation** of `scheduleDualNotification` that accepts this config and schedules the dual flow (content fetch at one time, user notification at a later time). + +--- + +## Call from the consuming app + +```ts +await DailyNotification.scheduleDualNotification({ config }); +``` + +`config` is built by the app’s `buildDualScheduleConfig({ notifyTime })` and has the following shape. + +--- + +## Config shape the app sends + +The app sends a single `config` object that matches the plugin’s `DualScheduleConfiguration` (see `definitions.ts`). Example for `notifyTime: "18:30"` (6:30 PM): + +```json +{ + "contentFetch": { + "enabled": true, + "schedule": "25 18 * * *", + "callbacks": {} + }, + "userNotification": { + "enabled": true, + "schedule": "30 18 * * *", + "title": "New Activity", + "body": "Check your starred projects and offers for updates.", + "sound": true, + "priority": "normal" + }, + "relationship": { + "autoLink": true, + "contentTimeout": 300000, + "fallbackBehavior": "show_default" + } +} +``` + +- **Cron format:** `"minute hour * * *"` (daily at that local time). +- **contentFetch.schedule:** 5 minutes **before** the user’s chosen time (e.g. 18:25 for notify at 18:30). +- **userNotification.schedule:** The user’s chosen time (e.g. 18:30). +- **contentFetch.callbacks:** The app sends `{}`; the actual fetch is done by the **native fetcher** (already configured via `configureNativeFetcher`). The plugin should run the content-fetch job at the contentFetch cron and use the native fetcher to get content; at userNotification time it should show a notification using that content or the fallback title/body. +- **relationship.contentTimeout:** Milliseconds to wait for content before showing the notification (app uses 5 minutes = 300000). +- **relationship.fallbackBehavior:** `"show_default"` means if content isn’t ready in time, show the notification with the default title/body from `userNotification`. + +The app does **not** send `contentFetch.url` or `contentFetch.timesafariConfig`; it relies on the native fetcher and `configureNativeFetcher` / `updateStarredPlans` for API behavior. + +--- + +## Expected plugin behavior (iOS) + +1. **Accept** the `config` argument (object with `contentFetch`, `userNotification`, and optional `relationship`). +2. **Parse** the cron expressions for `contentFetch.schedule` and `userNotification.schedule` (e.g. using a shared cron parser or the same approach as Android). +3. **Schedule** two things: + - **Content fetch:** At the time given by `contentFetch.schedule`, run the **native notification content fetcher** (the one configured via `configureNativeFetcher`). Store the result in the plugin’s cache (or equivalent) for use when the user notification fires. + - **User notification:** At the time given by `userNotification.schedule`, show a local notification. Use cached content from the fetch if available and within `relationship.contentTimeout`; otherwise use `userNotification.title` and `userNotification.body` (per `relationship.fallbackBehavior: "show_default"`). +4. **Do not** reject with `UNIMPLEMENTED`; resolve the promise once scheduling has succeeded (or reject with a descriptive error if scheduling fails). +5. **cancelDualSchedule()** should cancel both the content-fetch schedule and the user-notification schedule so the user can turn off New Activity from the app. + +Alignment with **Android** (if implemented there) is desirable: same config shape, same semantics (prefetch then notify, fallback to default title/body). The plugin’s **definitions.ts** already defines `DualScheduleConfiguration`, `ContentFetchConfig`, `UserNotificationConfig`, and the `scheduleDualNotification` / `cancelDualSchedule` API. + +--- + +## Where to look in the plugin (iOS) + +- **Plugin entry:** `ios/Plugin/DailyNotificationPlugin.swift` (or equivalent)—find the handler for `scheduleDualNotification` (e.g. method that receives `call.getObject("config")`). +- **Android reference:** `android/` implementation of `scheduleDualNotification` and how it schedules WorkManager/alarms for content fetch and for the user notification. +- **Definitions:** `src/definitions.ts` — `DualScheduleConfiguration`, `scheduleDualNotification`, `cancelDualSchedule`. +- **Native fetcher:** The app configures the native fetcher before calling `scheduleDualNotification`; the iOS plugin should invoke that same fetcher when the content-fetch job runs (BGAppRefreshTask or equivalent), not a URL from the config. + +--- + +## Acceptance criteria + +- [ ] On iOS, calling `DailyNotification.scheduleDualNotification({ config })` with the config shape above **does not** reject with `code: "UNIMPLEMENTED"`. +- [ ] The content-fetch job is scheduled at `contentFetch.schedule` and uses the configured native fetcher to fetch content. +- [ ] The user notification is scheduled at `userNotification.schedule` and shows with API-derived content when available, or with `userNotification.title` / `userNotification.body` as fallback. +- [ ] Calling `DailyNotification.cancelDualSchedule()` cancels both schedules on iOS. +- [ ] Behavior is consistent with Android where applicable (same config, same lifecycle). + +--- + +## Relationship to consuming app + +The consuming app will continue to call: + +1. `configureNativeFetcher(...)` on startup and when enabling New Activity. +2. `updateStarredPlans({ planIds })` when enabling or when Account view loads with New Activity on. +3. `scheduleDualNotification({ config })` when the user turns on New Activity and picks a time. +4. `cancelDualSchedule()` when the user turns off New Activity. + +No change to the app’s config shape or call order is planned; the fix is entirely on the plugin iOS side to implement or correct `scheduleDualNotification` (and ensure `cancelDualSchedule` clears the dual schedule).