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:
@@ -61,16 +61,14 @@ The app depends on:
|
||||
"@timesafari/daily-notification-plugin": "git+https://gitea.anomalistdesign.com/trent_larson/daily-notification-plugin.git#master"
|
||||
```
|
||||
|
||||
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`:
|
||||
- `"@timesafari/daily-notification-plugin": "file:../daily-notification-plugin"`
|
||||
(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)
|
||||
|
||||
|
||||
80
doc/consuming-app-handoff-ios-native-fetcher-chained-dual.md
Normal file
80
doc/consuming-app-handoff-ios-native-fetcher-chained-dual.md
Normal file
@@ -0,0 +1,80 @@
|
||||
# Consuming app handoff: iOS native fetcher + chained dual (mirror)
|
||||
|
||||
**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** `NativeNotificationContentFetcher`–style registration (`DailyNotificationPlugin.registerNativeFetcher`)
|
||||
- **iOS** `updateStarredPlans` / `getStarredPlans` (parity with Android `daily_notification_timesafari` / `starredPlanIds` semantics)
|
||||
- **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):**
|
||||
|
||||
```swift
|
||||
import TimesafariDailyNotificationPlugin
|
||||
|
||||
DailyNotificationPlugin.registerNativeFetcher(TimeSafariNativeFetcher.shared)
|
||||
```
|
||||
|
||||
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).
|
||||
|
||||
### 2. iOS — `UNUserNotificationCenterDelegate` / rollover
|
||||
|
||||
Chained dual notifications set:
|
||||
|
||||
- `notification_id` = `org.timesafari.dailynotification.dual`
|
||||
- `scheduled_time` = `NSNumber` (fire time in ms)
|
||||
|
||||
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).
|
||||
- Module import: `TimesafariDailyNotificationPlugin` (Pod `TimesafariDailyNotificationPlugin`).
|
||||
@@ -6,8 +6,7 @@
|
||||
2. **Notifications show when the app is in the foreground** (not only background/closed).
|
||||
3. **Plugin loads at app launch** so recovery runs after reboot without the user opening notification UI.
|
||||
|
||||
**Reference:** Test app at
|
||||
`/Users/aardimus/Sites/trentlarson/daily-notification-plugin_test/daily-notification-plugin/test-apps/daily-notification-test`
|
||||
**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`).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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 plugin’s `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 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.
|
||||
|
||||
### 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 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
|
||||
|
||||
| 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 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`.
|
||||
- **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 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)
|
||||
### 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`).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -101,5 +101,8 @@ Add a short “New Activity notifications” section to BUILDING.md or a user-fa
|
||||
| Settings type | `src/interfaces/accountView.ts` |
|
||||
| Android native fetcher | `android/app/src/main/java/app/timesafari/TimeSafariNativeFetcher.java` |
|
||||
| Android registration | `android/app/src/main/java/app/timesafari/MainActivity.java` |
|
||||
| iOS native fetcher | `ios/App/App/TimeSafariNativeFetcher.swift` |
|
||||
| iOS registration | `ios/App/App/AppDelegate.swift` (`DailyNotificationPlugin.registerNativeFetcher`) |
|
||||
| Plugin 3.x handoff | `doc/consuming-app-handoff-ios-native-fetcher-chained-dual.md` |
|
||||
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
### 8.1 What lines up
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
**Date:** 2026-02-18
|
||||
**Generated:** 2026-02-18 17:47:06 PST
|
||||
**Target repo:** daily-notification-plugin (local copy at `daily-notification-plugin_test`)
|
||||
**Target repo:** `@timesafari/daily-notification-plugin` (https://gitea.anomalistdesign.com/trent_larson/daily-notification-plugin)
|
||||
**Consuming app:** crowd-funder-for-time-pwa (TimeSafari)
|
||||
**Platform:** Android
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
C86585DF2ED456DE00824752 /* TimeSafariShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = C86585D52ED456DE00824752 /* TimeSafariShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
C8C56E142EE0474B00737D0E /* SharedImageUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8C56E132EE0474B00737D0E /* SharedImageUtility.swift */; };
|
||||
C8C56E162EE064CB00737D0E /* SharedImagePlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8C56E152EE064CA00737D0E /* SharedImagePlugin.swift */; };
|
||||
B7E1C4F82A9D3E506F1B2C8D /* TimeSafariNativeFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3F8E2D91B4C5E60718293A4 /* TimeSafariNativeFetcher.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -59,6 +60,7 @@
|
||||
C86585E52ED4577F00824752 /* App.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = App.entitlements; sourceTree = "<group>"; };
|
||||
C8C56E132EE0474B00737D0E /* SharedImageUtility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedImageUtility.swift; sourceTree = "<group>"; };
|
||||
C8C56E152EE064CA00737D0E /* SharedImagePlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedImagePlugin.swift; sourceTree = "<group>"; };
|
||||
A3F8E2D91B4C5E60718293A4 /* TimeSafariNativeFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeSafariNativeFetcher.swift; sourceTree = "<group>"; };
|
||||
E2E9297D5D02C549106C77F9 /* Pods-App.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.release.xcconfig"; path = "Target Support Files/Pods-App/Pods-App.release.xcconfig"; sourceTree = "<group>"; };
|
||||
EAEC6436E595F7CD3A1C9E96 /* Pods-App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.debug.xcconfig"; path = "Target Support Files/Pods-App/Pods-App.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
@@ -140,6 +142,7 @@
|
||||
children = (
|
||||
C8C56E152EE064CA00737D0E /* SharedImagePlugin.swift */,
|
||||
C8C56E132EE0474B00737D0E /* SharedImageUtility.swift */,
|
||||
A3F8E2D91B4C5E60718293A4 /* TimeSafariNativeFetcher.swift */,
|
||||
C86585E52ED4577F00824752 /* App.entitlements */,
|
||||
50379B222058CBB4000EE86E /* capacitor.config.json */,
|
||||
504EC3071FED79650016851F /* AppDelegate.swift */,
|
||||
@@ -359,6 +362,7 @@
|
||||
C8C56E162EE064CB00737D0E /* SharedImagePlugin.swift in Sources */,
|
||||
504EC3081FED79650016851F /* AppDelegate.swift in Sources */,
|
||||
C8C56E142EE0474B00737D0E /* SharedImageUtility.swift in Sources */,
|
||||
B7E1C4F82A9D3E506F1B2C8D /* TimeSafariNativeFetcher.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import UIKit
|
||||
import Capacitor
|
||||
import CapacitorCommunitySqlite
|
||||
import TimesafariDailyNotificationPlugin
|
||||
import UserNotifications
|
||||
|
||||
@UIApplicationMain
|
||||
@@ -9,6 +10,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
||||
var window: UIWindow?
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
// New Activity / dual schedule: plugin requires a registered native fetcher before configureNativeFetcher (parity with Android setNativeFetcher).
|
||||
DailyNotificationPlugin.registerNativeFetcher(TimeSafariNativeFetcher.shared)
|
||||
|
||||
// Set notification center delegate so notifications show in foreground and rollover is triggered
|
||||
UNUserNotificationCenter.current().delegate = self
|
||||
|
||||
@@ -89,14 +93,21 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
||||
/// Show notifications when app is in foreground and post DailyNotificationDelivered for rollover.
|
||||
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
|
||||
let userInfo = notification.request.content.userInfo
|
||||
if let notificationId = userInfo["notification_id"] as? String,
|
||||
let scheduledTime = userInfo["scheduled_time"] as? Int64 {
|
||||
if let notificationId = userInfo["notification_id"] as? String {
|
||||
let scheduledTime: Int64? = {
|
||||
if let v = userInfo["scheduled_time"] as? Int64 { return v }
|
||||
if let n = userInfo["scheduled_time"] as? NSNumber { return n.int64Value }
|
||||
if let i = userInfo["scheduled_time"] as? Int { return Int64(i) }
|
||||
return nil
|
||||
}()
|
||||
if let scheduledTime = scheduledTime {
|
||||
NotificationCenter.default.post(
|
||||
name: NSNotification.Name("DailyNotificationDelivered"),
|
||||
object: nil,
|
||||
userInfo: ["notification_id": notificationId, "scheduled_time": scheduledTime]
|
||||
)
|
||||
}
|
||||
}
|
||||
if #available(iOS 14.0, *) {
|
||||
completionHandler([.banner, .sound, .badge])
|
||||
} else {
|
||||
|
||||
215
ios/App/App/TimeSafariNativeFetcher.swift
Normal file
215
ios/App/App/TimeSafariNativeFetcher.swift
Normal file
@@ -0,0 +1,215 @@
|
||||
import Foundation
|
||||
import TimesafariDailyNotificationPlugin
|
||||
|
||||
/// Native content fetcher for API-driven New Activity notifications on iOS.
|
||||
/// Mirrors `TimeSafariNativeFetcher.java` (POST `plansLastUpdatedBetween`, starred plans, JWT pool, pagination).
|
||||
final class TimeSafariNativeFetcher: NativeNotificationContentFetcher {
|
||||
static let shared = TimeSafariNativeFetcher()
|
||||
|
||||
private let endpoint = "/api/v2/report/plansLastUpdatedBetween"
|
||||
private let readTimeoutSec: TimeInterval = 15
|
||||
private let maxRetries = 3
|
||||
private let retryDelayMs = 1_000
|
||||
|
||||
/// Matches plugin `updateStarredPlans` storage (`DailyNotificationPlugin.swift`).
|
||||
private let prefsStarredKey = "daily_notification_timesafari.starredPlanIds"
|
||||
/// Matches Java `TimeSafariNativeFetcher` prefs namespace `daily_notification_timesafari` + `last_acked_jwt_id`.
|
||||
private let prefsLastAckedKey = "daily_notification_timesafari.last_acked_jwt_id"
|
||||
|
||||
private var apiBaseUrl: String?
|
||||
private var activeDid: String?
|
||||
private var jwtToken: String?
|
||||
private var jwtTokenPool: [String]?
|
||||
|
||||
private init() {}
|
||||
|
||||
func configure(apiBaseUrl: String, activeDid: String, jwtToken: String, jwtTokenPool: [String]?) {
|
||||
self.apiBaseUrl = apiBaseUrl.trimmingCharacters(in: .whitespacesAndNewlines).replacingOccurrences(
|
||||
of: "/$",
|
||||
with: "",
|
||||
options: .regularExpression
|
||||
)
|
||||
self.activeDid = activeDid
|
||||
self.jwtToken = jwtToken
|
||||
self.jwtTokenPool = (jwtTokenPool?.isEmpty == false) ? jwtTokenPool : nil
|
||||
}
|
||||
|
||||
func fetchContent(context: FetchContext) async throws -> [NotificationContent] {
|
||||
try await fetchContentWithRetry(context: context, retryCount: 0)
|
||||
}
|
||||
|
||||
/// One pool entry per UTC day (epoch day mod pool size); else primary `jwtToken` — same as Java.
|
||||
private func selectBearerTokenForRequest() -> String? {
|
||||
guard let pool = jwtTokenPool, !pool.isEmpty else { return jwtToken }
|
||||
let epochDay = Int64(Date().timeIntervalSince1970 * 1000) / (24 * 60 * 60 * 1000)
|
||||
let idx = Int(epochDay) % pool.count
|
||||
let t = pool[idx]
|
||||
if t.isEmpty { return jwtToken }
|
||||
return t
|
||||
}
|
||||
|
||||
private func fetchContentWithRetry(context: FetchContext, retryCount: Int) async throws -> [NotificationContent] {
|
||||
guard let base = apiBaseUrl, !base.isEmpty,
|
||||
activeDid != nil,
|
||||
let bearer = selectBearerTokenForRequest(), !bearer.isEmpty
|
||||
else {
|
||||
NSLog("[TimeSafariNativeFetcher] Not configured; call configureNativeFetcher from JS first.")
|
||||
return []
|
||||
}
|
||||
|
||||
guard let url = URL(string: base + endpoint) else {
|
||||
return []
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.setValue("Bearer \(bearer)", forHTTPHeaderField: "Authorization")
|
||||
request.timeoutInterval = readTimeoutSec
|
||||
|
||||
let planIds = getStarredPlanIds()
|
||||
var afterId = getLastAcknowledgedJwtId() ?? "0"
|
||||
if afterId.isEmpty { afterId = "0" }
|
||||
|
||||
let body: [String: Any] = [
|
||||
"planIds": planIds,
|
||||
"afterId": afterId,
|
||||
]
|
||||
request.httpBody = try JSONSerialization.data(withJSONObject: body)
|
||||
|
||||
NSLog(
|
||||
"[TimeSafariNativeFetcher] POST \(endpoint) planCount=\(planIds.count) afterId=\(afterId.prefix(12))…"
|
||||
)
|
||||
|
||||
let config = URLSessionConfiguration.ephemeral
|
||||
config.timeoutIntervalForRequest = readTimeoutSec
|
||||
config.timeoutIntervalForResource = readTimeoutSec
|
||||
let session = URLSession(configuration: config)
|
||||
|
||||
do {
|
||||
let (data, response) = try await session.data(for: request)
|
||||
guard let http = response as? HTTPURLResponse else {
|
||||
return []
|
||||
}
|
||||
|
||||
if http.statusCode == 200 {
|
||||
let bodyStr = String(data: data, encoding: .utf8) ?? ""
|
||||
let contents = parseApiResponse(responseBody: bodyStr, context: context)
|
||||
if !contents.isEmpty {
|
||||
updateLastAckedJwtIdFromResponse(responseBody: bodyStr)
|
||||
}
|
||||
return contents
|
||||
}
|
||||
|
||||
if retryCount < maxRetries && (http.statusCode >= 500 || http.statusCode == 429) {
|
||||
let delayMs = retryDelayMs * (1 << retryCount)
|
||||
try await Task.sleep(nanoseconds: UInt64(delayMs) * 1_000_000)
|
||||
return try await fetchContentWithRetry(context: context, retryCount: retryCount + 1)
|
||||
}
|
||||
|
||||
NSLog("[TimeSafariNativeFetcher] API error \(http.statusCode)")
|
||||
return []
|
||||
} catch {
|
||||
NSLog("[TimeSafariNativeFetcher] Fetch failed: \(error.localizedDescription)")
|
||||
if retryCount < maxRetries {
|
||||
let delayMs = retryDelayMs * (1 << retryCount)
|
||||
try await Task.sleep(nanoseconds: UInt64(delayMs) * 1_000_000)
|
||||
return try await fetchContentWithRetry(context: context, retryCount: retryCount + 1)
|
||||
}
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
private func getStarredPlanIds() -> [String] {
|
||||
guard let jsonStr = UserDefaults.standard.string(forKey: prefsStarredKey),
|
||||
!jsonStr.isEmpty, jsonStr != "[]",
|
||||
let data = jsonStr.data(using: .utf8),
|
||||
let arr = try? JSONSerialization.jsonObject(with: data) as? [Any]
|
||||
else {
|
||||
return []
|
||||
}
|
||||
return arr.compactMap { $0 as? String }
|
||||
}
|
||||
|
||||
private func getLastAcknowledgedJwtId() -> String? {
|
||||
let s = UserDefaults.standard.string(forKey: prefsLastAckedKey)
|
||||
return (s?.isEmpty == false) ? s : nil
|
||||
}
|
||||
|
||||
private func updateLastAckedJwtIdFromResponse(responseBody: String) {
|
||||
guard let data = responseBody.data(using: .utf8),
|
||||
let root = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
||||
let dataArray = root["data"] as? [[String: Any]], !dataArray.isEmpty
|
||||
else { return }
|
||||
|
||||
let lastItem = dataArray[dataArray.count - 1]
|
||||
var jwtId: String?
|
||||
if let j = lastItem["jwtId"] as? String {
|
||||
jwtId = j
|
||||
} else if let plan = lastItem["plan"] as? [String: Any], let j = plan["jwtId"] as? String {
|
||||
jwtId = j
|
||||
}
|
||||
if let jwtId = jwtId, !jwtId.isEmpty {
|
||||
UserDefaults.standard.set(jwtId, forKey: prefsLastAckedKey)
|
||||
}
|
||||
}
|
||||
|
||||
private func extractProjectDisplayTitle(_ item: [String: Any]) -> String {
|
||||
if let plan = item["plan"] as? [String: Any],
|
||||
let name = plan["name"] as? String,
|
||||
!name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
return name.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
}
|
||||
return "Unnamed Project"
|
||||
}
|
||||
|
||||
private func extractJwtIdFromItem(_ item: [String: Any]) -> String? {
|
||||
if let plan = item["plan"] as? [String: Any], let j = plan["jwtId"] as? String, !j.isEmpty {
|
||||
return j
|
||||
}
|
||||
if let j = item["jwtId"] as? String, !j.isEmpty { return j }
|
||||
return nil
|
||||
}
|
||||
|
||||
private func parseApiResponse(responseBody: String, context: FetchContext) -> [NotificationContent] {
|
||||
guard let data = responseBody.data(using: .utf8),
|
||||
let root = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
||||
let dataArray = root["data"] as? [[String: Any]], !dataArray.isEmpty
|
||||
else {
|
||||
return []
|
||||
}
|
||||
|
||||
let firstItem = dataArray[0]
|
||||
let firstTitle = extractProjectDisplayTitle(firstItem)
|
||||
let jwtId = extractJwtIdFromItem(firstItem)
|
||||
let nowMs = Int64(Date().timeIntervalSince1970 * 1000)
|
||||
let scheduledMs: Int64 = context.scheduledTimeMillis ?? (nowMs + 3_600_000)
|
||||
|
||||
let n = dataArray.count
|
||||
let quotedFirst = "\u{201C}\(firstTitle)\u{201D}"
|
||||
let title: String
|
||||
let body: String
|
||||
if n == 1 {
|
||||
title = "Starred Project Update"
|
||||
body = "\(quotedFirst) has been updated."
|
||||
} else {
|
||||
title = "Starred Project Updates"
|
||||
let more = n - 1
|
||||
body = "\(quotedFirst) + \(more) more have been updated."
|
||||
}
|
||||
|
||||
let id = "endorser_\(jwtId ?? "batch_\(nowMs)")"
|
||||
return [
|
||||
NotificationContent(
|
||||
id: id,
|
||||
title: title,
|
||||
body: body,
|
||||
scheduledTime: scheduledMs,
|
||||
fetchedAt: nowMs,
|
||||
url: apiBaseUrl,
|
||||
payload: nil,
|
||||
etag: nil
|
||||
),
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -86,7 +86,7 @@ PODS:
|
||||
- SQLCipher/common (4.9.0)
|
||||
- SQLCipher/standard (4.9.0):
|
||||
- SQLCipher/common
|
||||
- TimesafariDailyNotificationPlugin (2.1.1):
|
||||
- TimesafariDailyNotificationPlugin (3.0.0):
|
||||
- Capacitor
|
||||
- ZIPFoundation (0.9.19)
|
||||
|
||||
@@ -172,7 +172,7 @@ SPEC CHECKSUMS:
|
||||
nanopb: 438bc412db1928dac798aa6fd75726007be04262
|
||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||
SQLCipher: 31878d8ebd27e5c96db0b7cb695c96e9f8ad77da
|
||||
TimesafariDailyNotificationPlugin: ab9860e6ab9db8019f64f3c08f115a0c4ffd32d9
|
||||
TimesafariDailyNotificationPlugin: 4a344236630d9209234d46a417d351ac9c27e1b0
|
||||
ZIPFoundation: b8c29ea7ae353b309bc810586181fd073cb3312c
|
||||
|
||||
PODFILE CHECKSUM: 6d92bfa46c6c2d31d19b8c0c38f56a8ae9fd222f
|
||||
|
||||
154
package-lock.json
generated
154
package-lock.json
generated
@@ -370,9 +370,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-define-polyfill-provider": {
|
||||
"version": "0.6.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.7.tgz",
|
||||
"integrity": "sha512-6Fqi8MtQ/PweQ9xvux65emkLQ83uB+qAVtfHkC9UodyHMIZdxNI01HjLCLUtybElp2KY2XNE0nOgyP1E1vXw9w==",
|
||||
"version": "0.6.8",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.8.tgz",
|
||||
"integrity": "sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -4637,33 +4637,33 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@expo/cli": {
|
||||
"version": "55.0.18",
|
||||
"resolved": "https://registry.npmjs.org/@expo/cli/-/cli-55.0.18.tgz",
|
||||
"integrity": "sha512-3sJwu8KvCvQIXBnhUlHgLBZBe+ZK4Da9R5rgI4znaowJavYWMqzRClLzyE6Kri66WVoMX7Q4HUVIh8prRlO0XA==",
|
||||
"version": "55.0.19",
|
||||
"resolved": "https://registry.npmjs.org/@expo/cli/-/cli-55.0.19.tgz",
|
||||
"integrity": "sha512-PPNWwPXHcLDFgNNmkLmlLm3fLiNTxr7sbhNx4mXdjo0U/2Wg3rWaCeg1yMx49llOpDLZEWJpyAwPvTBqWc8glw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@expo/code-signing-certificates": "^0.0.6",
|
||||
"@expo/config": "~55.0.10",
|
||||
"@expo/config": "~55.0.11",
|
||||
"@expo/config-plugins": "~55.0.7",
|
||||
"@expo/devcert": "^1.2.1",
|
||||
"@expo/env": "~2.1.1",
|
||||
"@expo/image-utils": "^0.8.12",
|
||||
"@expo/json-file": "^10.0.12",
|
||||
"@expo/log-box": "55.0.7",
|
||||
"@expo/log-box": "55.0.8",
|
||||
"@expo/metro": "~54.2.0",
|
||||
"@expo/metro-config": "~55.0.11",
|
||||
"@expo/osascript": "^2.4.2",
|
||||
"@expo/package-manager": "^1.10.3",
|
||||
"@expo/plist": "^0.5.2",
|
||||
"@expo/prebuild-config": "^55.0.10",
|
||||
"@expo/prebuild-config": "^55.0.11",
|
||||
"@expo/require-utils": "^55.0.3",
|
||||
"@expo/router-server": "^55.0.11",
|
||||
"@expo/schema-utils": "^55.0.2",
|
||||
"@expo/spawn-async": "^1.7.2",
|
||||
"@expo/ws-tunnel": "^1.0.1",
|
||||
"@expo/xcpretty": "^4.4.0",
|
||||
"@react-native/dev-middleware": "0.83.2",
|
||||
"@react-native/dev-middleware": "0.83.4",
|
||||
"accepts": "^1.3.8",
|
||||
"arg": "^5.0.2",
|
||||
"better-opn": "~3.0.2",
|
||||
@@ -4772,9 +4772,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@expo/cli/node_modules/brace-expansion": {
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz",
|
||||
"integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==",
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
|
||||
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -4894,9 +4894,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@expo/cli/node_modules/lru-cache": {
|
||||
"version": "11.2.6",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz",
|
||||
"integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==",
|
||||
"version": "11.2.7",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz",
|
||||
"integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
@@ -4914,13 +4914,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@expo/cli/node_modules/minimatch": {
|
||||
"version": "10.2.4",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz",
|
||||
"integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==",
|
||||
"version": "10.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
|
||||
"integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"brace-expansion": "^5.0.2"
|
||||
"brace-expansion": "^5.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
@@ -5091,9 +5091,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@expo/config": {
|
||||
"version": "55.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@expo/config/-/config-55.0.10.tgz",
|
||||
"integrity": "sha512-qCHxo9H1ZoeW+y0QeMtVZ3JfGmumpGrgUFX60wLWMarraoQZSe47ZUm9kJSn3iyoPjUtUNanO3eXQg+K8k4rag==",
|
||||
"version": "55.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@expo/config/-/config-55.0.11.tgz",
|
||||
"integrity": "sha512-14AkSmR1gOIUhCsPJ0cAo5ZduMNsPQsmFV9jBNZn1xC5Zb3D8x5eqvUie5QzWaUwdcyrq79uYJ2bTCiC6+nD0Q==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -5642,9 +5642,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@expo/log-box": {
|
||||
"version": "55.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@expo/log-box/-/log-box-55.0.7.tgz",
|
||||
"integrity": "sha512-m7V1k2vlMp4NOj3fopjOg4zl/ANXyTRF3HMTMep2GZAKsPiDzgOQ41nm8CaU50/HlDIGXlCObss07gOn20UpHQ==",
|
||||
"version": "55.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@expo/log-box/-/log-box-55.0.8.tgz",
|
||||
"integrity": "sha512-WVEuW1XcntUdOpQk8k9cUymM5FHKmEcPr6QO9SVIin3WYk5FbbwHRYr1T6GfwWF0UA2s9w9heeYolesq99vFIw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -6099,18 +6099,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@expo/prebuild-config": {
|
||||
"version": "55.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@expo/prebuild-config/-/prebuild-config-55.0.10.tgz",
|
||||
"integrity": "sha512-AMylDld5G7YJGfEhEyXtgWRuBB83802QBoewF1vJ6NMDtufukuPhMJzOs9E4UXNsjLTaQcgT4yTWhsAWl7o1AQ==",
|
||||
"version": "55.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@expo/prebuild-config/-/prebuild-config-55.0.11.tgz",
|
||||
"integrity": "sha512-PqjbTTHXS0dnZMH4X5/0rnLxKfQqyN1s/5lmxITn+U6WDUNibatUepfjwV+5C2jU4hv5z2haqX6e9hQ0zUtDMA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@expo/config": "~55.0.10",
|
||||
"@expo/config": "~55.0.11",
|
||||
"@expo/config-plugins": "~55.0.7",
|
||||
"@expo/config-types": "^55.0.5",
|
||||
"@expo/image-utils": "^0.8.12",
|
||||
"@expo/json-file": "^10.0.12",
|
||||
"@react-native/normalize-colors": "0.83.2",
|
||||
"@react-native/normalize-colors": "0.83.4",
|
||||
"debug": "^4.3.1",
|
||||
"resolve-from": "^5.0.0",
|
||||
"semver": "^7.6.0",
|
||||
@@ -7670,23 +7670,23 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native/babel-plugin-codegen": {
|
||||
"version": "0.83.2",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.83.2.tgz",
|
||||
"integrity": "sha512-XbcN/BEa64pVlb0Hb/E/Ph2SepjVN/FcNKrJcQvtaKZA6mBSO8pW8Eircdlr61/KBH94LihHbQoQDzkQFpeaTg==",
|
||||
"version": "0.83.4",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.83.4.tgz",
|
||||
"integrity": "sha512-UFsK+c1rvT84XZfzpmwKePsc5nTr5LK7hh18TI0DooNlVcztDbMDsQZpDnhO/gmk7aTbWEqO5AB3HJ7tvGp+Jg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@babel/traverse": "^7.25.3",
|
||||
"@react-native/codegen": "0.83.2"
|
||||
"@react-native/codegen": "0.83.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 20.19.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native/babel-preset": {
|
||||
"version": "0.83.2",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.83.2.tgz",
|
||||
"integrity": "sha512-X/RAXDfe6W+om/Fw1i6htTxQXFhBJ2jgNOWx3WpI3KbjeIWbq7ib6vrpTeIAW2NUMg+K3mML1NzgD4dpZeqdjA==",
|
||||
"version": "0.83.4",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.83.4.tgz",
|
||||
"integrity": "sha512-SXPFn3Jp4gOzlBDnDOKPzMfxQPKJMYJs05EmEeFB/6km46xZ9l+2YKXwAwxfNhHnmwNf98U/bnVndU95I0TMCw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -7731,7 +7731,7 @@
|
||||
"@babel/plugin-transform-typescript": "^7.25.2",
|
||||
"@babel/plugin-transform-unicode-regex": "^7.24.7",
|
||||
"@babel/template": "^7.25.0",
|
||||
"@react-native/babel-plugin-codegen": "0.83.2",
|
||||
"@react-native/babel-plugin-codegen": "0.83.4",
|
||||
"babel-plugin-syntax-hermes-parser": "0.32.0",
|
||||
"babel-plugin-transform-flow-enums": "^0.0.2",
|
||||
"react-refresh": "^0.14.0"
|
||||
@@ -7771,9 +7771,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native/codegen": {
|
||||
"version": "0.83.2",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.83.2.tgz",
|
||||
"integrity": "sha512-9uK6X1miCXqtL4c759l74N/XbQeneWeQVjoV7SD2CGJuW7ZefxaoYenwGPs7rMoCdtS6wuIyR3hXQ+uWEBGYXA==",
|
||||
"version": "0.83.4",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.83.4.tgz",
|
||||
"integrity": "sha512-CJ7XutzIqJPz3Lp/5TOiRWlU/JAjTboMT1BHNLSXjYHXwTmgHM3iGEbpCOtBMjWvsojRTJyRO/G3ghInIIXEYg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -7954,9 +7954,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native/debugger-frontend": {
|
||||
"version": "0.83.2",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.83.2.tgz",
|
||||
"integrity": "sha512-t4fYfa7xopbUF5S4+ihNEwgaq4wLZLKLY0Ms8z72lkMteVd3bOX2Foxa8E2wTfRvdhPOkSpOsTeNDmD8ON4DoQ==",
|
||||
"version": "0.83.4",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.83.4.tgz",
|
||||
"integrity": "sha512-mCE2s/S7SEjax3gZb6LFAraAI3x13gRVWJWqT0HIm71e4ITObENNTDuMw4mvZ/wr4Gz2wv4FcBH5/Nla9LXOcg==",
|
||||
"license": "BSD-3-Clause",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
@@ -7964,9 +7964,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native/debugger-shell": {
|
||||
"version": "0.83.2",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/debugger-shell/-/debugger-shell-0.83.2.tgz",
|
||||
"integrity": "sha512-z9go6NJMsLSDJT5MW6VGugRsZHjYvUTwxtsVc3uLt4U9W6T3J6FWI2wHpXIzd2dUkXRfAiRQ3Zi8ZQQ8fRFg9A==",
|
||||
"version": "0.83.4",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/debugger-shell/-/debugger-shell-0.83.4.tgz",
|
||||
"integrity": "sha512-FtAnrvXqy1xeZ+onwilvxEeeBsvBlhtfrHVIC2R/BOJAK9TbKEtFfjio0wsn3DQIm+UZq48DSa+p9jJZ2aJUww==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -7978,15 +7978,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native/dev-middleware": {
|
||||
"version": "0.83.2",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.83.2.tgz",
|
||||
"integrity": "sha512-Zi4EVaAm28+icD19NN07Gh8Pqg/84QQu+jn4patfWKNkcToRFP5vPEbbp0eLOGWS+BVB1d1Fn5lvMrJsBbFcOg==",
|
||||
"version": "0.83.4",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.83.4.tgz",
|
||||
"integrity": "sha512-3s9nXZc/kj986nI2RPqxiIJeTS3o7pvZDxbHu7GE9WVIGX9YucA1l/tEiXd7BAm3TBFOfefDOT08xD46wH+R3Q==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@isaacs/ttlcache": "^1.4.1",
|
||||
"@react-native/debugger-frontend": "0.83.2",
|
||||
"@react-native/debugger-shell": "0.83.2",
|
||||
"@react-native/debugger-frontend": "0.83.4",
|
||||
"@react-native/debugger-shell": "0.83.4",
|
||||
"chrome-launcher": "^0.15.2",
|
||||
"chromium-edge-launcher": "^0.2.0",
|
||||
"connect": "^3.6.5",
|
||||
@@ -8086,9 +8086,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native/normalize-colors": {
|
||||
"version": "0.83.2",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.83.2.tgz",
|
||||
"integrity": "sha512-gkZAb9LoVVzNuYzzOviH7DiPTXQoZPHuiTH2+O2+VWNtOkiznjgvqpwYAhg58a5zfRq5GXlbBdf5mzRj5+3Y5Q==",
|
||||
"version": "0.83.4",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.83.4.tgz",
|
||||
"integrity": "sha512-9ezxaHjxqTkTOLg62SGg7YhFaE+fxa/jlrWP0nwf7eGFHlGOiTAaRR2KUfiN3K05e+EMbEhgcH/c7bgaXeGyJw==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
@@ -8685,8 +8685,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@timesafari/daily-notification-plugin": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "git+https://gitea.anomalistdesign.com/trent_larson/daily-notification-plugin.git#9121b1e0f7e1be50d00eb3e78d52e06816196697",
|
||||
"version": "3.0.0",
|
||||
"resolved": "git+https://gitea.anomalistdesign.com/trent_larson/daily-notification-plugin.git#fbb5a9407111f6f96e7862ab552f4c1af9a41ddf",
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
@@ -11209,14 +11209,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/babel-plugin-polyfill-corejs2": {
|
||||
"version": "0.4.16",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.16.tgz",
|
||||
"integrity": "sha512-xaVwwSfebXf0ooE11BJovZYKhFjIvQo7TsyVpETuIeH2JHv0k/T6Y5j22pPTvqYqmpkxdlPAJlyJ0tfOJAoMxw==",
|
||||
"version": "0.4.17",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.17.tgz",
|
||||
"integrity": "sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@babel/compat-data": "^7.28.6",
|
||||
"@babel/helper-define-polyfill-provider": "^0.6.7",
|
||||
"@babel/helper-define-polyfill-provider": "^0.6.8",
|
||||
"semver": "^6.3.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
@@ -11248,13 +11248,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/babel-plugin-polyfill-regenerator": {
|
||||
"version": "0.6.7",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.7.tgz",
|
||||
"integrity": "sha512-OTYbUlSwXhNgr4g6efMZgsO8//jA61P7ZbRX3iTT53VON8l+WQS8IAUEVo4a4cWknrg2W8Cj4gQhRYNCJ8GkAA==",
|
||||
"version": "0.6.8",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.8.tgz",
|
||||
"integrity": "sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-define-polyfill-provider": "^0.6.7"
|
||||
"@babel/helper-define-polyfill-provider": "^0.6.8"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
|
||||
@@ -11323,9 +11323,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/babel-preset-expo": {
|
||||
"version": "55.0.12",
|
||||
"resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-55.0.12.tgz",
|
||||
"integrity": "sha512-oR46ExGZpRijmPUsr0rFH5X4lR/mvwqJAFXJRLpynZcvyv2pHPTeGMNfd/p5oPMbdbaeMS6G+3k18p48u2Qjbw==",
|
||||
"version": "55.0.13",
|
||||
"resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-55.0.13.tgz",
|
||||
"integrity": "sha512-7m3Hpi6R1M+3u2LEU15OV59ATtbqz6kFvL6y9TaZTeOGLV28MFULawCQw3BtO/qMYUPz0vkH1OdbCuG7E2cTbg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -11345,7 +11345,7 @@
|
||||
"@babel/plugin-transform-runtime": "^7.24.7",
|
||||
"@babel/preset-react": "^7.22.15",
|
||||
"@babel/preset-typescript": "^7.23.0",
|
||||
"@react-native/babel-preset": "0.83.2",
|
||||
"@react-native/babel-preset": "0.83.4",
|
||||
"babel-plugin-react-compiler": "^1.0.0",
|
||||
"babel-plugin-react-native-web": "~0.21.0",
|
||||
"babel-plugin-syntax-hermes-parser": "^0.32.0",
|
||||
@@ -11356,7 +11356,7 @@
|
||||
"peerDependencies": {
|
||||
"@babel/runtime": "^7.20.0",
|
||||
"expo": "*",
|
||||
"expo-widgets": "^55.0.6",
|
||||
"expo-widgets": "^55.0.8",
|
||||
"react-refresh": ">=0.14.0 <1.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
@@ -13277,9 +13277,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/core-js-compat": {
|
||||
"version": "3.48.0",
|
||||
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.48.0.tgz",
|
||||
"integrity": "sha512-OM4cAF3D6VtH/WkLtWvyNC56EZVXsZdU3iqaMG2B4WvYrlqU831pc4UtG5yp0sE9z8Y02wVN7PjW5Zf9Gt0f1Q==",
|
||||
"version": "3.49.0",
|
||||
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.49.0.tgz",
|
||||
"integrity": "sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -15194,9 +15194,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/expo-file-system": {
|
||||
"version": "55.0.11",
|
||||
"resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-55.0.11.tgz",
|
||||
"integrity": "sha512-KMUd6OY375J9WD79ZvjvCDZMveT7YfgiGWdi58/gfuTBsr14TRuoPk8RRQHAtc4UquzWViKcHwna9aPY7/XPpw==",
|
||||
"version": "55.0.12",
|
||||
"resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-55.0.12.tgz",
|
||||
"integrity": "sha512-MFN/3L3gm174nxP2HqKQsSsPbjAj92wuidKFGSbl3Lt6oJTS09EbTwszX5BhYeeVSprcsw8pnlxYSmhkSqGEFw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peerDependencies": {
|
||||
|
||||
Reference in New Issue
Block a user