- 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).
141 lines
9.5 KiB
Markdown
141 lines
9.5 KiB
Markdown
# 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 **Capacitor’s 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** plugin’s `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` entry’s `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`
|
||
|
||
Capacitor’s `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 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).
|