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

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

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

View File

@@ -61,16 +61,14 @@ The app depends on:
"@timesafari/daily-notification-plugin": "git+https://gitea.anomalistdesign.com/trent_larson/daily-notification-plugin.git#master" "@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. - `npm install` / `npm update` in the app would not pull the fixes.
- The apps `node_modules` would only have the fixes if they were copied/linked from the fixed repo. - The apps `node_modules` would only have the fixes if they were copied/linked from the fixed repo.
**Do this:** **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`: - **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.
- `"@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.
### 3. Fallback text from native fetcher (Bug 2 only) ### 3. Fallback text from native fetcher (Bug 2 only)

View 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`).

View File

@@ -6,8 +6,7 @@
2. **Notifications show when the app is in the foreground** (not only background/closed). 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. 3. **Plugin loads at app launch** so recovery runs after reboot without the user opening notification UI.
**Reference:** Test app at **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`).
`/Users/aardimus/Sites/trentlarson/daily-notification-plugin_test/daily-notification-plugin/test-apps/daily-notification-test`
--- ---

View File

@@ -2,9 +2,7 @@
**Purpose:** Describe what is required for **iOS** to match **Android** for the daily-notification-plugin **API-driven “New Activity”** flow (`scheduleDualNotification` / `cancelDualSchedule`, with prefetch and Endorser-backed content). The canonical product behavior is documented in `doc/notification-from-api-call.md` and `doc/notification-new-activity-lay-of-the-land.md`. **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 **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.
`/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.
--- ---
@@ -26,7 +24,7 @@ Platform differences (iOS **BGTaskScheduler** is opportunistic; Android **alarms
- **Host native fetcher:** `android/.../TimeSafariNativeFetcher.java` implements the plugins `NativeNotificationContentFetcher` and calls **`POST …/api/v2/report/plansLastUpdatedBetween`** using starred plan IDs (via plugin storage from `updateStarredPlans`). - **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))`. - **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 ### 3.1 This repository
- **No iOS `TimeSafariNativeFetcher`.** There is no Swift/Objective-C equivalent registered with the plugin (unlike Androids `setNativeFetcher`). - **iOS native fetcher:** `ios/App/App/TimeSafariNativeFetcher.swift` implements `NativeNotificationContentFetcher` (Endorser `plansLastUpdatedBetween`, same prefs keys as Java). **`AppDelegate`** calls `DailyNotificationPlugin.registerNativeFetcher(TimeSafariNativeFetcher.shared)` at launch **before** any `configureNativeFetcher` from JS (see plugin `doc/CONSUMING_APP_HANDOFF_IOS_NATIVE_FETCHER_AND_CHAINED_DUAL.md` and **`doc/consuming-app-handoff-ios-native-fetcher-chained-dual.md`**).
- **JS/TS is already shared:** `nativeFetcherConfig.ts`, `dualScheduleConfig.ts`, `syncStarredPlansToNativePlugin.ts`, and `AccountViewView.vue` call the same APIs on both platforms. - **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`). - **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. - **AppDelegate** posts `DailyNotificationDelivered` for foreground presentation—aligned with plugin rollover behavior.
### 3.2 Bundled plugin (`node_modules/@timesafari/daily-notification-plugin`, iOS) ### 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). - **`scheduleDualNotification` / `cancelDualSchedule`** — see plugin release notes; clean sync + `pod install` if you see `UNIMPLEMENTED` (`doc/plugin-feedback-ios-scheduleDualNotification.md`).
- **`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. - **`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).
- **`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`** — implemented on iOS in current plugin; persists **`daily_notification_timesafari.starredPlanIds`** for the host fetcher.
- **`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. - **Chained dual** — user notification is armed **after** prefetch for that cycle (plugin); iOS remains subject to BG scheduling limits; see **§3.3**.
- **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) ### 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 ## 4. Work breakdown
### 4A. Plugin (`daily-notification-plugin`) — required for real parity ### 4A. Plugin (`daily-notification-plugin`) — status (v3.x)
1. **`updateStarredPlans` on iOS** 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.
- 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`** 1. **`updateStarredPlans` on iOS** — shipped in current plugin.
- 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** 2. **iOS `plansLastUpdatedBetween` / host fetcher** — shipped: host registers **`TimeSafariNativeFetcher`** (Swift); plugin does not duplicate Endorser logic when a fetcher is registered.
- 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)** 3. **Dual schedule / chaining** — shipped (notify after prefetch; see plugin release notes and **§3.3**).
- 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.
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)** 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`). - **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 ### 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`). 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. **Re-test** `syncStarredPlansToNativePlugin`: once iOS exposes `updateStarredPlans`, remove reliance on “optional UNIMPLEMENTED” for correctness tests (the helper can stay defensive). 2. **iOS native fetcher****Done:** `TimeSafariNativeFetcher.swift` + `registerNativeFetcher` in `AppDelegate` (see handoff doc).
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`). 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`. 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) ### 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 | | 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` | | Feature plan & file list | `doc/notification-from-api-call.md` |
| Dual vs Daily Reminder confusion | `doc/notification-new-activity-lay-of-the-land.md` | | Dual vs Daily Reminder confusion | `doc/notification-new-activity-lay-of-the-land.md` |
| iOS `UNIMPLEMENTED` / PluginHeaders | `doc/plugin-feedback-ios-scheduleDualNotification.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`). - **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). - **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). - **JSON body:** `planIds` (array of strings, possibly empty), `afterId` (string; use `"0"` if none stored).
- **Starred plans:** Persist `updateStarredPlans` the same way Androids plugin + host expect: SharedPreferences name **`daily_notification_timesafari`**, key **`starredPlanIds`** (JSON array string). iOS should use **the same keys** if using `UserDefaults` / app group so behavior matches the Java fetcher and plugin docs. - **Starred plans:** Android: SharedPreferences **`daily_notification_timesafari`** + key **`starredPlanIds`**. iOS (plugin + host): `UserDefaults.standard` key **`daily_notification_timesafari.starredPlanIds`** (JSON array string).
- **Pagination:** After a successful response with non-empty `data`, update **`last_acked_jwt_id`** from the last rows `jwtId` (item or nested `plan.jwtId`)—see Java `updateLastAckedJwtIdFromResponse`. - **Pagination:** After a successful response with non-empty `data`, update **`last_acked_jwt_id`** from the last rows `jwtId` (item or nested `plan.jwtId`)—see Java `updateLastAckedJwtIdFromResponse`. iOS host (`TimeSafariNativeFetcher.swift`) persists **`daily_notification_timesafari.last_acked_jwt_id`** in `UserDefaults.standard`.
- **Empty `data`:** Return **no** notification items (empty list); do not synthesize a “no updates” push from an empty result—Java returns empty `contents` when `data` is absent or empty. - **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`). - **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. - **iOS:** `ios/Plugin/DailyNotificationPlugin.swift`, `DailyNotificationScheduleHelper.swift`, native fetcher registry, BG / UN paths.
- **Android:** `DailyNotificationPlugin.kt`, `FetchWorker` / `DailyNotificationFetchWorker`, `ScheduleHelper`per dual-schedule feedback doc. - **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). 1. Tag / publish **`@timesafari/daily-notification-plugin`**.
2. **iOS** `updateStarredPlans` + **`plansLastUpdatedBetween`** parity (or **iOS `setNativeFetcher`** + thin Swift adapter). 2. **Consuming app:** bump, `npm install`, `npx cap sync`, `pod install`, QA (`doc/consuming-app-handoff-ios-native-fetcher-chained-dual.md`).
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`.
--- ---

View File

@@ -101,5 +101,8 @@ Add a short “New Activity notifications” section to BUILDING.md or a user-fa
| Settings type | `src/interfaces/accountView.ts` | | Settings type | `src/interfaces/accountView.ts` |
| Android native fetcher | `android/app/src/main/java/app/timesafari/TimeSafariNativeFetcher.java` | | Android native fetcher | `android/app/src/main/java/app/timesafari/TimeSafariNativeFetcher.java` |
| Android registration | `android/app/src/main/java/app/timesafari/MainActivity.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` |

View File

@@ -220,7 +220,7 @@ The steps and expected notification copy below are **Android-specific**: this re
## 8. Plugin Repo Alignment and Attention Items ## 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 ### 8.1 What lines up

View File

@@ -2,7 +2,7 @@
**Date:** 2026-02-18 **Date:** 2026-02-18
**Generated:** 2026-02-18 17:47:06 PST **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) **Consuming app:** crowd-funder-for-time-pwa (TimeSafari)
**Platform:** Android **Platform:** Android

View File

@@ -18,6 +18,7 @@
C86585DF2ED456DE00824752 /* TimeSafariShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = C86585D52ED456DE00824752 /* TimeSafariShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 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 */; }; C8C56E142EE0474B00737D0E /* SharedImageUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8C56E132EE0474B00737D0E /* SharedImageUtility.swift */; };
C8C56E162EE064CB00737D0E /* SharedImagePlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8C56E152EE064CA00737D0E /* SharedImagePlugin.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 */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
@@ -59,6 +60,7 @@
C86585E52ED4577F00824752 /* App.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = App.entitlements; sourceTree = "<group>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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 */ /* End PBXFileReference section */
@@ -140,6 +142,7 @@
children = ( children = (
C8C56E152EE064CA00737D0E /* SharedImagePlugin.swift */, C8C56E152EE064CA00737D0E /* SharedImagePlugin.swift */,
C8C56E132EE0474B00737D0E /* SharedImageUtility.swift */, C8C56E132EE0474B00737D0E /* SharedImageUtility.swift */,
A3F8E2D91B4C5E60718293A4 /* TimeSafariNativeFetcher.swift */,
C86585E52ED4577F00824752 /* App.entitlements */, C86585E52ED4577F00824752 /* App.entitlements */,
50379B222058CBB4000EE86E /* capacitor.config.json */, 50379B222058CBB4000EE86E /* capacitor.config.json */,
504EC3071FED79650016851F /* AppDelegate.swift */, 504EC3071FED79650016851F /* AppDelegate.swift */,
@@ -359,6 +362,7 @@
C8C56E162EE064CB00737D0E /* SharedImagePlugin.swift in Sources */, C8C56E162EE064CB00737D0E /* SharedImagePlugin.swift in Sources */,
504EC3081FED79650016851F /* AppDelegate.swift in Sources */, 504EC3081FED79650016851F /* AppDelegate.swift in Sources */,
C8C56E142EE0474B00737D0E /* SharedImageUtility.swift in Sources */, C8C56E142EE0474B00737D0E /* SharedImageUtility.swift in Sources */,
B7E1C4F82A9D3E506F1B2C8D /* TimeSafariNativeFetcher.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };

View File

@@ -1,6 +1,7 @@
import UIKit import UIKit
import Capacitor import Capacitor
import CapacitorCommunitySqlite import CapacitorCommunitySqlite
import TimesafariDailyNotificationPlugin
import UserNotifications import UserNotifications
@UIApplicationMain @UIApplicationMain
@@ -9,6 +10,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
var window: UIWindow? var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 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 // Set notification center delegate so notifications show in foreground and rollover is triggered
UNUserNotificationCenter.current().delegate = self UNUserNotificationCenter.current().delegate = self
@@ -89,13 +93,20 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
/// Show notifications when app is in foreground and post DailyNotificationDelivered for rollover. /// Show notifications when app is in foreground and post DailyNotificationDelivered for rollover.
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
let userInfo = notification.request.content.userInfo let userInfo = notification.request.content.userInfo
if let notificationId = userInfo["notification_id"] as? String, if let notificationId = userInfo["notification_id"] as? String {
let scheduledTime = userInfo["scheduled_time"] as? Int64 { let scheduledTime: Int64? = {
NotificationCenter.default.post( if let v = userInfo["scheduled_time"] as? Int64 { return v }
name: NSNotification.Name("DailyNotificationDelivered"), if let n = userInfo["scheduled_time"] as? NSNumber { return n.int64Value }
object: nil, if let i = userInfo["scheduled_time"] as? Int { return Int64(i) }
userInfo: ["notification_id": notificationId, "scheduled_time": scheduledTime] 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, *) { if #available(iOS 14.0, *) {
completionHandler([.banner, .sound, .badge]) completionHandler([.banner, .sound, .badge])

View 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
),
]
}
}

View File

@@ -86,7 +86,7 @@ PODS:
- SQLCipher/common (4.9.0) - SQLCipher/common (4.9.0)
- SQLCipher/standard (4.9.0): - SQLCipher/standard (4.9.0):
- SQLCipher/common - SQLCipher/common
- TimesafariDailyNotificationPlugin (2.1.1): - TimesafariDailyNotificationPlugin (3.0.0):
- Capacitor - Capacitor
- ZIPFoundation (0.9.19) - ZIPFoundation (0.9.19)
@@ -172,7 +172,7 @@ SPEC CHECKSUMS:
nanopb: 438bc412db1928dac798aa6fd75726007be04262 nanopb: 438bc412db1928dac798aa6fd75726007be04262
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
SQLCipher: 31878d8ebd27e5c96db0b7cb695c96e9f8ad77da SQLCipher: 31878d8ebd27e5c96db0b7cb695c96e9f8ad77da
TimesafariDailyNotificationPlugin: ab9860e6ab9db8019f64f3c08f115a0c4ffd32d9 TimesafariDailyNotificationPlugin: 4a344236630d9209234d46a417d351ac9c27e1b0
ZIPFoundation: b8c29ea7ae353b309bc810586181fd073cb3312c ZIPFoundation: b8c29ea7ae353b309bc810586181fd073cb3312c
PODFILE CHECKSUM: 6d92bfa46c6c2d31d19b8c0c38f56a8ae9fd222f PODFILE CHECKSUM: 6d92bfa46c6c2d31d19b8c0c38f56a8ae9fd222f

154
package-lock.json generated
View File

@@ -370,9 +370,9 @@
} }
}, },
"node_modules/@babel/helper-define-polyfill-provider": { "node_modules/@babel/helper-define-polyfill-provider": {
"version": "0.6.7", "version": "0.6.8",
"resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.7.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.8.tgz",
"integrity": "sha512-6Fqi8MtQ/PweQ9xvux65emkLQ83uB+qAVtfHkC9UodyHMIZdxNI01HjLCLUtybElp2KY2XNE0nOgyP1E1vXw9w==", "integrity": "sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA==",
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
@@ -4637,33 +4637,33 @@
} }
}, },
"node_modules/@expo/cli": { "node_modules/@expo/cli": {
"version": "55.0.18", "version": "55.0.19",
"resolved": "https://registry.npmjs.org/@expo/cli/-/cli-55.0.18.tgz", "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-55.0.19.tgz",
"integrity": "sha512-3sJwu8KvCvQIXBnhUlHgLBZBe+ZK4Da9R5rgI4znaowJavYWMqzRClLzyE6Kri66WVoMX7Q4HUVIh8prRlO0XA==", "integrity": "sha512-PPNWwPXHcLDFgNNmkLmlLm3fLiNTxr7sbhNx4mXdjo0U/2Wg3rWaCeg1yMx49llOpDLZEWJpyAwPvTBqWc8glw==",
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
"@expo/code-signing-certificates": "^0.0.6", "@expo/code-signing-certificates": "^0.0.6",
"@expo/config": "~55.0.10", "@expo/config": "~55.0.11",
"@expo/config-plugins": "~55.0.7", "@expo/config-plugins": "~55.0.7",
"@expo/devcert": "^1.2.1", "@expo/devcert": "^1.2.1",
"@expo/env": "~2.1.1", "@expo/env": "~2.1.1",
"@expo/image-utils": "^0.8.12", "@expo/image-utils": "^0.8.12",
"@expo/json-file": "^10.0.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": "~54.2.0",
"@expo/metro-config": "~55.0.11", "@expo/metro-config": "~55.0.11",
"@expo/osascript": "^2.4.2", "@expo/osascript": "^2.4.2",
"@expo/package-manager": "^1.10.3", "@expo/package-manager": "^1.10.3",
"@expo/plist": "^0.5.2", "@expo/plist": "^0.5.2",
"@expo/prebuild-config": "^55.0.10", "@expo/prebuild-config": "^55.0.11",
"@expo/require-utils": "^55.0.3", "@expo/require-utils": "^55.0.3",
"@expo/router-server": "^55.0.11", "@expo/router-server": "^55.0.11",
"@expo/schema-utils": "^55.0.2", "@expo/schema-utils": "^55.0.2",
"@expo/spawn-async": "^1.7.2", "@expo/spawn-async": "^1.7.2",
"@expo/ws-tunnel": "^1.0.1", "@expo/ws-tunnel": "^1.0.1",
"@expo/xcpretty": "^4.4.0", "@expo/xcpretty": "^4.4.0",
"@react-native/dev-middleware": "0.83.2", "@react-native/dev-middleware": "0.83.4",
"accepts": "^1.3.8", "accepts": "^1.3.8",
"arg": "^5.0.2", "arg": "^5.0.2",
"better-opn": "~3.0.2", "better-opn": "~3.0.2",
@@ -4772,9 +4772,9 @@
} }
}, },
"node_modules/@expo/cli/node_modules/brace-expansion": { "node_modules/@expo/cli/node_modules/brace-expansion": {
"version": "5.0.4", "version": "5.0.5",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
"integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
@@ -4894,9 +4894,9 @@
} }
}, },
"node_modules/@expo/cli/node_modules/lru-cache": { "node_modules/@expo/cli/node_modules/lru-cache": {
"version": "11.2.6", "version": "11.2.7",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz",
"integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==",
"license": "BlueOak-1.0.0", "license": "BlueOak-1.0.0",
"optional": true, "optional": true,
"engines": { "engines": {
@@ -4914,13 +4914,13 @@
} }
}, },
"node_modules/@expo/cli/node_modules/minimatch": { "node_modules/@expo/cli/node_modules/minimatch": {
"version": "10.2.4", "version": "10.2.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
"integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
"license": "BlueOak-1.0.0", "license": "BlueOak-1.0.0",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
"brace-expansion": "^5.0.2" "brace-expansion": "^5.0.5"
}, },
"engines": { "engines": {
"node": "18 || 20 || >=22" "node": "18 || 20 || >=22"
@@ -5091,9 +5091,9 @@
} }
}, },
"node_modules/@expo/config": { "node_modules/@expo/config": {
"version": "55.0.10", "version": "55.0.11",
"resolved": "https://registry.npmjs.org/@expo/config/-/config-55.0.10.tgz", "resolved": "https://registry.npmjs.org/@expo/config/-/config-55.0.11.tgz",
"integrity": "sha512-qCHxo9H1ZoeW+y0QeMtVZ3JfGmumpGrgUFX60wLWMarraoQZSe47ZUm9kJSn3iyoPjUtUNanO3eXQg+K8k4rag==", "integrity": "sha512-14AkSmR1gOIUhCsPJ0cAo5ZduMNsPQsmFV9jBNZn1xC5Zb3D8x5eqvUie5QzWaUwdcyrq79uYJ2bTCiC6+nD0Q==",
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
@@ -5642,9 +5642,9 @@
} }
}, },
"node_modules/@expo/log-box": { "node_modules/@expo/log-box": {
"version": "55.0.7", "version": "55.0.8",
"resolved": "https://registry.npmjs.org/@expo/log-box/-/log-box-55.0.7.tgz", "resolved": "https://registry.npmjs.org/@expo/log-box/-/log-box-55.0.8.tgz",
"integrity": "sha512-m7V1k2vlMp4NOj3fopjOg4zl/ANXyTRF3HMTMep2GZAKsPiDzgOQ41nm8CaU50/HlDIGXlCObss07gOn20UpHQ==", "integrity": "sha512-WVEuW1XcntUdOpQk8k9cUymM5FHKmEcPr6QO9SVIin3WYk5FbbwHRYr1T6GfwWF0UA2s9w9heeYolesq99vFIw==",
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
@@ -6099,18 +6099,18 @@
} }
}, },
"node_modules/@expo/prebuild-config": { "node_modules/@expo/prebuild-config": {
"version": "55.0.10", "version": "55.0.11",
"resolved": "https://registry.npmjs.org/@expo/prebuild-config/-/prebuild-config-55.0.10.tgz", "resolved": "https://registry.npmjs.org/@expo/prebuild-config/-/prebuild-config-55.0.11.tgz",
"integrity": "sha512-AMylDld5G7YJGfEhEyXtgWRuBB83802QBoewF1vJ6NMDtufukuPhMJzOs9E4UXNsjLTaQcgT4yTWhsAWl7o1AQ==", "integrity": "sha512-PqjbTTHXS0dnZMH4X5/0rnLxKfQqyN1s/5lmxITn+U6WDUNibatUepfjwV+5C2jU4hv5z2haqX6e9hQ0zUtDMA==",
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
"@expo/config": "~55.0.10", "@expo/config": "~55.0.11",
"@expo/config-plugins": "~55.0.7", "@expo/config-plugins": "~55.0.7",
"@expo/config-types": "^55.0.5", "@expo/config-types": "^55.0.5",
"@expo/image-utils": "^0.8.12", "@expo/image-utils": "^0.8.12",
"@expo/json-file": "^10.0.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", "debug": "^4.3.1",
"resolve-from": "^5.0.0", "resolve-from": "^5.0.0",
"semver": "^7.6.0", "semver": "^7.6.0",
@@ -7670,23 +7670,23 @@
} }
}, },
"node_modules/@react-native/babel-plugin-codegen": { "node_modules/@react-native/babel-plugin-codegen": {
"version": "0.83.2", "version": "0.83.4",
"resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.83.2.tgz", "resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.83.4.tgz",
"integrity": "sha512-XbcN/BEa64pVlb0Hb/E/Ph2SepjVN/FcNKrJcQvtaKZA6mBSO8pW8Eircdlr61/KBH94LihHbQoQDzkQFpeaTg==", "integrity": "sha512-UFsK+c1rvT84XZfzpmwKePsc5nTr5LK7hh18TI0DooNlVcztDbMDsQZpDnhO/gmk7aTbWEqO5AB3HJ7tvGp+Jg==",
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
"@babel/traverse": "^7.25.3", "@babel/traverse": "^7.25.3",
"@react-native/codegen": "0.83.2" "@react-native/codegen": "0.83.4"
}, },
"engines": { "engines": {
"node": ">= 20.19.4" "node": ">= 20.19.4"
} }
}, },
"node_modules/@react-native/babel-preset": { "node_modules/@react-native/babel-preset": {
"version": "0.83.2", "version": "0.83.4",
"resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.83.2.tgz", "resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.83.4.tgz",
"integrity": "sha512-X/RAXDfe6W+om/Fw1i6htTxQXFhBJ2jgNOWx3WpI3KbjeIWbq7ib6vrpTeIAW2NUMg+K3mML1NzgD4dpZeqdjA==", "integrity": "sha512-SXPFn3Jp4gOzlBDnDOKPzMfxQPKJMYJs05EmEeFB/6km46xZ9l+2YKXwAwxfNhHnmwNf98U/bnVndU95I0TMCw==",
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
@@ -7731,7 +7731,7 @@
"@babel/plugin-transform-typescript": "^7.25.2", "@babel/plugin-transform-typescript": "^7.25.2",
"@babel/plugin-transform-unicode-regex": "^7.24.7", "@babel/plugin-transform-unicode-regex": "^7.24.7",
"@babel/template": "^7.25.0", "@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-syntax-hermes-parser": "0.32.0",
"babel-plugin-transform-flow-enums": "^0.0.2", "babel-plugin-transform-flow-enums": "^0.0.2",
"react-refresh": "^0.14.0" "react-refresh": "^0.14.0"
@@ -7771,9 +7771,9 @@
} }
}, },
"node_modules/@react-native/codegen": { "node_modules/@react-native/codegen": {
"version": "0.83.2", "version": "0.83.4",
"resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.83.2.tgz", "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.83.4.tgz",
"integrity": "sha512-9uK6X1miCXqtL4c759l74N/XbQeneWeQVjoV7SD2CGJuW7ZefxaoYenwGPs7rMoCdtS6wuIyR3hXQ+uWEBGYXA==", "integrity": "sha512-CJ7XutzIqJPz3Lp/5TOiRWlU/JAjTboMT1BHNLSXjYHXwTmgHM3iGEbpCOtBMjWvsojRTJyRO/G3ghInIIXEYg==",
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
@@ -7954,9 +7954,9 @@
} }
}, },
"node_modules/@react-native/debugger-frontend": { "node_modules/@react-native/debugger-frontend": {
"version": "0.83.2", "version": "0.83.4",
"resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.83.2.tgz", "resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.83.4.tgz",
"integrity": "sha512-t4fYfa7xopbUF5S4+ihNEwgaq4wLZLKLY0Ms8z72lkMteVd3bOX2Foxa8E2wTfRvdhPOkSpOsTeNDmD8ON4DoQ==", "integrity": "sha512-mCE2s/S7SEjax3gZb6LFAraAI3x13gRVWJWqT0HIm71e4ITObENNTDuMw4mvZ/wr4Gz2wv4FcBH5/Nla9LXOcg==",
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"optional": true, "optional": true,
"engines": { "engines": {
@@ -7964,9 +7964,9 @@
} }
}, },
"node_modules/@react-native/debugger-shell": { "node_modules/@react-native/debugger-shell": {
"version": "0.83.2", "version": "0.83.4",
"resolved": "https://registry.npmjs.org/@react-native/debugger-shell/-/debugger-shell-0.83.2.tgz", "resolved": "https://registry.npmjs.org/@react-native/debugger-shell/-/debugger-shell-0.83.4.tgz",
"integrity": "sha512-z9go6NJMsLSDJT5MW6VGugRsZHjYvUTwxtsVc3uLt4U9W6T3J6FWI2wHpXIzd2dUkXRfAiRQ3Zi8ZQQ8fRFg9A==", "integrity": "sha512-FtAnrvXqy1xeZ+onwilvxEeeBsvBlhtfrHVIC2R/BOJAK9TbKEtFfjio0wsn3DQIm+UZq48DSa+p9jJZ2aJUww==",
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
@@ -7978,15 +7978,15 @@
} }
}, },
"node_modules/@react-native/dev-middleware": { "node_modules/@react-native/dev-middleware": {
"version": "0.83.2", "version": "0.83.4",
"resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.83.2.tgz", "resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.83.4.tgz",
"integrity": "sha512-Zi4EVaAm28+icD19NN07Gh8Pqg/84QQu+jn4patfWKNkcToRFP5vPEbbp0eLOGWS+BVB1d1Fn5lvMrJsBbFcOg==", "integrity": "sha512-3s9nXZc/kj986nI2RPqxiIJeTS3o7pvZDxbHu7GE9WVIGX9YucA1l/tEiXd7BAm3TBFOfefDOT08xD46wH+R3Q==",
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
"@isaacs/ttlcache": "^1.4.1", "@isaacs/ttlcache": "^1.4.1",
"@react-native/debugger-frontend": "0.83.2", "@react-native/debugger-frontend": "0.83.4",
"@react-native/debugger-shell": "0.83.2", "@react-native/debugger-shell": "0.83.4",
"chrome-launcher": "^0.15.2", "chrome-launcher": "^0.15.2",
"chromium-edge-launcher": "^0.2.0", "chromium-edge-launcher": "^0.2.0",
"connect": "^3.6.5", "connect": "^3.6.5",
@@ -8086,9 +8086,9 @@
} }
}, },
"node_modules/@react-native/normalize-colors": { "node_modules/@react-native/normalize-colors": {
"version": "0.83.2", "version": "0.83.4",
"resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.83.2.tgz", "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.83.4.tgz",
"integrity": "sha512-gkZAb9LoVVzNuYzzOviH7DiPTXQoZPHuiTH2+O2+VWNtOkiznjgvqpwYAhg58a5zfRq5GXlbBdf5mzRj5+3Y5Q==", "integrity": "sha512-9ezxaHjxqTkTOLg62SGg7YhFaE+fxa/jlrWP0nwf7eGFHlGOiTAaRR2KUfiN3K05e+EMbEhgcH/c7bgaXeGyJw==",
"license": "MIT", "license": "MIT",
"optional": true "optional": true
}, },
@@ -8685,8 +8685,8 @@
} }
}, },
"node_modules/@timesafari/daily-notification-plugin": { "node_modules/@timesafari/daily-notification-plugin": {
"version": "2.2.0", "version": "3.0.0",
"resolved": "git+https://gitea.anomalistdesign.com/trent_larson/daily-notification-plugin.git#9121b1e0f7e1be50d00eb3e78d52e06816196697", "resolved": "git+https://gitea.anomalistdesign.com/trent_larson/daily-notification-plugin.git#fbb5a9407111f6f96e7862ab552f4c1af9a41ddf",
"license": "MIT", "license": "MIT",
"workspaces": [ "workspaces": [
"packages/*" "packages/*"
@@ -11209,14 +11209,14 @@
} }
}, },
"node_modules/babel-plugin-polyfill-corejs2": { "node_modules/babel-plugin-polyfill-corejs2": {
"version": "0.4.16", "version": "0.4.17",
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.16.tgz", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.17.tgz",
"integrity": "sha512-xaVwwSfebXf0ooE11BJovZYKhFjIvQo7TsyVpETuIeH2JHv0k/T6Y5j22pPTvqYqmpkxdlPAJlyJ0tfOJAoMxw==", "integrity": "sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w==",
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
"@babel/compat-data": "^7.28.6", "@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" "semver": "^6.3.1"
}, },
"peerDependencies": { "peerDependencies": {
@@ -11248,13 +11248,13 @@
} }
}, },
"node_modules/babel-plugin-polyfill-regenerator": { "node_modules/babel-plugin-polyfill-regenerator": {
"version": "0.6.7", "version": "0.6.8",
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.7.tgz", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.8.tgz",
"integrity": "sha512-OTYbUlSwXhNgr4g6efMZgsO8//jA61P7ZbRX3iTT53VON8l+WQS8IAUEVo4a4cWknrg2W8Cj4gQhRYNCJ8GkAA==", "integrity": "sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg==",
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
"@babel/helper-define-polyfill-provider": "^0.6.7" "@babel/helper-define-polyfill-provider": "^0.6.8"
}, },
"peerDependencies": { "peerDependencies": {
"@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
@@ -11323,9 +11323,9 @@
} }
}, },
"node_modules/babel-preset-expo": { "node_modules/babel-preset-expo": {
"version": "55.0.12", "version": "55.0.13",
"resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-55.0.12.tgz", "resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-55.0.13.tgz",
"integrity": "sha512-oR46ExGZpRijmPUsr0rFH5X4lR/mvwqJAFXJRLpynZcvyv2pHPTeGMNfd/p5oPMbdbaeMS6G+3k18p48u2Qjbw==", "integrity": "sha512-7m3Hpi6R1M+3u2LEU15OV59ATtbqz6kFvL6y9TaZTeOGLV28MFULawCQw3BtO/qMYUPz0vkH1OdbCuG7E2cTbg==",
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
@@ -11345,7 +11345,7 @@
"@babel/plugin-transform-runtime": "^7.24.7", "@babel/plugin-transform-runtime": "^7.24.7",
"@babel/preset-react": "^7.22.15", "@babel/preset-react": "^7.22.15",
"@babel/preset-typescript": "^7.23.0", "@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-compiler": "^1.0.0",
"babel-plugin-react-native-web": "~0.21.0", "babel-plugin-react-native-web": "~0.21.0",
"babel-plugin-syntax-hermes-parser": "^0.32.0", "babel-plugin-syntax-hermes-parser": "^0.32.0",
@@ -11356,7 +11356,7 @@
"peerDependencies": { "peerDependencies": {
"@babel/runtime": "^7.20.0", "@babel/runtime": "^7.20.0",
"expo": "*", "expo": "*",
"expo-widgets": "^55.0.6", "expo-widgets": "^55.0.8",
"react-refresh": ">=0.14.0 <1.0.0" "react-refresh": ">=0.14.0 <1.0.0"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
@@ -13277,9 +13277,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/core-js-compat": { "node_modules/core-js-compat": {
"version": "3.48.0", "version": "3.49.0",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.48.0.tgz", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.49.0.tgz",
"integrity": "sha512-OM4cAF3D6VtH/WkLtWvyNC56EZVXsZdU3iqaMG2B4WvYrlqU831pc4UtG5yp0sE9z8Y02wVN7PjW5Zf9Gt0f1Q==", "integrity": "sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==",
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
@@ -15194,9 +15194,9 @@
} }
}, },
"node_modules/expo-file-system": { "node_modules/expo-file-system": {
"version": "55.0.11", "version": "55.0.12",
"resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-55.0.11.tgz", "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-55.0.12.tgz",
"integrity": "sha512-KMUd6OY375J9WD79ZvjvCDZMveT7YfgiGWdi58/gfuTBsr14TRuoPk8RRQHAtc4UquzWViKcHwna9aPY7/XPpw==", "integrity": "sha512-MFN/3L3gm174nxP2HqKQsSsPbjAj92wuidKFGSbl3Lt6oJTS09EbTwszX5BhYeeVSprcsw8pnlxYSmhkSqGEFw==",
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"peerDependencies": { "peerDependencies": {