docs: add New Activity iOS/Android parity guide and refine follow-ups

Add doc/new-activity-notifications-ios-android-parity.md covering dual-schedule
and Endorser API parity, plugin vs app work, Android dual-path notes, prefetch
vs notify ordering on iOS (§3.3), and clarified Phase B JWT pool status on
both platforms. Link the guide from doc/notification-from-api-call.md under the
iOS checklist.
This commit is contained in:
Jose Olarte III
2026-04-01 20:49:02 +08:00
parent 8ba84888ee
commit 8290943b53
2 changed files with 126 additions and 0 deletions

View File

@@ -0,0 +1,124 @@
# New Activity Notifications: iOS Parity with Android
**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.
---
## 1. What “parity” means here
| Concern | Intended behavior |
|--------|---------------------|
| **Scheduling** | Dual schedule: prefetch job **before** notify time (app uses cron T5 minutes), then user-visible notification at the chosen time. |
| **API content** | Prefetch calls the **same Endorser semantics** as the Android host: **`plansLastUpdatedBetween`** (POST) with **starred plan IDs**, JWT auth, aggregated titles/bodies consistent with `TimeSafariNativeFetcher`. |
| **Starred plans** | `updateStarredPlans({ planIds })` from the app must affect what the native prefetch queries. |
| **Configure** | `configureNativeFetcher({ apiBaseUrl, activeDid, jwtToken, … })` supplies credentials the native layer uses for prefetch. |
| **Lifecycle** | `cancelDualSchedule()` removes the dual prefetch + notify schedule without breaking the separate Daily Reminder. |
Platform differences (iOS **BGTaskScheduler** is opportunistic; Android **alarms/WorkManager** can be more exact) mean **timing** may never be identical, but **API behavior and user-visible copy** should align.
---
## 2. Current state: Android (this app)
- **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.
---
## 3. Current state: iOS (this app + bundled plugin)
### 3.1 This repository
- **No iOS `TimeSafariNativeFetcher`.** There is no Swift/Objective-C equivalent registered with the plugin (unlike Androids `setNativeFetcher`).
- **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`):
- **`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.
### 3.3 Prefetch before notify (ordering, not cron)
iOS has no system cron; the app/plugin may still **parse** cron to compute “next run” times. The hard part is **ordering**: if **prefetch** is driven by **`BGTaskScheduler`** (opportunistic) and **notify** by **`UNUserNotificationCenter`** at a fixed time **T**, those are **independent**. The OS can deliver the local notification at **T** while prefetch runs **after** **T** or not at all—so the awkward case (notify first, prefetch later, stale or fallback content) **can** happen. Two peer timers do **not** imply “fetch always completes before **T**.”
To **enforce** prefetch-before-notify as a rule, use **chaining**, not two unrelated schedules:
- After prefetch for that cycle **finishes** (success or explicit timeout policy), **then** schedule or **replace** the pending `UNNotificationRequest` for time **T** with the resolved title/body (or fallback). Until then, do not arm a user-visible notification that claims fresh API content.
- **Tradeoffs:** If prefetch is late, the notification may be **late**; if prefetch never runs before a deadline, use **fallback** copy at **T** or skip—product choice.
- **Parsing cron** remains useful to compute **T** and to decide when to **submit** BG work; **ordering** is a **pipeline** decision (fetch → cache → arm notify), not “BG at T5 and UN at **T** both scheduled up front.”
Plugin work item **§4A.3** should reflect this: document the chosen strategy (chained arm vs best-effort dual timer) and how it interacts with `relationship.contentTimeout` / fallback.
---
## 4. Work breakdown
### 4A. Plugin (`daily-notification-plugin`) — required for real parity
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.
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).
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.
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.
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`).
- **Android:** `TimeSafariNativeFetcher` selects a bearer from the pool for background requests (`doc/plugin-feedback-daily-notification-configureNativeFetcher-jwt-pool.md`).
- **iOS:** The bundled plugins `configureNativeFetcher` **already accepts and persists** `jwtTokens` / `jwtTokenPoolJson`, and the in-plugin fetch path uses a bearer from the primary token or pool. What is **not** yet at parity with Android is **which API** that token is used for (`offers` GET vs `plansLastUpdatedBetween` + starred plans)—that falls under **§4A.2**, not “waiting for Phase B on iOS.”
- **Expiry:** Re-calling `configureNativeFetcherIfReady` on foreground / Account (see `notification-from-api-call.md`) remains relevant on both platforms.
### 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`).
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).
### 4C. Related product bug (both platforms)
- **`PushNotificationPermission.vue` vs New Activity:** Enabling New Activity can still schedule the **single** daily reminder by mistake; turning New Activity off may not cancel that reminder. See `doc/notification-new-activity-lay-of-the-land.md`. Fixing this is orthogonal to iOS/Android API parity but affects perceived “notifications behavior.”
---
## 5. Reference map (this repo)
| Topic | Document |
|-------|-----------|
| 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` |
| Android dual schedule + native fetcher | `doc/plugin-feedback-android-dual-schedule-native-fetch-and-timing.md` |
| Integration & Xcode | `doc/daily-notification-plugin-integration.md` |
| Android host fetcher | `android/.../TimeSafariNativeFetcher.java`, `MainActivity.java` |
---
## 6. Acceptance checklist (iOS vs Android product intent)
- [ ] Prefetch uses **plansLastUpdatedBetween** (or host fetcher with identical behavior), not only `offers` GET.
- [ ] **Starred plan IDs** from settings change what is queried (`updateStarredPlans` works on iOS).
- [ ] Notification title/body match the **same rules** as Android for “starred project updates” (including empty updates).
- [ ] `configureNativeFetcher` + JWT refresh story documented; re-config on foreground if needed (`notification-from-api-call.md`).
- [ ] `cancelDualSchedule` clears dual prefetch/notify without leaving orphan schedules.
- [ ] Understand and document **iOS timing** limitations vs Android for support/Help copy.
- [ ] **Prefetch vs notify ordering** on iOS: chosen strategy (chained arm vs independent BG + UN) documented; avoids claiming fresh API content when prefetch has not run yet (**§3.3**).

View File

@@ -59,6 +59,8 @@ The app must:
### iOS ### iOS
**Parity outline (API, starred plans, plugin vs app work):** See **`doc/new-activity-notifications-ios-android-parity.md`**.
- **Confirm iOS native fetcher / dual schedule** - **Confirm iOS native fetcher / dual schedule**
Plugin exposes `configureNativeFetcher` on iOS. Confirm whether the plugin expects an iOS-specific native fetcher registration (similar to Androids `setNativeFetcher`) and, if so, register a TimeSafari fetcher implementation for iOS so API-driven notifications work on iPhone. Plugin exposes `configureNativeFetcher` on iOS. Confirm whether the plugin expects an iOS-specific native fetcher registration (similar to Androids `setNativeFetcher`) and, if so, register a TimeSafari fetcher implementation for iOS so API-driven notifications work on iPhone.
- **Verify dual schedule on iOS** - **Verify dual schedule on iOS**