diff --git a/doc/directives/0003-iOS-Android-Parity-Directive.md b/doc/directives/0003-iOS-Android-Parity-Directive.md index 097f527..8f48483 100644 --- a/doc/directives/0003-iOS-Android-Parity-Directive.md +++ b/doc/directives/0003-iOS-Android-Parity-Directive.md @@ -792,6 +792,46 @@ A "successful run" is defined as: BGTask handler invoked, content fetch complete ## Validation & QA Plan +### Testing Strategy + +**Comprehensive testing guide:** See `doc/test-app-ios/IOS_PREFETCH_TESTING.md` for detailed testing procedures, log checklists, and behavior classification. + +**Key Testing Principles:** +- **Simulator:** Test logic correctness (deterministic behavior) +- **Real Device:** Test timing and reliability (heuristic behavior) +- **Log-Driven:** Use structured logging to trace full execution paths +- **Failure Modes:** Test negative paths (network failures, permission denials) + +**Behavior Classification:** +- **🟒 Deterministic:** Code logic, error handling, data processing (test in simulator) +- **🟑 Partially Deterministic:** Scheduling logic works, but timing is heuristic (test flow in simulator, timing on device) +- **πŸ”΄ Heuristic:** Background execution timing, notification delivery timing (test on real device only) + +### Method Behavior Classification + +**🟒 Deterministic Methods (Test in Simulator):** +- `configure()` - Configuration logic +- `getLastNotification()` - Data retrieval +- `cancelAllNotifications()` - Cancellation logic +- `getNotificationStatus()` - Status calculation +- `updateSettings()` - Settings update logic +- `getBatteryStatus()` - Battery state reading +- `getPowerState()` - Power state reading +- `getRollingWindowStats()` - Statistics calculation +- `testJWTGeneration()` - JWT generation logic +- `testEndorserAPI()` - API call logic + +**🟑 Partially Deterministic Methods (Test Flow in Simulator, Timing on Device):** +- `scheduleDailyNotification()` - Scheduling logic is deterministic, but iOS delivery timing is heuristic +- `maintainRollingWindow()` - Logic is deterministic, but when iOS allows execution is heuristic + +**πŸ”΄ Heuristic Methods (Test on Real Device Only):** +- Background task execution timing (BGTaskScheduler) - iOS controls when tasks run +- Notification delivery timing (UNUserNotificationCenter) - iOS controls delivery timing +- Reboot recovery detection (uptime comparison) - May vary based on system state + +**See `doc/test-app-ios/IOS_PREFETCH_TESTING.md` for detailed testing procedures for each behavior class.** + ### Unit Tests - [ ] Test all new methods match Android behavior @@ -811,12 +851,13 @@ A "successful run" is defined as: BGTask handler invoked, content fetch complete ### Manual Testing -- [ ] Test on iOS Simulator (primary) -- [ ] Test on physical iOS devices (validation) +- [ ] Test on iOS Simulator (primary) - Logic correctness +- [ ] Test on physical iOS devices (validation) - Timing and reliability - [ ] Test Background App Refresh settings - [ ] Test notification permissions - [ ] Test battery optimization scenarios - [ ] Test app lifecycle events +- [ ] Test prefetch execution (see `doc/test-app-ios/IOS_PREFETCH_TESTING.md`) --- @@ -1049,6 +1090,7 @@ scripts/ - `ios/Plugin/DailyNotificationDatabase.swift` - Database (CoreData) - `ios/Plugin/DailyNotificationRollingWindow.swift` - Rolling window - `ios/Plugin/DailyNotificationTTLEnforcer.swift` - TTL enforcement +- `ios/Plugin/DailyNotificationBackgroundTaskTestHarness.swift` - BGTaskScheduler test harness (reference implementation) - `ios/Plugin/DailyNotificationETagManager.swift` - ETag management - `ios/Plugin/DailyNotificationBackgroundTaskManager.swift` - Background tasks @@ -1061,6 +1103,7 @@ scripts/ - `doc/implementation-roadmap.md` - Implementation roadmap - `doc/INTEGRATION_CHECKLIST.md` - Integration checklist - `doc/test-app-ios/IOS_TEST_APP_REQUIREMENTS.md` - iOS test app detailed requirements (must be created in Phase 1) +- `doc/test-app-ios/IOS_PREFETCH_TESTING.md` - Comprehensive iOS prefetch testing guide (testing procedures, log checklists, behavior classification) - `README.md` - Project documentation --- @@ -1447,6 +1490,7 @@ scripts/ |---------|-----------------|----------------|-------------|-------| | Daily notification | Exact Β±0s | Β±180s toleranceΒΉ | Test case: verify **observed** delivery within 3 min window | 1 | | Daily prefetch | Worker at exact time | BGTask within 15 min window | Logs + UI: check fetch timestamp | 1 | +| Prefetch execution | Deterministic logic | Heuristic timing (device-only) | Simulator: logic correctness; Device: timing verification (see `IOS_PREFETCH_TESTING.md`) | 1 | | Rolling window | 2 days ahead | iOS 64 limit enforced | Stats call: verify pending count ≀ 64 | 2 | | TTL enforcement | Discard stale at fire | Same: discard stale at fire | Deliver-time check: verify TTL validation | 2 | | Reboot recovery | BOOT_COMPLETED | Uptime comparison | App launch: verify reschedule after reboot | 2 | @@ -1513,12 +1557,20 @@ scripts/ - Verify task registered in AppDelegate before app finishes launching - **Simulator-only debugging trick:** Use LLDB command to manually trigger: `e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"com.timesafari.dailynotification.fetch"]` - Note: This is for simulator testing only, not available in production + - **Testing Guide:** See `doc/test-app-ios/IOS_PREFETCH_TESTING.md` for comprehensive testing procedures 2. **Notifications Not Delivering:** - Check notification permissions: `UNUserNotificationCenter.current().getNotificationSettings()` - Verify notification scheduled: `UNUserNotificationCenter.current().getPendingNotificationRequests()` - Check notification category registered +3. **Prefetch Not Executing:** + - Verify BGTask scheduled: Check logs for `[DNP-FETCH] BGAppRefreshTask scheduled` + - Check Background App Refresh enabled in Settings + - In Simulator: Use Xcode β†’ Debug β†’ Simulate Background Fetch + - On Device: Check logs via Xcode Devices console + - **Testing Guide:** See `doc/test-app-ios/IOS_PREFETCH_TESTING.md` for detailed troubleshooting + 3. **Build Failures:** - Ensure CocoaPods installed: `pod install` - Check Capacitor plugin path in `capacitor.plugins.json` diff --git a/doc/test-app-ios/IOS_PREFETCH_TESTING.md b/doc/test-app-ios/IOS_PREFETCH_TESTING.md new file mode 100644 index 0000000..3c43372 --- /dev/null +++ b/doc/test-app-ios/IOS_PREFETCH_TESTING.md @@ -0,0 +1,644 @@ +# 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. + +**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 + +**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 + +**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 | `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) + diff --git a/doc/test-app-ios/IOS_TEST_APP_REQUIREMENTS.md b/doc/test-app-ios/IOS_TEST_APP_REQUIREMENTS.md index 30db40a..f969b0d 100644 --- a/doc/test-app-ios/IOS_TEST_APP_REQUIREMENTS.md +++ b/doc/test-app-ios/IOS_TEST_APP_REQUIREMENTS.md @@ -1,16 +1,58 @@ # iOS Test App Requirements +**Purpose:** What the iOS test app must provide so that the testing guide can be executed with parity vs Android + +**Plugin Target:** DailyNotificationPlugin v3.x (iOS) +**Phase:** Phase 1 – Prefetch MVP **Status:** πŸ“‹ **REQUIRED FOR PHASE 1** -**Date:** 2025-01-XX +**Date:** 2025-11-15 **Author:** Matthew Raymer **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 glossary in `IOS_PREFETCH_TESTING.md` for terminology. + --- ## 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 @@ -54,6 +96,20 @@ The test app UI must support: - 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" | `getNotificationStatus()` | Maps to permission state | +| "Request Permissions" | `requestPermissions()` | Drives iOS UNUserNotificationCenter | +| "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) | + +**See `IOS_PREFETCH_TESTING.md` Behavior Classification for deterministic vs heuristic methods.** + --- ## iOS Permissions Configuration @@ -64,10 +120,11 @@ The test app's `Info.plist` **MUST** include: ```xml + BGTaskSchedulerPermittedIdentifiers - com.timesafari.dailynotification.fetch - com.timesafari.dailynotification.notify + com.timesafari.dailynotification.fetch + com.timesafari.dailynotification.notify @@ -121,12 +178,29 @@ Use the build script: ./scripts/build-ios-test-app.sh --device ``` +### 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()` +- This ensures test app remains buildable as plugin evolves + +**Phase 1:** Manual testing only; CI integration is out of scope. + ### Build Requirements +**Required Tools:** - **Xcode:** 15.0 or later - **macOS:** 13.0 (Ventura) or later - **iOS Deployment Target:** iOS 15.0 or later -- **CocoaPods:** Must run `pod install` before first build +- **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. --- @@ -167,6 +241,8 @@ 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:** @@ -193,21 +269,27 @@ e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWith **Key Log Prefixes:** - `DNP-PLUGIN:` - Main plugin operations - `DNP-FETCH:` - Background fetch operations -- `DNP-FETCH-SCHEDULE:` - BGTask scheduling -- `DailyNotificationStorage:` - Storage operations -- `DailyNotificationScheduler:` - Scheduling operations +- `DNP-SCHEDULER:` - Scheduling operations +- `DNP-STORAGE:` - Storage operations ### Common Debugging Scenarios +**Scenario: BGTask not running when expected β†’ follow this checklist:** + 1. **BGTask Not Running:** - Check Info.plist has `BGTaskSchedulerPermittedIdentifiers` - - Verify task registered in AppDelegate + - 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 - - Verify notification scheduled + - 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. **Build Failures:** - Run `pod install` @@ -278,56 +360,172 @@ test-apps/ios-test-app/ ### Basic Functionality -1. **Plugin Registration** +**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** +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** +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 -1. **BGTask Scheduling** +**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** +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** +1. **Permission Denied** `[P1-Core]` - Deny notification permissions - Try to schedule notification - Verify error returned + - **See also:** `IOS_PREFETCH_TESTING.md – Simulator Test Plan: Step 6 (Negative-Path Tests)` -2. **Invalid Parameters** +2. **Invalid Parameters** `[P1-Core]` - Try to schedule with invalid time format - Verify error returned +### 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 + +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 + +These features can then be referenced from `IOS_PREFETCH_TESTING.md` as shortcuts for specific test scenarios. + +## 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` -- **Testing Guide:** `doc/IOS_PHASE1_TESTING_GUIDE.md` + +--- + +## 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) + +--- + +## Changelog (high-level) + +- 2025-11-15 β€” Initial Phase 1 version (prefetch MVP, Android parity) --- **Status:** πŸ“‹ **REQUIRED FOR PHASE 1** -**Last Updated:** 2025-01-XX +**Last Updated:** 2025-11-15 diff --git a/ios/Plugin/DailyNotificationBackgroundTaskTestHarness.swift b/ios/Plugin/DailyNotificationBackgroundTaskTestHarness.swift new file mode 100644 index 0000000..a349e4c --- /dev/null +++ b/ios/Plugin/DailyNotificationBackgroundTaskTestHarness.swift @@ -0,0 +1,218 @@ +// +// DailyNotificationBackgroundTaskTestHarness.swift +// DailyNotificationPlugin +// +// Test harness for BGTaskScheduler prefetch functionality +// Reference implementation demonstrating task registration, scheduling, and handling +// +// See: doc/test-app-ios/IOS_PREFETCH_TESTING.md for testing procedures +// + +import Foundation +import BackgroundTasks +import UIKit + +/// Minimal BGTaskScheduler test harness for DailyNotificationPlugin prefetch testing +/// +/// This is a reference implementation demonstrating: +/// - Task registration +/// - Task scheduling +/// - Task handler implementation +/// - Expiration handling +/// - Completion reporting +/// +/// **Usage:** +/// - Reference this when implementing actual prefetch logic in `DailyNotificationBackgroundTaskManager.swift` +/// - Use in test app for debugging BGTaskScheduler behavior +/// - See `doc/test-app-ios/IOS_PREFETCH_TESTING.md` for comprehensive testing guide +/// +/// **Info.plist Requirements:** +/// ```xml +/// BGTaskSchedulerPermittedIdentifiers +/// +/// com.timesafari.dailynotification.fetch +/// +/// ``` +/// +/// **Background Modes (Xcode Capabilities):** +/// - Background fetch +/// - Background processing (if using BGProcessingTask) +class DailyNotificationBackgroundTaskTestHarness { + + // MARK: - Constants + + static let prefetchTaskIdentifier = "com.timesafari.dailynotification.fetch" + + // MARK: - Registration + + /// Register BGTaskScheduler task handler + /// + /// Call this in AppDelegate.application(_:didFinishLaunchingWithOptions:) + /// before app finishes launching. + static func registerBackgroundTasks() { + BGTaskScheduler.shared.register( + forTaskWithIdentifier: prefetchTaskIdentifier, + using: nil + ) { task in + // This closure is called when the task is launched by the system + handlePrefetchTask(task: task as! BGAppRefreshTask) + } + + print("[DNP-FETCH] Registered BGTaskScheduler with id=\(prefetchTaskIdentifier)") + } + + // MARK: - Scheduling + + /// Schedule a BGAppRefreshTask for prefetch + /// + /// - Parameter earliestOffsetSeconds: Seconds from now when task can begin + /// - Returns: true if scheduling succeeded, false otherwise + @discardableResult + static func schedulePrefetchTask(earliestOffsetSeconds: TimeInterval) -> Bool { + let request = BGAppRefreshTaskRequest(identifier: prefetchTaskIdentifier) + request.earliestBeginDate = Date(timeIntervalSinceNow: earliestOffsetSeconds) + + do { + try BGTaskScheduler.shared.submit(request) + print("[DNP-FETCH] BGAppRefreshTask scheduled (earliestBeginDate=\(String(describing: request.earliestBeginDate)))") + return true + } catch { + print("[DNP-FETCH] Failed to schedule BGAppRefreshTask: \(error)") + return false + } + } + + // MARK: - Handler + + /// Handle BGAppRefreshTask execution + /// + /// This is called by the system when the background task is launched. + /// Replace PrefetchOperation with your actual prefetch logic. + private static func handlePrefetchTask(task: BGAppRefreshTask) { + print("[DNP-FETCH] BGTask handler invoked (task.identifier=\(task.identifier))") + + // Schedule the next one early, so that there's always a pending task + // In real implementation, calculate next schedule based on notification time + schedulePrefetchTask(earliestOffsetSeconds: 60 * 30) // 30 minutes later, for example + + // Define the work + let queue = OperationQueue() + queue.maxConcurrentOperationCount = 1 + + let operation = PrefetchOperation() + + // Set expiration handler + // Called if iOS decides to end the task early (typically ~30 seconds) + task.expirationHandler = { + print("[DNP-FETCH] Task expired") + operation.cancel() + } + + // Set completion handler + operation.completionBlock = { + let success = !operation.isCancelled + print("[DNP-FETCH] Task completionBlock (success=\(success))") + task.setTaskCompleted(success: success) + } + + queue.addOperation(operation) + } +} + +// MARK: - Prefetch Operation + +/// Simple Operation example for testing +/// +/// Replace this with your actual prefetch logic: +/// - HTTP fetch from TimeSafari API +/// - JWT signing +/// - ETag validation +/// - Content caching +/// - Error handling +class PrefetchOperation: Operation { + + override func main() { + if isCancelled { return } + + print("[DNP-FETCH] PrefetchOperation: starting fake fetch...") + + // Simulate some work + // In real implementation, this would be: + // - Make HTTP request + // - Parse response + // - Cache content + // - Update database + Thread.sleep(forTimeInterval: 2) + + if isCancelled { return } + + print("[DNP-FETCH] PrefetchOperation: finished fake fetch.") + } +} + +// MARK: - AppDelegate Integration Example + +/* + Example integration in AppDelegate.swift: + + import UIKit + import BackgroundTasks + + @main + class AppDelegate: UIResponder, UIApplicationDelegate { + + func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + + // Register background tasks BEFORE app finishes launching + DailyNotificationBackgroundTaskTestHarness.registerBackgroundTasks() + + // Schedule initial task (for testing) + DailyNotificationBackgroundTaskTestHarness.schedulePrefetchTask(earliestOffsetSeconds: 5 * 60) // 5 minutes + + return true + } + } + */ + +// MARK: - Testing in Simulator + +/* + To test in simulator: + + 1. Run app in Xcode + 2. Background the app (Home button / Cmd+Shift+H) + 3. In Xcode menu: + - Debug β†’ Simulate Background Fetch, or + - Debug β†’ Simulate Background Refresh + 4. Check console logs for [DNP-FETCH] messages + + Expected logs: + - [DNP-FETCH] Registered BGTaskScheduler with id=... + - [DNP-FETCH] BGAppRefreshTask scheduled (earliestBeginDate=...) + - [DNP-FETCH] BGTask handler invoked (task.identifier=...) + - [DNP-FETCH] PrefetchOperation: starting fake fetch... + - [DNP-FETCH] PrefetchOperation: finished fake fetch. + - [DNP-FETCH] Task completionBlock (success=true) + */ + +// MARK: - Testing on Real Device + +/* + To test on real device: + + 1. Install app on iPhone + 2. Enable Background App Refresh in Settings β†’ [Your App] + 3. Schedule a notification with prefetch + 4. Lock device and leave idle (plugged in for best results) + 5. Monitor logs via: + - Xcode β†’ Devices & Simulators β†’ device β†’ open console + - Or os_log aggregator / remote logging + + Note: Real device timing is heuristic, not deterministic. + iOS will run the task when it determines it's appropriate, + not necessarily at the exact earliestBeginDate. + */ +