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.
If the fixes were only made in a **different** clone (e.g. `daily-notification-plugin_test`) and never pushed to that gitea`master`, then:
If the fixes were only made in a **local clone** and never pushed to **gitea**`master`, then:
-`npm install` / `npm update` in the app would not pull the fixes.
- The app’s `node_modules` would only have the fixes if they were copied/linked from the fixed repo.
**Do this:**
-If the fixes live in another clone: either **push** the fixed plugin to gitea `master` and run `npm update @timesafari/daily-notification-plugin` (then`npx cap sync android`, then clean build), **or** point the app at the fixed plugin locally, e.g. in **app**`package.json`:
(adjust path to your fixed plugin repo), then `npm install`, `npx cap sync android`, clean build and reinstall.
-**Push** the fixed plugin to the official gitea repo (`trent_larson/daily-notification-plugin`), then in this app run `npm update @timesafari/daily-notification-plugin` (or set `package.json` to the branch/tag/commit you need), `npm install`,`npx cap sync android`, clean build and reinstall. The app should always depend on the published git remote, not a local `file:` path.
### 3. Fallback text from native fetcher (Bug 2 only)
**Canonical source:**`daily-notification-plugin` repo, `doc/CONSUMING_APP_HANDOFF_IOS_NATIVE_FETCHER_AND_CHAINED_DUAL.md` (same content as below for offline use).
---
## Implemented in this app
- **`ios/App/App/TimeSafariNativeFetcher.swift`** — Swift `NativeNotificationContentFetcher` mirroring `TimeSafariNativeFetcher.java` (`POST …/plansLastUpdatedBetween`, starred IDs from `daily_notification_timesafari.starredPlanIds`, JWT pool selection, pagination key `daily_notification_timesafari.last_acked_jwt_id`, aggregated copy).
- **`AppDelegate.swift`** — `DailyNotificationPlugin.registerNativeFetcher(TimeSafariNativeFetcher.shared)` at launch **before** any JS `configureNativeFetcher`; foreground handler reads `scheduled_time` as `Int64`, `NSNumber`, or `Int` for `DailyNotificationDelivered`.
## Dependency
- **`@timesafari/daily-notification-plugin`** must be **≥ 3.0.0** (register native fetcher, chained dual, iOS `updateStarredPlans`). Declare it in `package.json` from the official remote (`git+https://gitea.anomalistdesign.com/trent_larson/daily-notification-plugin.git`, branch or tag as needed), then `npm install` so `package-lock.json` resolves the published tree.
## Bump / sync (after plugin version is resolved)
1.`npm install`
2.`npx cap sync ios && npx cap sync android`
3.`cd ios/App && pod install`
4. Clean build in Xcode / Android Studio
## QA focus
- iOS: Fetcher registered before `configureNativeFetcher`; `updateStarredPlans` not `UNIMPLEMENTED`.
- Both: New Activity fires **after** prefetch for that cycle where the plugin implements chaining.
- Android: Existing `MainActivity.setNativeFetcher` unchanged; regression-test `cancelDualSchedule` vs Daily Reminder.
---
## Original handoff text (from plugin)
This document is for the **host app** repository (e.g. crowd-funder-for-time-pwa) after bumping `@timesafari/daily-notification-plugin` to a version that includes:
- **iOS** chained dual flow: user notification is **armed only after** prefetch completes (delay if fetch is late; max slip 15 minutes before fallback copy)
- **Android** chained dual flow: exact **notify** alarm is scheduled **after** dual prefetch completes (no longer scheduled at initial `scheduleDualNotification` before fetch)
Material from `doc/new-activity-notifications-ios-android-parity.md` still applies; the plugin doc adds **app-side** steps not spelled out there.
### 1. iOS — register native fetcher before `configureNativeFetcher`
The plugin **rejects**`configureNativeFetcher` if no fetcher is registered (aligned with Android).
**In `AppDelegate` (or earliest app startup before Capacitor calls into the plugin):**
Implement **`TimeSafariNativeFetcher`** as a Swift type that:
- Conforms to `NativeNotificationContentFetcher`
- Implements `fetchContent(context: FetchContext) async throws -> [NotificationContent]` with the same **Endorser** behavior as `TimeSafariNativeFetcher.java`
- Implements `configure(apiBaseUrl:activeDid:jwtToken:jwtTokenPool:)` if the fetcher needs credentials pushed from TypeScript
**Starred plan IDs for the fetcher:** Read JSON array string from UserDefaults key **`daily_notification_timesafari.starredPlanIds`** (written by `updateStarredPlans` from JS).
Ensure **`DailyNotificationDelivered`** forwards **`notification_id`** and **`scheduled_time`** from **notification content `userInfo`**.
### 3. Android — no API change for `setNativeFetcher`
Host apps that already call `DailyNotificationPlugin.setNativeFetcher(TimeSafariNativeFetcher(...))` keep that flow.
**Behavior change:** the dual **notify** alarm is scheduled when **dual prefetch work finishes**, not at the initial `scheduleDualNotification` only.
### 4. Assumptions
- Swift host implements `TimeSafariNativeFetcher`; the plugin does **not** embed `plansLastUpdatedBetween` on iOS when a host fetcher is registered (mirrors Android).
**Reference:**In the **daily-notification-plugin** repository, the test app lives at `test-apps/daily-notification-test` (same repo as `https://gitea.anomalistdesign.com/trent_larson/daily-notification-plugin`).
**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
— 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.
- **Host native fetcher:** `android/.../TimeSafariNativeFetcher.java` implements the plugin’s `NativeNotificationContentFetcher` and calls **`POST …/api/v2/report/plansLastUpdatedBetween`** using starred plan IDs (via plugin storage from `updateStarredPlans`).
- **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`.
- **No iOS `TimeSafariNativeFetcher`.** There is no Swift/Objective-C equivalent registered with the plugin (unlike Android’s `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 plugin’s 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.
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 plugin’s 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 app’s `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 Android’s 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 T−5 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
@@ -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 Android’s 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 row’s `jwtId` (item or nested `plan.jwtId`)—see Java `updateLastAckedJwtIdFromResponse`.
- **Pagination:** After a successful response with non-empty `data`, update **`last_acked_jwt_id`** from the last row’s `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)
@@ -220,7 +220,7 @@ The steps and expected notification copy below are **Android-specific**: this re
## 8. Plugin Repo Alignment and Attention Items
Comparison with the **daily-notification-plugin** repo (e.g. `daily-notification-plugin_test` or gitea `master`) to confirm our documentation and usage line up, and to flag anything that needs attention for the New Activity feature.
Comparison with the **daily-notification-plugin** repo on gitea (`trent_larson/daily-notification-plugin`, `master` or the tag this app pins) to confirm our documentation and usage line up, and to flag anything that needs attention for the New Activity feature.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.