Files
crowd-funder-for-time-pwa/doc/new-activity-notifications-ios-android-parity.md
Jose Olarte III 8290943b53 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.
2026-04-01 20:49:02 +08:00

12 KiB
Raw Blame History

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).
  • 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).