# iOS Test App Requirements **Purpose:** What the iOS test app must provide so that the testing guide can be executed with parity vs Android **Version:** 1.0.1 **Scope:** Phase 1 Prefetch MVP **Next Target:** Phase 2 (Rolling Window + TTL Telemetry) **Maintainer:** Matthew Raymer **Status:** 📋 **REQUIRED FOR PHASE 1** **Date:** 2025-11-15 **Directive Reference:** `doc/directives/0003-iOS-Android-Parity-Directive.md` **Note:** This app exists to support the prefetch testing scenarios in `doc/test-app-ios/IOS_PREFETCH_TESTING.md`. **Android parity:** Behavior is aligned with `test-apps/android-test-app` where platform constraints allow. Timing and BGTask heuristics **will differ** from Android's exact alarms: - **Android:** Exact alarms via AlarmManager / WorkManager - **iOS:** Heuristic BGTaskScheduler (see glossary); no hard guarantee of 5-min prefetch **Glossary:** See `doc/test-app-ios/IOS_PREFETCH_GLOSSARY.md` for complete terminology definitions. --- ## Overview This document defines the requirements for the iOS test app (`test-apps/ios-test-app/`) that must be created as part of Phase 1 implementation. The iOS test app must provide UI parity with the Android test app (`test-apps/android-test-app/`) while respecting iOS-specific constraints and capabilities. ## Non-Goals (Phase 1) **Out of scope for Phase 1:** - ❌ No full UX polish (color, branding) - ❌ No localization / accessibility guarantees (text only for internal QA) - ❌ No production signing / App Store deployment - ❌ No advanced UI features beyond basic functionality testing ## Security & Privacy Constraints **Critical requirements for test app implementation:** - Test app MUST use **non-production** endpoints and credentials - JWT / API keys used here are **test-only** and may be rotated or revoked at any time - Logs MUST NOT include real user PII (names, emails, phone numbers) - Any screenshots or shared logs should be scrubbed of secrets before external sharing - Test app should clearly indicate it's a development/testing build (not production) ## If You Only Have 30 Minutes Quick setup checklist: 1. Copy HTML/JS from Android test app (`test-apps/android-test-app/app/src/main/assets/public/index.html`) 2. Wire plugin into Capacitor (`capacitor.config.json`) 3. Add Info.plist keys (BGTask identifiers, background modes, notification permissions) 4. Build/run (`./scripts/build-ios-test-app.sh --simulator` or Xcode) 5. Press buttons: Check Plugin Status → Request Permissions → Schedule Test Notification 6. See logs with prefixes `DNP-PLUGIN`, `DNP-FETCH`, `DNP-SCHEDULER` --- ## UI Parity Requirements ### HTML/JS UI The iOS test app **MUST** use the same HTML/JS UI as the Android test app to ensure consistent testing experience across platforms. **Source:** Copy from `test-apps/android-test-app/app/src/main/assets/public/index.html` **Required UI Elements:** - Plugin registration status indicator - Permission status display (✅/❌ indicators) - Test notification button - Check permissions button - Request permissions button - Channel management buttons (Check Channel Status, Open Channel Settings) - Status display area - Log output area (optional, for debugging) ### UI Functionality The test app UI must support: 1. **Plugin Status Check** - Display plugin availability status - Show "Plugin is loaded and ready!" when available 2. **Permission Management** - Display current permission status - Request permissions button - Check permissions button - Show ✅/❌ indicators for each permission 3. **Channel Management** (iOS parity with Android) - Check channel status button (iOS: checks app-wide notification authorization) - Open channel settings button (iOS: opens app Settings, not per-channel) - Note: iOS doesn't have per-channel control like Android; these methods provide app-wide equivalents 4. **Notification Testing** - Schedule test notification button - Display scheduled time - Show notification status 5. **Status Display** - Show last notification time - Show pending notification count - Display error messages if any ### UI Elements to Plugin Methods Mapping | UI Element / Button | Plugin Method / API Call | Notes | |---------------------|-------------------------|-------| | "Check Plugin Status" | `DailyNotification.configure()` or status call | Verify plugin load & config | | "Check Permissions" | `checkPermissionStatus()` | Returns current notification permission status | | "Request Permissions" | `requestNotificationPermissions()` | Requests notification permissions (shows system dialog) | | "Schedule Test Notification" | `scheduleDailyNotification()` | Should schedule prefetch + notify | | "Show Last Notification" | `getLastNotification()` | Uses deterministic path (Bucket A) | | "Cancel All Notifications" | `cancelAllNotifications()` | Uses deterministic path (Bucket A) | | "Get Notification Status" | `getNotificationStatus()` | Uses deterministic path (Bucket A) | | "Check Channel Status" | `isChannelEnabled(channelId?)` | Checks if notifications enabled (iOS: app-wide) | | "Open Channel Settings" | `openChannelSettings(channelId?)` | Opens notification settings (iOS: app Settings) | **See `IOS_PREFETCH_TESTING.md` Behavior Classification for deterministic vs heuristic methods.** --- ## iOS Permissions Configuration ### Info.plist Requirements The test app's `Info.plist` **MUST** include: ```xml BGTaskSchedulerPermittedIdentifiers com.timesafari.dailynotification.fetch com.timesafari.dailynotification.notify UIBackgroundModes background-fetch background-processing remote-notification NSUserNotificationsUsageDescription This app uses notifications to deliver daily updates and reminders. ``` ### Background App Refresh - Background App Refresh must be enabled in Settings - Test app should check and report Background App Refresh status - User should be guided to enable Background App Refresh if disabled --- ## Build Options ### Xcode GUI Build 1. **Open Workspace:** ```bash cd test-apps/ios-test-app open App.xcworkspace # or App.xcodeproj ``` 2. **Select Target:** - Choose iOS Simulator (iPhone 15, iPhone 15 Pro, etc.) - Or physical device (requires signing) 3. **Build and Run:** - Press Cmd+R - Or Product → Run ### Command-Line Build Use the build script: ```bash # From repo root ./scripts/build-ios-test-app.sh --simulator # Or for device ./scripts/build-ios-test-app.sh --device ``` **Phase 2 Enhancement:** Refactor into modular subcommands: ```bash ./scripts/build-ios-test-app.sh setup # pod install + sync ./scripts/build-ios-test-app.sh run-sim # build + run simulator ./scripts/build-ios-test-app.sh device # build + deploy device ``` **Copy-Paste Commands:** ```bash # Setup (first time or after dependency changes) cd test-apps/ios-test-app pod install npx cap sync ios # Build for simulator xcodebuild -workspace App.xcworkspace \ -scheme ios-test-app \ -configuration Debug \ -sdk iphonesimulator \ -destination 'platform=iOS Simulator,name=iPhone 15' \ build # Run on simulator xcodebuild -workspace App.xcworkspace \ -scheme ios-test-app \ -configuration Debug \ -sdk iphonesimulator \ -destination 'platform=iOS Simulator,name=iPhone 15' \ test ``` ### Future CI Integration (Optional) **Note for Phase 2+:** Consider adding `xcodebuild`-based CI job that: - Builds `test-apps/ios-test-app` for simulator - Runs a minimal UI test that: - Launches app - Calls `configure()` and `getNotificationStatus()` - Validates log sequence + successful fetch simulation via LLDB trigger - This ensures test app remains buildable as plugin evolves **Phase 1:** Manual testing only; CI integration is out of scope. **CI Readiness (Phase 2):** - Add `xcodebuild` target for "Prefetch Integration Test" - Validate log sequence + successful fetch simulation via LLDB trigger - Use log validation script (`validate-ios-logs.sh`) for automated sequence checking ### Build Requirements **Required Tools:** - **Xcode:** 15.0 or later - **macOS:** 13.0 (Ventura) or later - **iOS Deployment Target:** iOS 15.0 or later - **CocoaPods:** >= 1.13 (must run `pod install` before first build) - **Node.js:** 20.x (recommended) - **npm:** Latest stable (comes with Node.js) - **Xcode Command Line Tools:** Must run `xcode-select --install` if not already installed **Note:** Mismatched versions are **out of scope** for Phase 1 support. Use recommended versions to avoid compatibility issues. --- ## Capacitor Configuration ### Plugin Registration The test app **MUST** register the DailyNotification plugin: **`capacitor.config.json` or `capacitor.config.ts`:** ```json { "plugins": { "DailyNotification": { "enabled": true } } } ``` ### Plugin Path The plugin must be accessible from the test app: - **Development:** Plugin source at `../../ios/Plugin/` - **Production:** Plugin installed via npm/CocoaPods ### Sync Command After making changes to plugin or web assets: ```bash cd test-apps/ios-test-app npx cap sync ios ``` --- ## Debugging Strategy **When executing the scenarios in `IOS_PREFETCH_TESTING.md`, use the following commands while looking for logs with prefixes `DNP-PLUGIN`, `DNP-FETCH`, `DNP-SCHEDULER`.** ### Xcode Debugger **Check Pending Notifications:** ```swift po UNUserNotificationCenter.current().pendingNotificationRequests() ``` **Check Permission Status:** ```swift po await UNUserNotificationCenter.current().notificationSettings() ``` **Manually Trigger BGTask (Simulator Only):** ```swift e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"com.timesafari.dailynotification.fetch"] ``` **Copy-Paste Commands:** ```swift // In Xcode LLDB console: // Check pending notifications po UNUserNotificationCenter.current().pendingNotificationRequests() // Check permission status po await UNUserNotificationCenter.current().notificationSettings() // Manually trigger BGTask (simulator only) e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"com.timesafari.dailynotification.fetch"] // Force reschedule all tasks (test harness) po DailyNotificationBackgroundTaskTestHarness.forceRescheduleAll() // Simulate time warp (test harness) po DailyNotificationBackgroundTaskTestHarness.simulateTimeWarp(minutesForward: 60) ``` ### Console.app Logging 1. Open Console.app (Applications → Utilities) 2. Select device/simulator 3. Filter by: `DNP-` or `DailyNotification` **Key Log Prefixes:** - `DNP-PLUGIN:` - Main plugin operations - `DNP-FETCH:` - Background fetch operations - `DNP-SCHEDULER:` - Scheduling operations - `DNP-STORAGE:` - Storage operations **Structured Logging (Swift Logger):** The plugin uses Swift `Logger` categories for structured logging: - `com.timesafari.dailynotification.plugin` - Plugin operations - `com.timesafari.dailynotification.fetch` - Fetch operations - `com.timesafari.dailynotification.scheduler` - Scheduling operations - `com.timesafari.dailynotification.storage` - Storage operations Filter in Console.app by subsystem: `com.timesafari.dailynotification` **Phase 2: Log Validation Script** Add helper script (`validate-ios-logs.sh`) to grep for required sequence markers: ```bash grep -E "\[DNP-(FETCH|SCHEDULER|PLUGIN)\]" device.log | ./scripts/validate-ios-logs.sh ``` This confirms that all critical log steps occurred in proper order and flags missing or out-of-order events automatically. ### Common Debugging Scenarios **Scenario: BGTask not running when expected → follow this checklist:** 1. **BGTask Not Running:** - Check Info.plist has `BGTaskSchedulerPermittedIdentifiers` - Verify identifiers match code exactly (case-sensitive) - Verify task registered in AppDelegate before app finishes launching - Check Background App Refresh enabled in Settings → [Your App] - Verify app not force-quit (iOS won't run BGTask for force-quit apps) - Use simulator-only LLDB command to manually trigger - **See also:** `IOS_PREFETCH_TESTING.md – Log Checklist: Section 3` 2. **Notifications Not Delivering:** - Check notification permissions: `UNUserNotificationCenter.current().getNotificationSettings()` - Verify notification scheduled: `UNUserNotificationCenter.current().getPendingNotificationRequests()` - Check notification category registered - **See also:** `IOS_PREFETCH_TESTING.md – Log Checklist: Section 4` 3. **BGTaskScheduler Fails on Simulator:** - **Expected Behavior:** BGTaskSchedulerErrorDomain Code=1 (notPermitted) is **normal on simulator** - BGTaskScheduler doesn't work reliably on simulator - this is an iOS limitation, not a plugin bug - Notification scheduling still works; prefetch won't run on simulator but will work on real devices - Error handling logs clear message: "Background fetch scheduling failed (expected on simulator)" - **See also:** `IOS_PREFETCH_TESTING.md – Known OS Limitations` for details - **Testing:** Use Xcode → Debug → Simulate Background Fetch for simulator testing 4. **Plugin Not Discovered:** - Check plugin class conforms to `CAPBridgedPlugin` protocol (required for Capacitor discovery) - Verify `@objc extension DailyNotificationPlugin: CAPBridgedPlugin` exists in plugin code - Check plugin framework is force-loaded in AppDelegate before Capacitor initializes - Verify `pluginMethods` array includes all `@objc` methods - **See also:** `doc/directives/0003-iOS-Android-Parity-Directive.md – Plugin Discovery Issue` for detailed troubleshooting 5. **Build Failures:** - Run `pod install` - Clean build folder (Cmd+Shift+K) - Verify Capacitor plugin path --- ## Test App Implementation Checklist ### Setup - [ ] Create `test-apps/ios-test-app/` directory - [ ] Initialize Capacitor iOS project - [ ] Copy HTML/JS UI from Android test app - [ ] Configure Info.plist with BGTask identifiers - [ ] Configure Info.plist with background modes - [ ] Add notification permission description ### Plugin Integration - [ ] Register DailyNotification plugin in Capacitor config - [ ] Ensure plugin path is correct - [ ] Run `npx cap sync ios` - [ ] Verify plugin loads in test app ### UI Implementation - [ ] Copy HTML/JS from Android test app - [ ] Test plugin status display - [ ] Test permission status display - [ ] Test notification scheduling UI - [ ] Test status display ### Build & Test - [ ] Build script works (`./scripts/build-ios-test-app.sh`) - [ ] App builds in Xcode - [ ] App runs on simulator - [ ] Plugin methods work from UI - [ ] Notifications deliver correctly - [ ] BGTask executes (with manual trigger in simulator) --- ## File Structure ``` test-apps/ios-test-app/ ├── App.xcworkspace # Xcode workspace (if using CocoaPods) ├── App.xcodeproj # Xcode project ├── App/ # Main app directory │ ├── App/ │ │ ├── AppDelegate.swift │ │ ├── SceneDelegate.swift │ │ ├── Info.plist # Must include BGTask identifiers │ │ └── Assets.xcassets │ └── Public/ # Web assets (HTML/JS) │ └── index.html # Same as Android test app ├── Podfile # CocoaPods dependencies ├── capacitor.config.json # Capacitor configuration └── package.json # npm dependencies (if any) ``` --- ## Testing Scenarios ### Basic Functionality **Happy Path Example:** When everything is working correctly, your first run should produce logs like this: ```text [DNP-PLUGIN] configure() called [DNP-PLUGIN] status = ready [DNP-PLUGIN] requestPermissions() → granted [DNP-SCHEDULER] scheduleDailyNotification() → notificationTime=2025-11-15T05:53:00Z [DNP-FETCH] Scheduling prefetch for notification at 2025-11-15T05:53:00Z [DNP-FETCH] BGAppRefreshTask scheduled (earliestBeginDate=2025-11-15T05:48:00Z) [DNP-SCHEDULER] Scheduling notification for 2025-11-15T05:53:00Z [DNP-STORAGE] Persisted schedule to DB (id=..., type=DAILY, ...) ``` **If your first run doesn't look roughly like this, fix that before proceeding to BGTask tests.** 1. **Plugin Registration** `[P1-Core][SIM+DEV]` - Launch app - Verify plugin status shows "Plugin is loaded and ready!" - **See also:** `IOS_PREFETCH_TESTING.md – Simulator Test Plan: Step 2` 2. **Permission Management** `[P1-Core][SIM+DEV]` - Check permissions - Request permissions - Verify permissions granted - **See also:** `IOS_PREFETCH_TESTING.md – Simulator Test Plan: Step 6 (Negative-Path Tests)` 3. **Notification Scheduling** `[P1-Prefetch][SIM+DEV]` - Schedule test notification - Verify notification scheduled - Wait for notification to appear - **See also:** `IOS_PREFETCH_TESTING.md – Simulator Test Plan: Steps 3-5` ### Background Tasks **Sim vs Device Behavior:** - `[SIM+DEV]` – can be fully tested on simulator and device (logic correctness) - `[DEV-ONLY]` – timing / heuristic behavior, must be verified on real hardware 1. **BGTask Scheduling** `[P1-Prefetch][SIM+DEV]` - Schedule notification with prefetch - Verify BGTask scheduled 5 minutes before notification - Manually trigger BGTask (simulator only) - Verify content fetched - **Note:** Logic and logs on sim; real timing on device - **See also:** `IOS_PREFETCH_TESTING.md – Simulator Test Plan: Steps 3-4` 2. **BGTask Miss Detection** `[P1-Prefetch][DEV-ONLY]` - Schedule notification - Wait 15+ minutes - Launch app - Verify BGTask rescheduled - **Note:** Only meaningful on device (heuristic timing) - **See also:** `IOS_PREFETCH_TESTING.md – Real Device Test Plan: Step 4` ### Error Handling 1. **Permission Denied** `[P1-Core][SIM+DEV]` - Deny notification permissions - Try to schedule notification - Verify error returned - **See also:** `IOS_PREFETCH_TESTING.md – Simulator Test Plan: Step 7 (Negative-Path Tests)` 2. **Invalid Parameters** `[P1-Core][SIM+DEV]` - Try to schedule with invalid time format - Verify error returned 3. **Network Failures** `[P1-Prefetch][SIM+DEV]` - Turn off network during fetch - Verify graceful fallback to on-demand fetch - Test recovery: network off → fail → network on → retry succeeds 4. **Server Errors / Auth Expiry** `[P1-Prefetch][SIM+DEV]` - Simulate HTTP 401 or 500 error - Verify plugin logs failure with reason (AUTH, NETWORK) - Confirm telemetry counter increments: `dnp_prefetch_failure_total{reason="AUTH"}` - Verify fallback to live fetch at notification time 5. **Corrupted Cache** `[P1-Prefetch][SIM+DEV]` - Manually tamper with stored cache (invalid JSON or remove entry) - Verify plugin detects invalid cache and falls back - Ensure no crash on reading bad cache (error handling wraps cache read) 6. **BGTask Execution Failure** `[P1-Prefetch][SIM+DEV]` - Simulate internal failure in BGTask handler - Verify expiration handler or completion still gets called - Ensure task marked complete even on exception 7. **Repeated Scheduling Calls** `[P1-Prefetch][SIM+DEV]` - Call `scheduleDailyNotification()` multiple times rapidly - Verify no duplicate scheduling (one active task rule enforced) - Confirm only one BGTask is actually submitted ### Advanced Features `[P2-Advanced]` 1. **Rolling Window** `[P2-Advanced]` - Schedule multiple notifications - Verify rolling window maintenance - Check notification limits (64 max on iOS) 2. **TTL Enforcement** `[P2-Advanced]` - Schedule notification with prefetch - Wait for TTL to expire - Verify stale content discarded at delivery --- ## Developer/Test Harness Features (Optional but Recommended) Since this test app is for internal testing, it can expose more tools: ### Dev-Only Toggles | Button | Action | Purpose | |--------|--------|---------| | "Simulate BGTask Now" | Calls LLDB trigger | Quick sanity test | | "Schedule 1-min Notification" | Auto schedules T-Lead=1 | Edge case testing | | "Simulate DST Shift" | Adds +1 hr offset | DST handling check | | "Show Cached Payload" | Displays JSON cache | Prefetch validation | | "Force Reschedule All" | Calls `forceRescheduleAll()` | BGTask recovery testing | | "Time Warp +N Minutes" | Calls `simulateTimeWarp()` | Accelerated TTL/T-Lead tests | 1. **Force Schedule Notification N Minutes from Now** - Bypass normal scheduling UI - Directly call `scheduleDailyNotification()` with calculated time - Useful for quick testing scenarios 2. **Force "Prefetch-Only" Task** - Trigger BGTask without scheduling notification - Useful for testing prefetch logic in isolation - Display raw JSON returned from API (last fetched payload) 3. **Display Raw API Response** - Show last fetched payload as JSON - Useful for debugging API responses - Can be referenced from `IOS_PREFETCH_TESTING.md` as shortcuts for specific scenarios 4. **Manual BGTask Trigger (Dev Build Only)** - Button to manually trigger BGTask (simulator only) - Wraps the LLDB command in UI for convenience 5. **UI Feedback Enhancements** - Add persistent toast/log area showing step-by-step plugin state ("Registered", "Scheduled", "BGTask fired", etc.) - Include color-coded state indicators for each stage (🟢 OK, 🟡 Pending, 🔴 Error) These features can then be referenced from `IOS_PREFETCH_TESTING.md` as shortcuts for specific test scenarios. ### Persistent Schedule Snapshot **Phase 2 Enhancement:** Store a simple JSON of the last prefetch state for post-run verification: ```json { "last_schedule": "2025-11-15T05:48:00Z", "last_prefetch": "2025-11-15T05:50:00Z", "last_notification": "2025-11-15T05:53:00Z", "prefetch_success": true, "cached_content_used": true, "contentHash": "abcdef123456", "scheduleHash": "xyz789" } ``` This can be used for post-run verification and telemetry aggregation. Access via test app UI button "Show Schedule Snapshot" or via UserDefaults key `DNP_ScheduleSnapshot`. ### UI Indicators for Tests **Status Display:** - Status label/icon: 🟢 green when last notification was fetched and delivered from cache - 🔴 red if something failed (network error, etc.) - 🟡 yellow if pending/unknown state **Last Operation Summary:** - Display last fetch time - Show whether cached content was used - Display any error message - Show telemetry counter values **Dump Prefetch Status Button:** - Triggers plugin to report all relevant info - Shows pending tasks, cache status, telemetry snapshot - Displays in scrollable text view - Useful on devices where attaching debugger is inconvenient ### In-App Log Viewer (Phase 2) **For QA Use:** - Read app's unified logging (OSLog) for entries with subsystem `com.timesafari.dailynotification` - Present logs on screen or allow export to file - Capture Logger output into text buffer during app session - **Security:** Only enable in test builds, not production **Export Test Results:** - Save test run summary to file (JSON format) - Include timestamps, outcomes, telemetry counters - Access via "Export Test Results" button - Collect from devices via Xcode or CI pipelines ## Risks & Gotchas **Common pitfalls when working with the test app:** 1. **Accidentally editing shared HTML/JS in iOS test app instead of Android canonical source** - Always edit Android test app HTML/JS first, then copy to iOS - Keep iOS test app HTML/JS as a copy, not the source of truth 2. **Forgetting `npx cap sync ios` after plugin or asset changes** - Run `npx cap sync ios` after any plugin code changes - Run `npx cap sync ios` after any web asset changes - Check `capacitor.config.json` is up to date 3. **Running with stale Pods** - Run `pod repo update` periodically - Run `pod install` after dependency changes - Clean build folder (Cmd+Shift+K) if Pods seem stale 4. **Changing BGTask identifiers in one place but not the other** - Info.plist `BGTaskSchedulerPermittedIdentifiers` must match Swift code exactly (case-sensitive) - Check both places when updating identifiers - See `IOS_PREFETCH_TESTING.md` for identifier requirements 5. **Mismatched tooling versions** - Use recommended versions (Node 20.x, CocoaPods >= 1.13, Xcode 15.0+) - Mismatched versions are out of scope for Phase 1 support ## References - **Directive:** `doc/directives/0003-iOS-Android-Parity-Directive.md` - **Testing Guide:** `doc/test-app-ios/IOS_PREFETCH_TESTING.md` - Comprehensive prefetch testing procedures - **Android Test App:** `test-apps/android-test-app/` - **Build Script:** `scripts/build-ios-test-app.sh` --- ## Review & Sign-Off Checklist (Phase 1) **Use this checklist to verify Phase 1 iOS test app is complete:** - [ ] Test app builds on simulator with recommended toolchain (Node 20.x, CocoaPods >= 1.13, Xcode 15.0+) - [ ] Test app builds on at least one real device - [ ] All UI buttons map to plugin methods as per the **UI Elements to Plugin Methods Mapping** table - [ ] Happy-path log sequence matches the example in **Testing Scenarios → Basic Functionality** - [ ] BGTask identifiers are consistent (Info.plist ↔ Swift ↔ docs) - [ ] Risks & Gotchas section has been read and acknowledged by the implementer - [ ] Security & Privacy Constraints have been followed (non-production endpoints, no PII in logs) --- ## Technical Correctness Requirements ### BGTask Scheduling & Lifecycle **Validation Requirements:** - Verify `earliestBeginDate` is at least 60 seconds in future (iOS requirement) - Log and handle scheduling errors gracefully (Code=1 on simulator is expected) - Cancel existing pending task before scheduling new (one active task rule) - Use `BGTaskScheduler.shared.getPendingTaskRequests()` in debug to verify only one task pending **Schedule Next Task at Execution:** - Adopt Apple's best practice: schedule next task IMMEDIATELY at start of BGTask handler - This ensures continuity even if app is terminated shortly after - Pattern: Schedule next → Initiate async work → Mark complete → Use expiration handler **Expiration Handler and Completion:** - Implement expiration handler to cancel ongoing operations if iOS terminates task (~30 seconds) - Always call `task.setTaskCompleted(success:)` exactly once - Use `success: false` if fetch didn't complete (system may reschedule sooner) - Use `success: true` if all went well - Re-schedule next task after marking completion (for recurring use cases) **Error Handling & Retry:** - Distinguish recoverable errors (transient network) vs permanent failures - For network failures: log failure reason, set `success: false`, consider cached data if available - For logic errors: log clear message, call `setTaskCompleted(success: false)`, exit cleanly - Ensure fallback to on-demand fetch at notification time if prefetch fails **Data Consistency & Cleanup:** - Cross-check `notificationTime` matches payload's `scheduled_for` field - Validate TTL on cached content at notification time (discard if expired) - Ensure content is saved to persistent store before marking BGTask complete - Implement cache cleanup for outdated data - Handle permission changes gracefully (detect failure at delivery, log outcome) ### Scheduling and Notification Coordination **Unified Scheduling Logic:** - Atomically schedule both UNNotificationRequest and BGAppRefreshTaskRequest - If one fails, cancel the other or report partial failure - Return clear status/error code to JS layer **BGTask Identifier Constants:** - Verify identifier in code exactly matches Info.plist (case-sensitive) - Test harness should verify on app launch (logs show successful registration) **Concurrency Considerations:** - Handle potentially overlapping schedules (Phase 2: multiple notifications) - Use one BGTask to fetch for next upcoming notification only - Store next notification's schedule ID and time in shared place - Use locks or dispatch synchronization to avoid race conditions **OS Limits:** - Acknowledge force-quit prevents BGTask execution (can't circumvent) - Tolerate running slightly later than `earliestBeginDate` (iOS heuristics) - Log actual execution time vs scheduled time for analysis --- ## Phase 2 Forward Plan **Planned enhancements for Phase 2:** - Add quick scenario buttons (Simulate BGTask Now, Schedule 1-min Notification, Simulate DST Shift, Show Cached Payload) - Implement persistent schedule snapshot (JSON of last prefetch state) - Add color-coded UI feedback (🟢 OK, 🟡 Pending, 🔴 Error) - Refactor build script into modular subcommands (`setup`, `run-sim`, `device`) - Integrate CI pipeline with `xcodebuild` target for "Prefetch Integration Test" - Add log validation script (`validate-ios-logs.sh`) for automated sequence checking - Implement rolling window & TTL validation - Add telemetry verification for multi-day scenarios - Test on different device models and iOS versions - Add in-app log viewer/export for QA use **See also:** `doc/directives/0003-iOS-Android-Parity-Directive.md` for Phase 2 implementation details. --- ## Changelog (high-level) - 2025-11-15 — Initial Phase 1 version (prefetch MVP, Android parity) --- **Status:** 📋 **REQUIRED FOR PHASE 1** **Last Updated:** 2025-11-15