Files
crowd-funder-for-time-pwa/doc/plugin-feedback-ios-scheduleDualNotification.md
Jose Olarte III 1389a166fa fix(ios): New Activity dual notification – handle updateStarredPlans and BGTaskScheduler errors
- Treat updateStarredPlans as optional: catch UNIMPLEMENTED and continue to
  scheduleDualNotification so missing native method no longer blocks scheduling.
- Show specific toast when BGTaskSchedulerErrorDomain error 1 occurs (e.g.
  Simulator): explain that a real device and Background App Refresh are required.
- Add PluginHeaders diagnostic in AccountViewView and main.capacitor.ts to debug
  UNIMPLEMENTED (log DNP methods at call time and at launch).
- Fix main.capacitor.ts build: use CapacitorWindow type and safe cap assignment
  so vite build --mode capacitor succeeds.
- Docs: add UNIMPLEMENTED troubleshooting and updateStarredPlans note in
  plugin-feedback-ios-scheduleDualNotification.md; add section 8.3 in
  notification-new-activity-lay-of-the-land.md.
- Lockfile updates (package-lock.json, Podfile.lock).
2026-03-19 19:26:59 +08:00

141 lines
9.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`
---
## Troubleshooting: `UNIMPLEMENTED` on iOS (Capacitor 6)
If **`configureNativeFetcher`** (or other DailyNotification methods) work but **`scheduleDualNotification`** still fails with **`{"code":"UNIMPLEMENTED"}`** and you **do not** see a native log line like `To Native -> DailyNotification scheduleDualNotification`, the failure is often **not** missing Swift code—it is **Capacitors JavaScript layer** rejecting the call because the method is **not listed** in `window.Capacitor.PluginHeaders` for `DailyNotification`. Those headers are built at runtime from the **compiled** plugins `pluginMethods` list (`CAPBridgedPlugin`).
**Fix in the consuming app (usual cause: stale Pods / binary):**
1. Ensure `node_modules/@timesafari/daily-notification-plugin` includes `scheduleDualNotification` in `DailyNotificationPlugin.swift`s `pluginMethods` (v2.1.0+).
2. From the project root: `npx cap sync ios`
3. `cd ios/App && pod install` (or delete `Pods` + `Podfile.lock` and `pod install` if upgrading the plugin).
4. Xcode: **Product → Clean Build Folder**, then rebuild and run on device/simulator.
**Verify:** Safari → Develop → attach to the app WebView → Console: inspect `window.Capacitor.PluginHeaders` and confirm the `DailyNotification` entrys `methods` array includes `{ name: "scheduleDualNotification", ... }`.
If a full clean rebuild still doesn't fix it, clear Xcode's **system** DerivedData (quit Xcode, run `rm -rf ~/Library/Developer/Xcode/DerivedData/*TimeSafari*`, reopen and rebuild). On launch the app logs `[Capacitor] DNP PluginHeaders methods: [...]`; if that list omits `scheduleDualNotification`, the native binary is still stale.
If the method **is** present in headers but scheduling still fails, debug the Swift implementation (reject message, BG tasks, etc.).
### Misleading `UNIMPLEMENTED` before `scheduleDualNotification`
Capacitors `registerPlugin` proxy returns a **callable stub for every property name**. So `if (DailyNotification?.updateStarredPlans)` is **always truthy** even when iOS does not expose `updateStarredPlans` in `pluginMethods`. Calling that stub throws **`UNIMPLEMENTED`** in JS **before** any `To Native -> DailyNotification scheduleDualNotification` line appears—so logs look like “dual schedule is unimplemented” when the real failure was **`updateStarredPlans`**.
**Consuming-app fix:** treat `updateStarredPlans` as optional: catch `UNIMPLEMENTED` and continue, or only call after verifying the method name exists on `PluginHeaders` for `DailyNotification`. If the plugin adds `updateStarredPlans` natively later, starred-plan filtering will start working without app changes.
---
## 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 plugins `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 apps `buildDualScheduleConfig({ notifyTime })` and has the following shape.
---
## Config shape the app sends
The app sends a single `config` object that matches the plugins `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 users chosen time (e.g. 18:25 for notify at 18:30).
- **userNotification.schedule:** The users 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 isnt 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 plugins 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 plugins **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 apps 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).