feat(ios): register Swift TimeSafariNativeFetcher for New Activity notifications

Add TimeSafariNativeFetcher (plansLastUpdatedBetween parity with Android) and
call DailyNotificationPlugin.registerNativeFetcher from AppDelegate before JS
configureNativeFetcher; broaden DailyNotificationDelivered scheduled_time types
in willPresent. Wire the new file into the App target; normalize PBX object IDs
to 24-char hex.

Document plugin ≥3 handoff (consuming-app-handoff-ios-native-fetcher-chained-dual),
refresh iOS/Android parity and notification-from-api-call file tables.
This commit is contained in:
Jose Olarte III
2026-04-02 19:02:48 +08:00
parent 90e6603d52
commit 7d87a746f9
12 changed files with 432 additions and 129 deletions

View File

@@ -2,9 +2,7 @@
**Purpose:** Describe what is required for **iOS** to match **Android** for the daily-notification-plugin **API-driven “New Activity”** flow (`scheduleDualNotification` / `cancelDualSchedule`, with prefetch and Endorser-backed content). The canonical product behavior is documented in `doc/notification-from-api-call.md` and `doc/notification-new-activity-lay-of-the-land.md`.
**Plugin source of truth (development):** The Capacitor package is `@timesafari/daily-notification-plugin` (declared in `package.json`). A local clone used for plugin work may live at
`/Users/aardimus/Sites/trentlarson/daily-notification-plugin_test/daily-notification-plugin`
— implement plugin changes there, publish or link the package, then bump/sync in this app.
**Plugin source of truth:** The Capacitor package is `@timesafari/daily-notification-plugin`, pulled from the official remote in `package.json` (`git+https://gitea.anomalistdesign.com/trent_larson/daily-notification-plugin.git`). Plugin development happens in that repository; this app bumps the dependency and runs `npm install` / `npx cap sync` after releases.
---
@@ -26,7 +24,7 @@ Platform differences (iOS **BGTaskScheduler** is opportunistic; Android **alarms
- **Host native fetcher:** `android/.../TimeSafariNativeFetcher.java` implements the plugins `NativeNotificationContentFetcher` and calls **`POST …/api/v2/report/plansLastUpdatedBetween`** using starred plan IDs (via plugin storage from `updateStarredPlans`).
- **Registration:** `MainActivity` calls `DailyNotificationPlugin.setNativeFetcher(new TimeSafariNativeFetcher(this))`.
- **Plugin gaps (Android):** Even with the above, the **dual-schedule** path in the plugin has had issues (native fetcher not used for dual prefetch, fetch time not aligned to `contentFetch.schedule`). See `doc/plugin-feedback-android-dual-schedule-native-fetch-and-timing.md`. Full “Android parity” with the product intent may require **plugin fixes** as well as the host fetcher.
- **Plugin (Android) — older notes:** Prior dual-schedule issues (native fetcher / fetch cron) are addressed in **plugin ≥ 3.0.0** (chained dual: notify after prefetch). Historical analysis: `doc/plugin-feedback-android-dual-schedule-native-fetch-and-timing.md`.
---
@@ -34,20 +32,19 @@ Platform differences (iOS **BGTaskScheduler** is opportunistic; Android **alarms
### 3.1 This repository
- **No iOS `TimeSafariNativeFetcher`.** There is no Swift/Objective-C equivalent registered with the plugin (unlike Androids `setNativeFetcher`).
- **iOS native fetcher:** `ios/App/App/TimeSafariNativeFetcher.swift` implements `NativeNotificationContentFetcher` (Endorser `plansLastUpdatedBetween`, same prefs keys as Java). **`AppDelegate`** calls `DailyNotificationPlugin.registerNativeFetcher(TimeSafariNativeFetcher.shared)` at launch **before** any `configureNativeFetcher` from JS (see plugin `doc/CONSUMING_APP_HANDOFF_IOS_NATIVE_FETCHER_AND_CHAINED_DUAL.md` and **`doc/consuming-app-handoff-ios-native-fetcher-chained-dual.md`**).
- **JS/TS is already shared:** `nativeFetcherConfig.ts`, `dualScheduleConfig.ts`, `syncStarredPlansToNativePlugin.ts`, and `AccountViewView.vue` call the same APIs on both platforms.
- **Info.plist** already lists `UIBackgroundModes` (fetch, processing) and `BGTaskSchedulerPermittedIdentifiers` for the plugins task IDs. Xcode **Signing & Capabilities** should still enable **Background fetch** and **Background processing** (see `doc/daily-notification-plugin-integration.md`).
- **AppDelegate** posts `DailyNotificationDelivered` for foreground presentation—aligned with plugin rollover behavior.
### 3.2 Bundled plugin (`node_modules/@timesafari/daily-notification-plugin`, iOS)
Verified from the Swift sources shipped with the package (version pinned in `ios/App/Podfile.lock`):
Requires **plugin ≥ 3.0.0** (register native fetcher, chained dual, iOS `updateStarredPlans`). Version pinned in `ios/App/Podfile.lock` after `pod install`.
- **`scheduleDualNotification` / `cancelDualSchedule`** are implemented and exposed in `pluginMethods` (not inherently `UNIMPLEMENTED` if Pods/binary are fresh). If you still see `UNIMPLEMENTED`, follow `doc/plugin-feedback-ios-scheduleDualNotification.md` (clean sync, `pod install`, PluginHeaders check).
- **`configureNativeFetcher`** persists JWT/API/DID to `UserDefaults`, but the in-file comment still states the **iOS native fetcher interface is not fully aligned with Android**—configuration is stored for use by the plugins own HTTP path.
- **`fetchContentFromAPI` (in-plugin)** calls **`GET /api/v2/report/offers`**, not **`plansLastUpdatedBetween`**. Response handling is a minimal JSON→`NotificationContent` mapping. This does **not** match `TimeSafariNativeFetcher` or the help copy for “starred project updates.”
- **`updateStarredPlans`** is **not** implemented on iOS: no Swift symbol and **no** `CAPPluginMethod` entry. The apps `syncStarredPlansToNativePlugin` intentionally tolerates `UNIMPLEMENTED`; starred plans therefore **do not** drive iOS prefetch today.
- **Prefetch execution** runs in **`BGAppRefreshTask`** (`handleBackgroundFetch`). Apple does not guarantee execution at a specific minute; the dual flow is **best-effort** compared to Androids exact alarms.
- **`scheduleDualNotification` / `cancelDualSchedule`** — see plugin release notes; clean sync + `pod install` if you see `UNIMPLEMENTED` (`doc/plugin-feedback-ios-scheduleDualNotification.md`).
- **`configureNativeFetcher`** **requires** `DailyNotificationPlugin.registerNativeFetcher` first; the host Swift fetcher performs **`plansLastUpdatedBetween`** (plugin does not use in-plugin `offers` GET when a fetcher is registered—mirrors Android).
- **`updateStarredPlans`** — implemented on iOS in current plugin; persists **`daily_notification_timesafari.starredPlanIds`** for the host fetcher.
- **Chained dual** — user notification is armed **after** prefetch for that cycle (plugin); iOS remains subject to BG scheduling limits; see **§3.3**.
### 3.3 Prefetch before notify (ordering, not cron)
@@ -65,20 +62,17 @@ Plugin work item **§4A.3** should reflect this: document the chosen strategy (c
## 4. Work breakdown
### 4A. Plugin (`daily-notification-plugin`) — required for real parity
### 4A. Plugin (`daily-notification-plugin`) — status (v3.x)
1. **`updateStarredPlans` on iOS**
- Add method to `DailyNotificationPlugin.swift`, register in `pluginMethods`, persist `planIds` (e.g. UserDefaults or existing storage), and read them in the prefetch path.
Items below were the original gap list; **plugin ≥ 3.0.0** ships **iOS** `updateStarredPlans`, **`registerNativeFetcher`**, **chained dual** on iOS and Android, and Android dual-path fixes. Remaining work is **release coordination** (bump, sync, QA), not greenfield plugin implementation.
2. **Align HTTP/API with Android / `TimeSafariNativeFetcher`**
- Replace or supplement `fetchContentFromAPI` so prefetch uses **`POST /api/v2/report/plansLastUpdatedBetween`** (or shared helper) with the same payload and response rules as the Java fetcher (empty data → no spurious “update” content; aggregated titles; pagination/`last_acked_jwt_id` if required).
- Alternatively, introduce an iOS **host registration** pattern mirroring `setNativeFetcher` so this app can ship **one** implementation in Swift and the plugin only orchestrates scheduling + cache (matches Android architecture and avoids duplicating business logic in the plugin).
1. **`updateStarredPlans` on iOS** — shipped in current plugin.
3. **Dual schedule timing vs iOS constraints**
- Document and implement the best possible mapping from `contentFetch.schedule` / `userNotification.schedule` to **BGTaskScheduler** + **UNNotification** triggers. Prefer **chaining** (arm or replace the user notification **after** prefetch for that cycle—see **§3.3**) where product requires “never show API-dependent copy before fetch,” rather than only two independent timers. Where exact T5 cannot be guaranteed, document behavior and any fallback (e.g. refresh when app becomes active) so product expectations are clear.
2. **iOS `plansLastUpdatedBetween` / host fetcher** — shipped: host registers **`TimeSafariNativeFetcher`** (Swift); plugin does not duplicate Endorser logic when a fetcher is registered.
4. **Android dual path (same repo)**
- For cross-platform parity, apply fixes described in `doc/plugin-feedback-android-dual-schedule-native-fetch-and-timing.md` so Android dual prefetch actually invokes the native fetcher at the **fetch** cron.
3. **Dual schedule / chaining** — shipped (notify after prefetch; see plugin release notes and **§3.3**).
4. **Android dual path** — chained dual + native fetcher alignment in current plugin (see `doc/plugin-feedback-android-dual-schedule-native-fetch-and-timing.md` for historical context).
5. **JWT pool / expiry (Phase B)**
- **App:** Phase B is already implemented: `configureNativeFetcherIfReady()` passes `jwtTokens` from `mintBackgroundJwtTokenPool` on **both** iOS and Android (`src/services/notifications/nativeFetcherConfig.ts`).
@@ -88,11 +82,11 @@ Plugin work item **§4A.3** should reflect this: document the chosen strategy (c
### 4B. This app (crowd-funder-for-time-pwa) — after or alongside plugin changes
1. **Bump `@timesafari/daily-notification-plugin`**, run `npx cap sync ios`, `cd ios/App && pod install`, clean build (see `doc/plugin-feedback-ios-scheduleDualNotification.md`).
2. **Re-test** `syncStarredPlansToNativePlugin`: once iOS exposes `updateStarredPlans`, remove reliance on “optional UNIMPLEMENTED” for correctness tests (the helper can stay defensive).
3. **If** the plugin adds `setNativeFetcher` (or similar) on iOS, add a **Swift** `TimeSafariNativeFetcher` implementation (port of the Java class) and register it at launch (mirror `MainActivity`).
1. **Bump `@timesafari/daily-notification-plugin`** to **≥ 3.0.0** via the git dependency in `package.json`, run `npm install`, `npx cap sync ios`, `cd ios/App && pod install`, clean build (`doc/plugin-feedback-ios-scheduleDualNotification.md`, **`doc/consuming-app-handoff-ios-native-fetcher-chained-dual.md`**).
2. **iOS native fetcher****Done:** `TimeSafariNativeFetcher.swift` + `registerNativeFetcher` in `AppDelegate` (see handoff doc).
3. **Re-test** `syncStarredPlansToNativePlugin` on iOS; the helper may still catch `UNIMPLEMENTED` for older plugin binaries.
4. **Xcode:** Confirm Background Modes capabilities match `Info.plist`.
5. **QA:** Full matrix in `doc/notification-from-api-call.md` (enable/disable, empty starred list, JWT expiry, foreground/background).
5. **QA:** Full matrix in `doc/notification-from-api-call.md` (enable/disable, empty starred list, JWT expiry, foreground/background); chained dual timing (notify after prefetch).
### 4C. Related product bug (both platforms)
@@ -104,6 +98,7 @@ Plugin work item **§4A.3** should reflect this: document the chosen strategy (c
| Topic | Document |
|-------|-----------|
| Plugin post-bump handoff (iOS fetcher + chained dual) | `doc/consuming-app-handoff-ios-native-fetcher-chained-dual.md` |
| Feature plan & file list | `doc/notification-from-api-call.md` |
| Dual vs Daily Reminder confusion | `doc/notification-new-activity-lay-of-the-land.md` |
| iOS `UNIMPLEMENTED` / PluginHeaders | `doc/plugin-feedback-ios-scheduleDualNotification.md` |
@@ -135,22 +130,20 @@ Implementations on **iOS** (in-plugin Swift or host `NativeNotificationContentFe
- **Method & path:** `POST` `{apiBaseUrl}/api/v2/report/plansLastUpdatedBetween` (no trailing slash mismatch on `apiBaseUrl`).
- **Headers:** `Content-Type: application/json`, `Authorization: Bearer {token}` (token from `jwtToken` or **JWT pool** selection—see Java `selectBearerTokenForRequest`: UTC day mod pool size).
- **JSON body:** `planIds` (array of strings, possibly empty), `afterId` (string; use `"0"` if none stored).
- **Starred plans:** Persist `updateStarredPlans` the same way Androids plugin + host expect: SharedPreferences name **`daily_notification_timesafari`**, key **`starredPlanIds`** (JSON array string). iOS should use **the same keys** if using `UserDefaults` / app group so behavior matches the Java fetcher and plugin docs.
- **Pagination:** After a successful response with non-empty `data`, update **`last_acked_jwt_id`** from the last rows `jwtId` (item or nested `plan.jwtId`)—see Java `updateLastAckedJwtIdFromResponse`.
- **Starred plans:** Android: SharedPreferences **`daily_notification_timesafari`** + key **`starredPlanIds`**. iOS (plugin + host): `UserDefaults.standard` key **`daily_notification_timesafari.starredPlanIds`** (JSON array string).
- **Pagination:** After a successful response with non-empty `data`, update **`last_acked_jwt_id`** from the last rows `jwtId` (item or nested `plan.jwtId`)—see Java `updateLastAckedJwtIdFromResponse`. iOS host (`TimeSafariNativeFetcher.swift`) persists **`daily_notification_timesafari.last_acked_jwt_id`** in `UserDefaults.standard`.
- **Empty `data`:** Return **no** notification items (empty list); do not synthesize a “no updates” push from an empty result—Java returns empty `contents` when `data` is absent or empty.
- **Non-empty `data`:** One aggregated `NotificationContent`: titles **Starred Project Update** / **Starred Project Updates**, bodies use typographic quotes around first project name and **has been updated.** / **+ N more have been updated.** (see Java `parseApiResponse`).
### 6.3 Likely plugin touchpoints (names may drift—search the repo)
### 6.3 Likely plugin touchpoints (maintenance / debugging)
- **iOS:** `ios/Plugin/DailyNotificationPlugin.swift`, `DailyNotificationScheduleHelper.swift`, background fetch / UN notification paths; add **`updateStarredPlans`** to `pluginMethods` and persistence.
- **Android:** `DailyNotificationPlugin.kt`, `FetchWorker` / `DailyNotificationFetchWorker`, `ScheduleHelper`per dual-schedule feedback doc.
- **iOS:** `ios/Plugin/DailyNotificationPlugin.swift`, `DailyNotificationScheduleHelper.swift`, native fetcher registry, BG / UN paths.
- **Android:** `DailyNotificationPlugin.kt`, fetch workers / `ScheduleHelper`see dual-schedule feedback doc for history.
### 6.4 Suggested order inside the plugin repo
### 6.4 Suggested order (plugin shipped ≥ 3.0.0)
1. **Android dual path** + native fetcher at fetch cron (fixes real device behavior for existing host).
2. **iOS** `updateStarredPlans` + **`plansLastUpdatedBetween`** parity (or **iOS `setNativeFetcher`** + thin Swift adapter).
3. **iOS** prefetch/notify **ordering** (§3.3): document and implement chained arm vs best-effort.
4. Release / tag → consuming app bumps **`@timesafari/daily-notification-plugin`**, `npx cap sync`, `pod install`.
1. Tag / publish **`@timesafari/daily-notification-plugin`**.
2. **Consuming app:** bump, `npm install`, `npx cap sync`, `pod install`, QA (`doc/consuming-app-handoff-ios-native-fetcher-chained-dual.md`).
---