# iOS Prefetch Testing Guide **Purpose:** How to test background prefetch for DailyNotificationPlugin on iOS (simulator + device) **Plugin Target:** DailyNotificationPlugin v3.x (iOS) **Phase:** Phase 1 – Prefetch MVP **Last Updated:** 2025-11-15 **Status:** 🎯 **ACTIVE** - Testing guide for Phase 1+ implementation **Note:** This guide assumes the iOS test app is implemented as described in `doc/test-app-ios/IOS_TEST_APP_REQUIREMENTS.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; no hard guarantee of 5-min prefetch --- ## Non-Goals (Phase 1) **Out of scope for Phase 1 testing:** - ❌ No testing of silent push notifications - ❌ No testing of advanced rolling-window policies beyond "schedule one daily notification" - ❌ No guarantees of exact N-minute timing (iOS heuristics only) - ❌ No testing of multi-day rolling windows (deferred to Phase 2) --- ## Table of Contents 1. [Goals](#goals) 2. [Assumptions](#assumptions) 3. [Simulator Test Plan (Logic-Correctness)](#simulator-test-plan) 4. [Real Device Test Plan (Timing & Reliability)](#real-device-test-plan) 5. [Log Checklist](#log-checklist) 6. [Behavior Classification](#behavior-classification) 7. [Test Harness Reference](#test-harness-reference) --- ## Goals Verify that the **iOS prefetch path actually runs**: - βœ… BGTask is registered - βœ… BGTask is scheduled - βœ… Fetch executes - βœ… Data is persisted & used by the notification **Separate concerns:** - **"Does the logic work?"** β†’ Test in Simulator - **"Does timing behave?"** β†’ Test on Real Device ## Time & T-Lead Rules **Critical:** These rules prevent bugs when tweaking prefetch timing. ### Time Storage - **`notificationTime` is stored in UTC** (not local time) - All internal calculations use UTC timestamps - Display to users may convert to local time, but storage remains UTC ### Prefetch Lead Calculation - **Prefetch lead:** `prefetchTime = notificationTime - leadMinutes` - **Default lead:** 5 minutes (300 seconds) - **Minimum lead:** 1 minute (60 seconds) - iOS requires at least 1 minute in future - **Maximum lead:** No hard limit, but iOS heuristics favor shorter windows ### Edge Cases **Negative/Too-Small Lead Times:** - If `notificationTime - leadMinutes < now + 60 seconds`: - Cap prefetch at `now + 60 seconds` (minimum iOS requirement) - Log: `[DNP-FETCH] Prefetch lead too small, capping at minimum (60s)` - Or disable prefetch if notification is too soon **DST (Daylight Saving Time) Changes:** - UTC storage prevents DST issues - Test case: Schedule notification at 2:00 AM on DST change day - Verify: Prefetch and notification times remain correct (UTC-based) **Timezone Changes:** - Test case: User changes timezone between schedule and fire - Verify: Notification fires at correct UTC time (not local time) - Log: `[DNP-SCHEDULER] Timezone changed, but UTC schedule unchanged` ### Test Cases 1. **DST Change:** - Schedule notification for 2:00 AM on DST transition day - Verify prefetch and notification use UTC, not local time - Check logs show UTC timestamps 2. **Timezone Change:** - Schedule notification - Change device timezone - Verify notification fires at correct UTC time - Check logs confirm UTC-based scheduling 3. **Too-Small Lead:** - Schedule notification 30 seconds in future - Verify prefetch is capped at 60 seconds minimum - Check logs show capping behavior ## If You Only Have 30 Minutes Quick validation checklist (each step links to log verification): 1. Build test app (per `IOS_TEST_APP_REQUIREMENTS.md`) 2. Run on iOS Simulator 3. Schedule notification 5 minutes in future β†’ **See Log Checklist β†’ Notification Scheduled** 4. Background app (Home button / Cmd+Shift+H) 5. Xcode β†’ Debug β†’ Simulate Background Fetch β†’ **See Log Checklist β†’ BGTask Fired** 6. Confirm logs show: Registration β†’ Scheduling β†’ BGTask handler β†’ Fetch success β†’ Task completed 7. Wait for notification time or manually trigger β†’ **See Log Checklist β†’ Notification Delivery** 8. Verify notification uses cached content (check logs for `[DNP-FETCH] Found cached content`) β†’ **See Log Checklist β†’ Notification Delivery** --- ## Assumptions - Using **BGTaskScheduler** (`BGAppRefreshTask`) for prefetch - App has **background modes** enabled: - Background fetch - Background processing (if using `BGProcessingTask`) - Info.plist has: - `BGTaskSchedulerPermittedIdentifiers` array with task identifier: `com.timesafari.dailynotification.fetch` - Plugin exposes: - `scheduleDailyNotification()` method that schedules both prefetch and notification ### Architecture at a Glance **Flow Overview:** ``` JS/HTML Test App β†’ Capacitor Bridge β†’ DailyNotificationPlugin (iOS) ↓ BGTaskScheduler (BGAppRefreshTask) ↓ HTTP API (JWT, ETag, etc.) ↓ Cache/DB (UserDefaults/CoreData) ↓ Notification Delivery (uses cached content if valid, falls back to live fetch) ``` **Key Components:** - **Test App:** Provides UI to trigger plugin methods (see `IOS_TEST_APP_REQUIREMENTS.md`) - **Plugin:** Handles scheduling, fetching, caching, notification delivery - **BGTaskScheduler:** iOS background execution for prefetch - **Storage:** Persists schedules and cached content - **Notification System:** Delivers notifications using prefetched or live content **Verification Points:** - BGTask registered at app startup - BGTask scheduled 5 minutes before notification time - BGTask executes and fetches content - Content persisted to cache/DB - Notification delivery uses cached content --- ## Simulator Test Plan (Logic-Correctness) **Objective:** Confirm that when a background task fires, your prefetch code runs end-to-end. ### 1. Harden Logging (One-Time Prep) Add structured logs at key points: **On app startup:** ``` [DNP-FETCH] Registering BGTaskScheduler task (id=com.timesafari.dailynotification.fetch) ``` **When scheduling:** ``` [DNP-FETCH] Scheduling BGAppRefreshTask (earliestBeginDate=2025-11-14T05:48:00Z) [DNP-FETCH] Prefetch scheduled 5 minutes before notification (notificationTime=2025-11-14T05:53:00Z) ``` **When BGTask handler fires:** ``` [DNP-FETCH] BGTask handler invoked (task.identifier=com.timesafari.dailynotification.fetch) ``` **Inside prefetch logic:** ``` [DNP-FETCH] Starting fetch (notificationTime=2025-11-14T05:53:00Z, scheduleId=...) [DNP-FETCH] Fetch success: status=200, items=1, ttl=86400 [DNP-FETCH] Fetch failed: error=..., willRetry=false ``` **When integrating with notifications:** ``` [DNP-FETCH] Using cached content for notification at 2025-11-14T05:53:00Z [DNP-FETCH] No cached content, falling back to on-demand fetch ``` ### 2. Start App in Simulator 1. Clean build, run on iPhone 15 simulator (or similar) 2. Verify console logs: - βœ… `[DNP-FETCH] Registering BGTaskScheduler task...` at startup ### 3. Schedule a Notification with Prefetch 1. Call plugin's `scheduleDailyNotification()` from JS layer 2. Check logs: - βœ… `[DNP-FETCH] Scheduling BGAppRefreshTask...` with sensible `earliestBeginDate` - βœ… `[DNP-SCHEDULER] Scheduling notification for 2025-11-14T05:53:00Z` - βœ… Any DB/state writes for the schedule **Note:** For a full happy-path log example, see `IOS_TEST_APP_REQUIREMENTS.md – Testing Scenarios β†’ Basic Functionality`. ### 4. Simulate Background Fetch With app running: 1. **Background the app** (Home button / Cmd+Shift+H) 2. In Xcode menu: - **Debug β†’ Simulate Background Fetch** or - **Debug β†’ Simulate Background Refresh** **Expected logs (in order):** ``` [DNP-FETCH] BGTask handler invoked (task.identifier=...) [DNP-FETCH] Starting fetch (notificationTime=..., scheduleId=...) [DNP-FETCH] API call: GET /api/notifications (JWT present, ETag=...) [DNP-FETCH] Fetch success: status=200, bytes=1234, ttl=86400 [DNP-FETCH] Cached content for scheduleId=... [DNP-FETCH] Task completed (success=true) ``` **See Sample Prefetch Response & Mapping below for expected API response structure.** ### 5. Trigger or Wait for Notification **Option A: Wait for scheduled time** - Wait until scheduled time - Look for: - `[DNP-FETCH] Using cached content for notification at ...` - `[DNP-SCHEDULER] Notification delivered (id=..., title=...)` **Option B: Manual trigger** - Use test app UI to trigger notification test path - Confirm notification shows content derived from prefetch, not fallback ### 6. Sample Prefetch Response & Mapping **Example API Response:** When prefetch succeeds, the API returns JSON like: ```json { "id": "notif-2025-11-15", "title": "Your daily update", "body": "Here is today's summary…", "ttl": 86400, "scheduled_for": "2025-11-15T05:53:00Z" } ``` **Response Mapping:** - `id` β†’ Used for notification identification and deduplication - `title` β†’ Notification title (displayed to user) - `body` β†’ Notification body (displayed to user) - `ttl` β†’ Cache validity check at delivery time (seconds) - `scheduled_for` β†’ Cross-check with `notificationTime` to ensure alignment **Log Verification:** When you see `[DNP-FETCH] Fetch success (status=200, bytes=1234, ttl=86400)`, the `ttl=86400` should match the `ttl` field in the JSON response. The `scheduled_for` timestamp should match the `notificationTime` used in scheduling. ### 7. Negative-Path Tests **Network failure:** 1. Turn off network in Simulator (Settings β†’ Network) 2. Trigger background fetch 3. Expect: - `[DNP-FETCH] Fetch failed: error=NetworkError, willRetry=false` - Then either: - Cached content used if available, or - Fallback path logged: `[DNP-FETCH] No cached content, falling back to on-demand fetch` **Permission denied:** 1. Deny notification permissions 2. Attempt to schedule 3. Expect: - `[DNP-PLUGIN] notifications_denied for scheduleDailyNotification` - Error returned: `{ error: "notifications_denied", ... }` --- ## Known OS Limitations **Critical:** These are iOS system limitations, not plugin bugs. If these conditions occur, it is **not a plugin bug**; confirm logs and document behavior. **BGTaskScheduler Simulator Limitation:** - **BGTaskSchedulerErrorDomain Code=1 (notPermitted) is EXPECTED on simulator** - BGTaskScheduler doesn't work reliably on iOS Simulator - this is a known iOS limitation - Background fetch scheduling will fail on simulator with Code=1 error - **This is NOT a plugin bug** - notification scheduling still works correctly - Prefetch won't run on simulator, but will work on real devices with Background App Refresh enabled - Error handling logs: "Background fetch scheduling failed (expected on simulator)" - **Testing:** Use Xcode β†’ Debug β†’ Simulate Background Fetch for simulator testing - **See also:** `doc/directives/0003-iOS-Android-Parity-Directive.md` for implementation details **iOS may NOT run BGTasks if:** - App has been force-quit by the user (iOS won't run BGTask for force-quit apps) - Background App Refresh is disabled in Settings β†’ [Your App] - Device is in Low Power Mode and idle - Device battery is critically low - **On Simulator:** BGTaskScheduler generally doesn't work (Code=1 error is expected) **iOS timing heuristics:** - iOS may delay or batch tasks; prefetch might run **much later** than `earliestBeginDate` (up to 15+ minutes) - iOS adapts BGTask frequency based on user behavior, app usage, and energy constraints - Notification delivery can drift by Β±180 seconds (this is expected, not a bug) **What to do:** - Check logs to confirm plugin logic executed correctly - Document the OS-imposed delay in test results - Verify fallback mechanisms work when prefetch is delayed --- ## Real Device Test Plan (Timing & Reliability) **Objective:** Confirm that **prefetch happens near the intended time** (e.g., 5 minutes before) in realistic conditions. ### 1. Install Dev Build on iPhone - Same BGTask + background modes config as simulator - Confirm logs visible via: - Xcode β†’ Devices & Simulators β†’ device β†’ open console, or - `os_log` aggregator / remote logging ### 2. Baseline Run 1. Open app 2. Schedule a notification for **30–60 minutes in the future** with prefetch 3. Lock the phone and leave it idle, **plugged in** (best-case for iOS) ### 3. Monitor Logs Look for: - BGTask scheduled with appropriate `earliestBeginDate` - Some time before the notification: - BGTask handler invoked - Prefetch fetch executes - At notification fire: - Notification uses prefetched data ### 4. Variations Test different conditions: - **Device on battery** vs plugged in - **App force-quit** vs just backgrounded - **Multiple days in a row**: iOS will adapt its heuristics; see if prefetch becomes more or less reliable - **Low Power Mode**: May delay/disable background tasks - **Background App Refresh disabled**: Should fail gracefully ### 5. Success Criteria In "good" conditions (plugged in, WiFi, not force-quit): - βœ… Prefetch runs at least once before the notification in the majority of tests - βœ… Failure modes are logged clearly: - So you know when **timing failed because of iOS**, not your code - βœ… **For at least one full test cycle, logs and telemetry counts confirm that the sequence: scheduled β†’ executed β†’ success β†’ used is coherent** **Acceptable outcomes:** - Prefetch runs within 15 minutes of `earliestBeginDate` (iOS heuristic window) - If prefetch misses, fallback to on-demand fetch works - All failures are logged with clear reasons ### Test Run Result Template Use this template when recording test runs (especially on real devices): ```text Date: Device model: iOS version: App build / commit: Lead time (T-Lead - see glossary): Scenario ID(s) tested: Conditions: (plugged vs battery, Wi-Fi vs LTE, Low Power Mode on/off, Background App Refresh on/off) Outcome summary: - Prefetch scheduled at: - BGTask executed at: - Notification fired at: - Cached content used: yes/no Failures observed (if any) and key log lines: Notes / follow-ups: ``` --- ## Log Checklist When everything is wired correctly, one full cycle should produce: ### 1. App Launch **Expected logs:** ``` [DNP-FETCH] Registering BGTaskScheduler task (id=com.timesafari.dailynotification.fetch) [DNP-PLUGIN] Startup complete (hasPendingSchedules=true|false) ``` **If you see registration but not startup:** - Check plugin initialization in AppDelegate - Verify Capacitor plugin registration - Check for initialization errors in logs **If you don't see registration:** - Check Info.plist has `BGTaskSchedulerPermittedIdentifiers` - Verify task registered in AppDelegate before app finishes launching - Check for registration errors in logs ### 2. Notification Scheduled (from JS / plugin) **Expected logs:** ``` [DNP-FETCH] Scheduling prefetch for notification at 2025-11-14T05:53:00Z [DNP-FETCH] BGAppRefreshTask scheduled (earliestBeginDate=2025-11-14T05:48:00Z) [DNP-SCHEDULER] Scheduling notification for 2025-11-14T05:53:00Z [DNP-STORAGE] Persisted schedule to DB (id=..., type=DAILY, ...) ``` **Note:** For a full happy-path log example, see `IOS_TEST_APP_REQUIREMENTS.md – Testing Scenarios β†’ Basic Functionality`. **If you see scheduling but not BGTask scheduled:** - Check BGTaskScheduler submission succeeded - Verify `earliestBeginDate` is at least 1 minute in future - Check for BGTaskScheduler errors in logs - **On Simulator:** BGTaskSchedulerErrorDomain Code=1 (notPermitted) is expected - see "Known OS Limitations" **If you see BGTask scheduled but not persisted:** - Check database write operations - Verify storage layer is initialized - Check for storage errors in logs ### 3. BGTask Fired (simulator or device) **Expected logs:** ``` [DNP-FETCH] BGTask handler invoked (id=com.timesafari.dailynotification.fetch) [DNP-FETCH] Resolved next notification needing content (time=..., scheduleId=...) [DNP-FETCH] Starting fetch from (notificationTime=..., jwtPresent=true) [DNP-FETCH] Fetch success (status=200, bytes=1234, ttl=86400) [DNP-FETCH] Cached content for scheduleId=... [DNP-FETCH] Task completed (success=true) ``` **If you see "Registering BGTaskScheduler task" but never see "BGTask handler invoked" on device:** - Check Background App Refresh enabled in Settings β†’ [Your App] - Verify app not force-quit (iOS won't run BGTask for force-quit apps) - Check Info.plist identifiers match code exactly (case-sensitive) - Verify task was actually scheduled (check `earliestBeginDate` in logs) - On Simulator: Use Xcode β†’ Debug β†’ Simulate Background Fetch **If you see handler invoked but not fetch starting:** - Check notification resolution logic - Verify schedule exists in database - Check for resolution errors in logs **If you see fetch starting but not success:** - Check network connectivity - Verify API endpoint is accessible - Check JWT/authentication - Look for HTTP error codes in logs ### 4. Notification Delivery **Expected logs:** ``` [DNP-SCHEDULER] Preparing content for notification at 2025-11-14T05:53:00Z [DNP-FETCH] Found cached content (age=300s, source=prefetch) [DNP-SCHEDULER] Notification built (title=..., body=..., id=...) [DNP-SCHEDULER] Notification scheduled/delivered ``` **If you see "Preparing content" but not "Found cached content":** - Check if prefetch actually ran (see BGTask logs) - Verify cache/DB read operations - Check TTL validation (content may have expired) - Look for fallback to on-demand fetch **If you see "Found cached content" but not "Notification built":** - Check notification building logic - Verify notification permissions granted - Check for notification construction errors ### 5. Failure Cases **Expected logs:** ``` [DNP-FETCH] Fetch failed (error=NetworkError, httpStatus=0, willRetry=false) [DNP-FETCH] No cached content available, falling back to on-demand fetch [DNP-FETCH] Task completed (success=false, reason=NETWORK) ``` **If you see fetch failures repeatedly:** - Check network connectivity - Verify API endpoint is correct - Check authentication (JWT) is valid - Look for specific error reasons in logs (NETWORK, AUTH, SYSTEM) **If you see "No cached content" but prefetch should have run:** - Check if BGTask actually executed (see BGTask logs) - Verify cache/DB write operations succeeded - Check for cache expiration (TTL may have passed) - Verify fallback to on-demand fetch works **If you can walk through a log and trace all steps like this, you're in good shape.** --- ## Behavior Classification Testable matrix of deterministic vs heuristic behavior: | Bucket | Component / Method | Deterministic? | Test on | Notes | |--------|-------------------|----------------|---------|-------| | A | `BGTaskScheduler.shared.register` | Yes | Sim + Dev | Registration must always log & succeed/fail deterministically | | A | `configure()`, `getLastNotification()`, `cancelAllNotifications()`, `getNotificationStatus()`, `updateSettings()` | Yes | Sim + Dev | Logic & I/O-only, no timing dependencies | | A | `checkPermissionStatus()`, `requestNotificationPermissions()` | Yes | Sim + Dev | Permission state reading and requests, deterministic | | A | `isChannelEnabled(channelId?)`, `openChannelSettings(channelId?)` | Yes | Sim + Dev | Channel status and settings (iOS: app-wide, not per-channel) | | A | `getBatteryStatus()`, `getPowerState()`, `getRollingWindowStats()` | Yes | Sim + Dev | State reading, deterministic | | A | `testJWTGeneration()`, `testEndorserAPI()` | Yes | Sim + Dev | API call logic, deterministic | | A | Fetch function logic (HTTP calls, DB writes, JSON parsing) | Yes | Sim + Dev | Code path is deterministic | | A | Error handling, retry logic, fallbacks | Yes | Sim + Dev | Logic is deterministic | | A | Log emission / structured logging | Yes | Sim + Dev | Logging is deterministic | | A | Notification building logic (given data) | Yes | Sim + Dev | Title/body/payload construction is deterministic | | A | TTL validation at delivery time | Yes | Sim + Dev | Validation logic is deterministic | | B | `scheduleDailyNotification()` | Logic yes, timing no | Sim + Dev | Use logs to verify scheduling but not run time | | B | `maintainRollingWindow()` | Logic yes, timing no | Sim + Dev | Logic deterministic, but when iOS allows execution is heuristic | | B | BGTaskScheduler scheduling (`earliestBeginDate`) | Logic yes, timing no | Sim + Dev | `earliestBeginDate` is a hint, not a guarantee | | B | Notification trigger + prefetch relationship | Logic yes, timing no | Sim + Dev | "If cached content exists, use it" is deterministic; whether prefetch ran in time is heuristic | | C | Time between `earliestBeginDate` and task execution | No | Device | Purely heuristic, iOS controls when tasks run | | C | Background task execution timing (BGTaskScheduler) | No | Device | iOS heuristics control execution timing | | C | Notification delivery timing (UNUserNotificationCenter) | No | Device | iOS controls delivery timing (Β±180s tolerance) | | C | Reboot recovery detection (uptime comparison) | No | Device | May vary based on system state | **Bucket Summary:** - **🟒 Bucket A (Deterministic):** Test in Simulator and Device - Logic correctness - **🟑 Bucket B (Partially Deterministic):** Test flow in Simulator, timing on Device - **πŸ”΄ Bucket C (Heuristic):** Test on Real Device only - Timing and reliability --- ## Test Harness Reference **File:** `ios/Plugin/DailyNotificationBackgroundTaskTestHarness.swift` **Purpose:** Use this harness if the plugin code is broken or being refactored; it isolates BGTask behavior from plugin wiring. **What it demonstrates:** - Task registration - Task scheduling - Task handler implementation - Expiration handling - Completion reporting **Usage:** - Reference implementation when building actual prefetch logic - Standalone testing of BGTaskScheduler behavior - Debugging BGTask registration/scheduling issues - See file for detailed usage examples and testing instructions --- ## Integration with Main Directive This testing guide supports: - **Phase 1:** Core prefetch functionality testing - **Phase 2:** Advanced features testing (rolling window, TTL) - **Phase 3:** TimeSafari integration testing (JWT, ETag) - **Validation Matrix:** Cross-platform feature validation ## Telemetry and Monitoring **Expected Counters (even if not yet implemented):** Define counters you expect the runtime to emit: - `dnp_prefetch_scheduled_total` - Total prefetch tasks scheduled - `dnp_prefetch_executed_total` - Total prefetch tasks executed - `dnp_prefetch_success_total` - Total successful prefetch executions - `dnp_prefetch_failure_total{reason="NETWORK|AUTH|SYSTEM"}` - Total failed prefetch executions by reason - `dnp_prefetch_used_for_notification_total` - Total notifications using prefetched content **Telemetry Pipeline:** These counters MUST be emitted via the same pipeline as Android (e.g., structlog β†’ rsyslog β†’ Prometheus/Influx/Loki). If telemetry is not yet wired on iOS, mark tests that rely on counters as **P2** and fall back to log inspection for Phase 1. **Success Criteria:** - For at least one full test cycle, logs and telemetry counts confirm that the sequence: scheduled β†’ executed β†’ success β†’ used is coherent - Counters increment as expected through the prefetch lifecycle - Failure counters provide clear reason categorization --- ## Test Campaign Sign-Off Checklist (Phase 1) **Use this checklist to verify Phase 1 prefetch testing is complete:** - [ ] Simulator: one full happy-path cycle verified (Schedule β†’ BGTask β†’ Fetch β†’ Delivery) - [ ] Device: at least one "best-case" run (plugged in, Wi-Fi, not force-quit) where prefetch runs before notification and cached content is used - [ ] At least one failure path logged (NETWORK or PERMISSIONS) and handled gracefully - [ ] Time & T-Lead edge-case tests run (too-small lead, timezone change) or explicitly deferred - [ ] Telemetry counters either wired and validated, or marked **P2** with log-based fallback documented **See also:** - `doc/directives/0003-iOS-Android-Parity-Directive.md` - Main implementation directive - `doc/test-app-ios/IOS_TEST_APP_REQUIREMENTS.md` - Test app setup requirements --- ## Glossary **BGTaskScheduler** – iOS framework for scheduling background tasks (BGAppRefreshTask / BGProcessingTask). Provides heuristic-based background execution, not exact timing guarantees. **UNUserNotificationCenter** – iOS notification framework for scheduling and delivering user notifications. Handles permission requests and notification delivery. **T-Lead** – The lead time between prefetch and notification fire, e.g., 5 minutes. Prefetch is scheduled at `notificationTime - T-Lead`. **Bucket A/B/C** – Deterministic vs heuristic classification used in Behavior Classification: - **Bucket A (Deterministic):** Test in Simulator and Device - Logic correctness - **Bucket B (Partially Deterministic):** Test flow in Simulator, timing on Device - **Bucket C (Heuristic):** Test on Real Device only - Timing and reliability **UTC** – Coordinated Universal Time. All internal timestamps are stored in UTC to avoid DST and timezone issues. **earliestBeginDate** – The earliest time iOS may execute a BGTask. This is a hint, not a guarantee; iOS may run the task later based on heuristics. --- **Status:** 🎯 **READY FOR USE** **Next Steps:** Use this guide when implementing and testing Phase 1+ prefetch functionality --- ## Changelog (high-level) - 2025-11-15 β€” Initial Phase 1 version (prefetch MVP, Android parity)