docs(ios): add comprehensive iOS implementation documentation
Adds complete iOS documentation suite to support iOS implementation parity with Android features. Includes implementation directives, recovery scenario mappings, database migration guide, troubleshooting guide, and test scripts. New Documentation: - iOS Implementation Directive: Phase-based implementation guide mirroring Android structure with iOS-specific considerations - iOS Recovery Scenario Mapping: Maps Android recovery scenarios to iOS equivalents with detection logic comparisons - iOS Core Data Migration Guide: Complete Room → Core Data entity mappings with implementation checklist for missing entities - iOS Troubleshooting Guide: Common issues, debugging techniques, and error code reference Enhanced Documentation: - API.md: Added iOS-only methods (permissions, background tasks, pending notifications), platform differences table, and iOS-specific error types. Updated version to 2.3.0. Test Infrastructure: - iOS test scripts for Phase 1 (cold start), Phase 2 (termination), and Phase 3 (boot recovery) testing scenarios All documentation addresses gaps identified in iOS Implementation Documentation Review and provides foundation for iOS recovery feature implementation (currently pending). Note: iOS recovery features (ReactivationManager, scenario detection) are NOT yet implemented. Documentation is ready to guide implementation.
This commit is contained in:
172
API.md
172
API.md
@@ -1,8 +1,8 @@
|
||||
# TimeSafari Daily Notification Plugin API Reference
|
||||
|
||||
**Author**: Matthew Raymer
|
||||
**Version**: 2.2.0
|
||||
**Last Updated**: 2025-11-06 09:51:00 UTC
|
||||
**Version**: 2.3.0
|
||||
**Last Updated**: 2025-12-08
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -128,6 +128,95 @@ const result = await DailyNotification.testAlarm({ secondsFromNow: 10 });
|
||||
console.log(`Test alarm scheduled for ${result.secondsFromNow} seconds`);
|
||||
```
|
||||
|
||||
#### iOS Only
|
||||
|
||||
##### `getNotificationPermissionStatus(): Promise<NotificationPermissionStatus>`
|
||||
|
||||
Get notification permission status on iOS. Required before scheduling notifications.
|
||||
|
||||
**Returns:**
|
||||
- `authorized`: `boolean` - Whether notifications are authorized
|
||||
- `denied`: `boolean` - Whether notifications are denied
|
||||
- `notDetermined`: `boolean` - Whether permission hasn't been requested yet
|
||||
- `provisional`: `boolean` - Whether provisional authorization is granted (iOS 12+)
|
||||
|
||||
**Example:**
|
||||
```typescript
|
||||
const status = await DailyNotification.getNotificationPermissionStatus();
|
||||
if (!status.authorized) {
|
||||
await DailyNotification.requestNotificationPermission();
|
||||
}
|
||||
```
|
||||
|
||||
##### `requestNotificationPermission(): Promise<{ granted: boolean }>`
|
||||
|
||||
Request notification permission from user. Must be called before scheduling notifications.
|
||||
|
||||
**Returns:**
|
||||
- `granted`: `boolean` - Whether permission was granted
|
||||
|
||||
**Example:**
|
||||
```typescript
|
||||
const result = await DailyNotification.requestNotificationPermission();
|
||||
if (result.granted) {
|
||||
await DailyNotification.scheduleDailyNotification({ ... });
|
||||
}
|
||||
```
|
||||
|
||||
##### `getPendingNotifications(): Promise<{ count: number; notifications: PendingNotification[] }>`
|
||||
|
||||
Get all pending notifications from UNUserNotificationCenter. Useful for debugging and verification.
|
||||
|
||||
**Returns:**
|
||||
- `count`: `number` - Number of pending notifications
|
||||
- `notifications`: `PendingNotification[]` - Array of pending notification details
|
||||
|
||||
**Example:**
|
||||
```typescript
|
||||
const result = await DailyNotification.getPendingNotifications();
|
||||
console.log(`Pending notifications: ${result.count}`);
|
||||
result.notifications.forEach(notif => {
|
||||
console.log(`Notification: ${notif.identifier} at ${notif.triggerDate}`);
|
||||
});
|
||||
```
|
||||
|
||||
##### `getBackgroundTaskStatus(): Promise<BackgroundTaskStatus>`
|
||||
|
||||
Get background task registration and execution status. Useful for debugging background prefetch.
|
||||
|
||||
**Returns:**
|
||||
- `fetchTaskRegistered`: `boolean` - Whether fetch background task is registered
|
||||
- `notifyTaskRegistered`: `boolean` - Whether notify background task is registered
|
||||
- `lastFetchExecution`: `number | null` - Last fetch execution time (epoch ms)
|
||||
- `lastNotifyExecution`: `number | null` - Last notify execution time (epoch ms)
|
||||
- `backgroundRefreshEnabled`: `boolean` - Whether Background App Refresh is enabled
|
||||
|
||||
**Example:**
|
||||
```typescript
|
||||
const status = await DailyNotification.getBackgroundTaskStatus();
|
||||
if (!status.backgroundRefreshEnabled) {
|
||||
console.warn('Background App Refresh is disabled. Enable in Settings.');
|
||||
}
|
||||
```
|
||||
|
||||
##### `openNotificationSettings(): Promise<void>`
|
||||
|
||||
Open notification settings in iOS Settings app. Useful for guiding users to enable notifications.
|
||||
|
||||
**Example:**
|
||||
```typescript
|
||||
await DailyNotification.openNotificationSettings();
|
||||
```
|
||||
|
||||
##### `openBackgroundAppRefreshSettings(): Promise<void>`
|
||||
|
||||
Open Background App Refresh settings in iOS Settings app. Useful for guiding users to enable background execution.
|
||||
|
||||
**Example:**
|
||||
```typescript
|
||||
await DailyNotification.openBackgroundAppRefreshSettings();
|
||||
```
|
||||
|
||||
### Management Methods
|
||||
|
||||
#### `maintainRollingWindow(): Promise<void>`
|
||||
@@ -239,6 +328,42 @@ interface ExactAlarmStatus {
|
||||
}
|
||||
```
|
||||
|
||||
### NotificationPermissionStatus (iOS)
|
||||
|
||||
```typescript
|
||||
interface NotificationPermissionStatus {
|
||||
authorized: boolean;
|
||||
denied: boolean;
|
||||
notDetermined: boolean;
|
||||
provisional: boolean; // iOS 12+
|
||||
}
|
||||
```
|
||||
|
||||
### PendingNotification (iOS)
|
||||
|
||||
```typescript
|
||||
interface PendingNotification {
|
||||
identifier: string;
|
||||
title: string;
|
||||
body: string;
|
||||
triggerDate: number; // epoch ms
|
||||
triggerType: 'calendar' | 'timeInterval' | 'location';
|
||||
repeats: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
### BackgroundTaskStatus (iOS)
|
||||
|
||||
```typescript
|
||||
interface BackgroundTaskStatus {
|
||||
fetchTaskRegistered: boolean;
|
||||
notifyTaskRegistered: boolean;
|
||||
lastFetchExecution: number | null; // epoch ms
|
||||
lastNotifyExecution: number | null; // epoch ms
|
||||
backgroundRefreshEnabled: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
### PerformanceMetrics
|
||||
|
||||
```typescript
|
||||
@@ -281,10 +406,26 @@ All methods return promises that reject with descriptive error messages. The plu
|
||||
|
||||
- **Network Errors**: Connection timeouts, DNS failures
|
||||
- **Storage Errors**: Database corruption, disk full
|
||||
- **Permission Errors**: Missing exact alarm permission
|
||||
- **Permission Errors**: Missing exact alarm permission (Android) or notification permission (iOS)
|
||||
- **Configuration Errors**: Invalid parameters, unsupported settings
|
||||
- **System Errors**: Out of memory, platform limitations
|
||||
|
||||
### Platform-Specific Errors
|
||||
|
||||
#### Android
|
||||
|
||||
- `EXACT_ALARM_PERMISSION_DENIED`: User denied exact alarm permission
|
||||
- `BOOT_RECEIVER_NOT_REGISTERED`: Boot receiver not properly registered
|
||||
- `ALARM_MANAGER_UNAVAILABLE`: AlarmManager service unavailable
|
||||
|
||||
#### iOS
|
||||
|
||||
- `NOTIFICATION_PERMISSION_DENIED`: User denied notification permission
|
||||
- `BACKGROUND_REFRESH_DISABLED`: Background App Refresh disabled in Settings
|
||||
- `PENDING_NOTIFICATION_LIMIT_EXCEEDED`: Exceeded 64 notification limit
|
||||
- `BG_TASK_NOT_REGISTERED`: Background task not registered in Info.plist
|
||||
- `BG_TASK_EXECUTION_FAILED`: Background task execution failed
|
||||
|
||||
## Platform Differences
|
||||
|
||||
### Android
|
||||
@@ -293,13 +434,36 @@ All methods return promises that reject with descriptive error messages. The plu
|
||||
- Falls back to windowed alarms (±10m) if exact permission denied
|
||||
- Supports reboot recovery with broadcast receivers
|
||||
- Full performance optimization features
|
||||
- Alarms do NOT persist across reboot (must reschedule)
|
||||
- Force stop clears all alarms (cannot bypass)
|
||||
- App code CAN run when alarm fires (via PendingIntent)
|
||||
|
||||
### iOS
|
||||
|
||||
- Uses `BGTaskScheduler` for background prefetch
|
||||
- Limited to 64 pending notifications
|
||||
- Uses `UNUserNotificationCenter` for notification scheduling
|
||||
- Limited to 64 pending notifications (OS constraint)
|
||||
- Automatic background task management
|
||||
- Battery optimization built-in
|
||||
- Notifications persist across app termination and reboot (OS-guaranteed)
|
||||
- App code does NOT run when notification fires (only if user taps)
|
||||
- ±180 second timing tolerance for calendar-based notifications
|
||||
- Background execution severely limited (BGTaskScheduler only, system-controlled)
|
||||
- No user-facing "force stop" equivalent
|
||||
- Must request notification permission before scheduling
|
||||
|
||||
### Key Differences Summary
|
||||
|
||||
| Feature | Android | iOS |
|
||||
| ------- | ------- | --- |
|
||||
| **Notification Persistence** | ❌ Must reschedule after reboot | ✅ Automatic (OS-guaranteed) |
|
||||
| **Code Execution on Fire** | ✅ Yes (PendingIntent) | ❌ No (only if user taps) |
|
||||
| **Background Execution** | ✅ WorkManager, JobScheduler | ⚠️ Limited (BGTaskScheduler) |
|
||||
| **Timing Accuracy** | ✅ Exact (with permission) | ⚠️ ±180 seconds tolerance |
|
||||
| **Force Stop** | ✅ User-facing option | ❌ No equivalent |
|
||||
| **Boot Recovery** | ✅ Must implement | ✅ Automatic (notifications persist) |
|
||||
| **Permission Model** | ✅ Runtime permission | ✅ Runtime permission |
|
||||
| **Pending Limit** | ✅ No limit | ❌ 64 notifications max |
|
||||
|
||||
### Electron
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# iOS Implementation Documentation Review
|
||||
|
||||
**Author**: Matthew Raymer
|
||||
**Date**: December 2024
|
||||
**Date**: 2025-12-08
|
||||
**Status**: 🎯 **ACTIVE** - Documentation Review for iOS Implementation
|
||||
**Purpose**: Ensure Android plugin and test app documentation contains sufficient detail for iOS implementation to mirror all features
|
||||
|
||||
@@ -653,6 +653,6 @@ The Android plugin and test app documentation is **comprehensive and well-struct
|
||||
---
|
||||
|
||||
**Document Version**: 1.0.0
|
||||
**Last Updated**: December 2024
|
||||
**Last Updated**: 2025-12-08
|
||||
**Next Review**: After iOS implementation begins
|
||||
|
||||
|
||||
697
docs/ios-core-data-migration.md
Normal file
697
docs/ios-core-data-migration.md
Normal file
@@ -0,0 +1,697 @@
|
||||
# iOS Core Data Migration Guide: Android Room → iOS Core Data
|
||||
|
||||
**Author**: Matthew Raymer
|
||||
**Date**: 2025-12-08
|
||||
**Status**: 🎯 **ACTIVE** - Database Migration Reference
|
||||
**Version**: 1.0.0
|
||||
**Last Synced With Plugin Version**: v1.1.0
|
||||
|
||||
## Purpose
|
||||
|
||||
This document provides a comprehensive mapping guide for migrating Android Room database entities to iOS Core Data entities, ensuring cross-platform data consistency and feature parity.
|
||||
|
||||
**Reference**:
|
||||
- [Android Database Schema](../android/src/main/java/com/timesafari/dailynotification/DatabaseSchema.kt) - Android Room schema
|
||||
- [iOS Core Data Model](../ios/Plugin/DailyNotificationModel.xcdatamodeld) - iOS Core Data model
|
||||
- [Database Consolidation Plan](../android/DATABASE_CONSOLIDATION_PLAN.md) - Unified schema design
|
||||
|
||||
---
|
||||
|
||||
## 1. Entity Mapping Overview
|
||||
|
||||
### 1.1 Complete Entity Mapping
|
||||
|
||||
| Android Room Entity | iOS Core Data Entity | Status | Priority |
|
||||
| ------------------- | -------------------- | ------ | -------- |
|
||||
| `ContentCache` | `ContentCache` | ✅ Implemented | - |
|
||||
| `Schedule` | `Schedule` | ✅ Implemented | - |
|
||||
| `Callback` | `Callback` | ✅ Implemented | - |
|
||||
| `History` | `History` | ✅ Implemented | - |
|
||||
| `NotificationContentEntity` | `NotificationContent` | ❌ Missing | **High** |
|
||||
| `NotificationDeliveryEntity` | `NotificationDelivery` | ❌ Missing | **High** |
|
||||
| `NotificationConfigEntity` | `NotificationConfig` | ❌ Missing | **Medium** |
|
||||
|
||||
### 1.2 Current Implementation Status
|
||||
|
||||
**✅ Implemented (4 entities)**:
|
||||
- `ContentCache` - Fetched content with TTL
|
||||
- `Schedule` - Recurring schedule patterns
|
||||
- `Callback` - Callback configurations
|
||||
- `History` - Execution history
|
||||
|
||||
**❌ Missing (3 entities)**:
|
||||
- `NotificationContent` - Specific notification instances
|
||||
- `NotificationDelivery` - Delivery tracking/analytics
|
||||
- `NotificationConfig` - Configuration management
|
||||
|
||||
---
|
||||
|
||||
## 2. Detailed Entity Mappings
|
||||
|
||||
### 2.1 ContentCache Entity
|
||||
|
||||
**Status**: ✅ **Implemented**
|
||||
|
||||
**Android Room**:
|
||||
```kotlin
|
||||
@Entity(tableName = "content_cache")
|
||||
data class ContentCache(
|
||||
@PrimaryKey val id: String,
|
||||
val fetchedAt: Long, // epoch ms
|
||||
val ttlSeconds: Int,
|
||||
val payload: ByteArray, // BLOB
|
||||
val meta: String? = null
|
||||
)
|
||||
```
|
||||
|
||||
**iOS Core Data**:
|
||||
```swift
|
||||
@objc(ContentCache)
|
||||
public class ContentCache: NSManagedObject {
|
||||
@NSManaged public var id: String?
|
||||
@NSManaged public var fetchedAt: Date?
|
||||
@NSManaged public var ttlSeconds: Int32
|
||||
@NSManaged public var payload: Data?
|
||||
@NSManaged public var meta: String?
|
||||
}
|
||||
```
|
||||
|
||||
**Mapping Notes**:
|
||||
- ✅ All fields mapped correctly
|
||||
- ✅ `Long` (epoch ms) → `Date` conversion handled
|
||||
- ✅ `ByteArray` → `Data` conversion handled
|
||||
- ✅ Optional fields properly marked
|
||||
|
||||
**Migration Status**: ✅ **Complete**
|
||||
|
||||
---
|
||||
|
||||
### 2.2 Schedule Entity
|
||||
|
||||
**Status**: ✅ **Implemented**
|
||||
|
||||
**Android Room**:
|
||||
```kotlin
|
||||
@Entity(tableName = "schedules")
|
||||
data class Schedule(
|
||||
@PrimaryKey val id: String,
|
||||
val kind: String, // 'fetch' or 'notify'
|
||||
val cron: String? = null,
|
||||
val clockTime: String? = null, // HH:mm
|
||||
val enabled: Boolean = true,
|
||||
val lastRunAt: Long? = null, // epoch ms
|
||||
val nextRunAt: Long? = null, // epoch ms
|
||||
val jitterMs: Int = 0,
|
||||
val backoffPolicy: String = "exp",
|
||||
val stateJson: String? = null
|
||||
)
|
||||
```
|
||||
|
||||
**iOS Core Data**:
|
||||
```swift
|
||||
@objc(Schedule)
|
||||
public class Schedule: NSManagedObject {
|
||||
@NSManaged public var id: String?
|
||||
@NSManaged public var kind: String?
|
||||
@NSManaged public var cron: String?
|
||||
@NSManaged public var clockTime: String?
|
||||
@NSManaged public var enabled: Bool
|
||||
@NSManaged public var lastRunAt: Date?
|
||||
@NSManaged public var nextRunAt: Date?
|
||||
@NSManaged public var jitterMs: Int32
|
||||
@NSManaged public var backoffPolicy: String?
|
||||
@NSManaged public var stateJson: String?
|
||||
}
|
||||
```
|
||||
|
||||
**Mapping Notes**:
|
||||
- ✅ All fields mapped correctly
|
||||
- ✅ `Long` (epoch ms) → `Date` conversion handled
|
||||
- ✅ Default values preserved
|
||||
- ✅ Optional fields properly marked
|
||||
|
||||
**Migration Status**: ✅ **Complete**
|
||||
|
||||
---
|
||||
|
||||
### 2.3 Callback Entity
|
||||
|
||||
**Status**: ✅ **Implemented**
|
||||
|
||||
**Android Room**:
|
||||
```kotlin
|
||||
@Entity(tableName = "callbacks")
|
||||
data class Callback(
|
||||
@PrimaryKey val id: String,
|
||||
val kind: String, // 'http', 'local', 'queue'
|
||||
val target: String,
|
||||
val headersJson: String? = null,
|
||||
val enabled: Boolean = true,
|
||||
val createdAt: Long // epoch ms
|
||||
)
|
||||
```
|
||||
|
||||
**iOS Core Data**:
|
||||
```swift
|
||||
@objc(Callback)
|
||||
public class Callback: NSManagedObject {
|
||||
@NSManaged public var id: String?
|
||||
@NSManaged public var kind: String?
|
||||
@NSManaged public var target: String?
|
||||
@NSManaged public var headersJson: String?
|
||||
@NSManaged public var enabled: Bool
|
||||
@NSManaged public var createdAt: Date?
|
||||
}
|
||||
```
|
||||
|
||||
**Mapping Notes**:
|
||||
- ✅ All fields mapped correctly
|
||||
- ✅ `Long` (epoch ms) → `Date` conversion handled
|
||||
- ✅ Optional fields properly marked
|
||||
|
||||
**Migration Status**: ✅ **Complete**
|
||||
|
||||
---
|
||||
|
||||
### 2.4 History Entity
|
||||
|
||||
**Status**: ✅ **Implemented**
|
||||
|
||||
**Android Room**:
|
||||
```kotlin
|
||||
@Entity(tableName = "history")
|
||||
data class History(
|
||||
@PrimaryKey(autoGenerate = true) val id: Int = 0,
|
||||
val refId: String,
|
||||
val kind: String, // fetch/notify/callback
|
||||
val occurredAt: Long, // epoch ms
|
||||
val durationMs: Long? = null,
|
||||
val outcome: String,
|
||||
val diagJson: String? = null
|
||||
)
|
||||
```
|
||||
|
||||
**iOS Core Data**:
|
||||
```swift
|
||||
@objc(History)
|
||||
public class History: NSManagedObject {
|
||||
@NSManaged public var id: String?
|
||||
@NSManaged public var refId: String?
|
||||
@NSManaged public var kind: String?
|
||||
@NSManaged public var occurredAt: Date?
|
||||
@NSManaged public var durationMs: Int32
|
||||
@NSManaged public var outcome: String?
|
||||
@NSManaged public var diagJson: String?
|
||||
}
|
||||
```
|
||||
|
||||
**Mapping Notes**:
|
||||
- ⚠️ `id` type differs: Android uses `Int` (auto-generated), iOS uses `String`
|
||||
- ✅ `Long` (epoch ms) → `Date` conversion handled
|
||||
- ✅ Optional fields properly marked
|
||||
|
||||
**Migration Consideration**: iOS uses `String` for `id` instead of auto-generated `Int`. This is acceptable as long as IDs are generated as UUIDs.
|
||||
|
||||
**Migration Status**: ✅ **Complete** (with note)
|
||||
|
||||
---
|
||||
|
||||
### 2.5 NotificationContent Entity
|
||||
|
||||
**Status**: ❌ **Missing - High Priority**
|
||||
|
||||
**Android Room**:
|
||||
```kotlin
|
||||
@Entity(tableName = "notification_content")
|
||||
data class NotificationContentEntity(
|
||||
@PrimaryKey val id: String,
|
||||
val pluginVersion: String?,
|
||||
val timesafariDid: String?,
|
||||
val notificationType: String?,
|
||||
val title: String?,
|
||||
val body: String?,
|
||||
val scheduledTime: Long, // epoch ms
|
||||
val timezone: String?,
|
||||
val priority: Int,
|
||||
val vibrationEnabled: Boolean,
|
||||
val soundEnabled: Boolean,
|
||||
val mediaUrl: String?,
|
||||
val encryptedContent: String?,
|
||||
val encryptionKeyId: String?,
|
||||
val createdAt: Long, // epoch ms
|
||||
val updatedAt: Long, // epoch ms
|
||||
val ttlSeconds: Long,
|
||||
val deliveryStatus: String?,
|
||||
val deliveryAttempts: Int,
|
||||
val lastDeliveryAttempt: Long, // epoch ms
|
||||
val userInteractionCount: Int,
|
||||
val lastUserInteraction: Long, // epoch ms
|
||||
val metadata: String?
|
||||
)
|
||||
```
|
||||
|
||||
**Required iOS Core Data Entity**:
|
||||
```swift
|
||||
@objc(NotificationContent)
|
||||
public class NotificationContent: NSManagedObject {
|
||||
@NSManaged public var id: String?
|
||||
@NSManaged public var pluginVersion: String?
|
||||
@NSManaged public var timesafariDid: String?
|
||||
@NSManaged public var notificationType: String?
|
||||
@NSManaged public var title: String?
|
||||
@NSManaged public var body: String?
|
||||
@NSManaged public var scheduledTime: Date?
|
||||
@NSManaged public var timezone: String?
|
||||
@NSManaged public var priority: Int32
|
||||
@NSManaged public var vibrationEnabled: Bool
|
||||
@NSManaged public var soundEnabled: Bool
|
||||
@NSManaged public var mediaUrl: String?
|
||||
@NSManaged public var encryptedContent: String?
|
||||
@NSManaged public var encryptionKeyId: String?
|
||||
@NSManaged public var createdAt: Date?
|
||||
@NSManaged public var updatedAt: Date?
|
||||
@NSManaged public var ttlSeconds: Int64
|
||||
@NSManaged public var deliveryStatus: String?
|
||||
@NSManaged public var deliveryAttempts: Int32
|
||||
@NSManaged public var lastDeliveryAttempt: Date?
|
||||
@NSManaged public var userInteractionCount: Int32
|
||||
@NSManaged public var lastUserInteraction: Date?
|
||||
@NSManaged public var metadata: String?
|
||||
}
|
||||
```
|
||||
|
||||
**Required Core Data Model XML**:
|
||||
```xml
|
||||
<entity name="NotificationContent" representedClassName="NotificationContent" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="id" optional="NO" attributeType="String"/>
|
||||
<attribute name="pluginVersion" optional="YES" attributeType="String"/>
|
||||
<attribute name="timesafariDid" optional="YES" attributeType="String"/>
|
||||
<attribute name="notificationType" optional="YES" attributeType="String"/>
|
||||
<attribute name="title" optional="YES" attributeType="String"/>
|
||||
<attribute name="body" optional="YES" attributeType="String"/>
|
||||
<attribute name="scheduledTime" optional="NO" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="timezone" optional="YES" attributeType="String"/>
|
||||
<attribute name="priority" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="vibrationEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="soundEnabled" optional="YES" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<attribute name="mediaUrl" optional="YES" attributeType="String"/>
|
||||
<attribute name="encryptedContent" optional="YES" attributeType="String"/>
|
||||
<attribute name="encryptionKeyId" optional="YES" attributeType="String"/>
|
||||
<attribute name="createdAt" optional="NO" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="updatedAt" optional="NO" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="ttlSeconds" optional="YES" attributeType="Integer 64" defaultValueString="604800" usesScalarValueType="YES"/>
|
||||
<attribute name="deliveryStatus" optional="YES" attributeType="String"/>
|
||||
<attribute name="deliveryAttempts" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="lastDeliveryAttempt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="userInteractionCount" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="lastUserInteraction" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="metadata" optional="YES" attributeType="String"/>
|
||||
<index name="index_notification_content_timesafari_did">
|
||||
<indexElement value="timesafariDid"/>
|
||||
</index>
|
||||
<index name="index_notification_content_notification_type">
|
||||
<indexElement value="notificationType"/>
|
||||
</index>
|
||||
<index name="index_notification_content_scheduled_time">
|
||||
<indexElement value="scheduledTime"/>
|
||||
</index>
|
||||
</entity>
|
||||
```
|
||||
|
||||
**Mapping Notes**:
|
||||
- `Long` (epoch ms) → `Date` conversion required
|
||||
- `Int` → `Int32` conversion
|
||||
- `Long` (ttlSeconds) → `Int64` conversion
|
||||
- Indexes should be added for performance
|
||||
|
||||
**Migration Status**: ❌ **Not Implemented**
|
||||
|
||||
---
|
||||
|
||||
### 2.6 NotificationDelivery Entity
|
||||
|
||||
**Status**: ❌ **Missing - High Priority**
|
||||
|
||||
**Android Room**:
|
||||
```kotlin
|
||||
@Entity(
|
||||
tableName = "notification_delivery",
|
||||
foreignKeys = @ForeignKey(
|
||||
entity = NotificationContentEntity::class,
|
||||
parentColumns = ["id"],
|
||||
childColumns = ["notification_id"],
|
||||
onDelete = ForeignKey.CASCADE
|
||||
)
|
||||
)
|
||||
data class NotificationDeliveryEntity(
|
||||
@PrimaryKey val id: String,
|
||||
val notificationId: String,
|
||||
val timesafariDid: String?,
|
||||
val deliveryTimestamp: Long, // epoch ms
|
||||
val deliveryStatus: String?,
|
||||
val deliveryMethod: String?,
|
||||
val deliveryAttemptNumber: Int,
|
||||
val deliveryDurationMs: Long,
|
||||
val userInteractionType: String?,
|
||||
val userInteractionTimestamp: Long, // epoch ms
|
||||
val userInteractionDurationMs: Long,
|
||||
val errorCode: String?,
|
||||
val errorMessage: String?,
|
||||
val deviceInfo: String?,
|
||||
val networkInfo: String?,
|
||||
val batteryLevel: Int,
|
||||
val dozeModeActive: Boolean,
|
||||
val exactAlarmPermission: Boolean,
|
||||
val notificationPermission: Boolean,
|
||||
val metadata: String?
|
||||
)
|
||||
```
|
||||
|
||||
**Required iOS Core Data Entity**:
|
||||
```swift
|
||||
@objc(NotificationDelivery)
|
||||
public class NotificationDelivery: NSManagedObject {
|
||||
@NSManaged public var id: String?
|
||||
@NSManaged public var notificationId: String?
|
||||
@NSManaged public var notificationContent: NotificationContent? // Relationship
|
||||
@NSManaged public var timesafariDid: String?
|
||||
@NSManaged public var deliveryTimestamp: Date?
|
||||
@NSManaged public var deliveryStatus: String?
|
||||
@NSManaged public var deliveryMethod: String?
|
||||
@NSManaged public var deliveryAttemptNumber: Int32
|
||||
@NSManaged public var deliveryDurationMs: Int64
|
||||
@NSManaged public var userInteractionType: String?
|
||||
@NSManaged public var userInteractionTimestamp: Date?
|
||||
@NSManaged public var userInteractionDurationMs: Int64
|
||||
@NSManaged public var errorCode: String?
|
||||
@NSManaged public var errorMessage: String?
|
||||
@NSManaged public var deviceInfo: String?
|
||||
@NSManaged public var networkInfo: String?
|
||||
@NSManaged public var batteryLevel: Int32
|
||||
@NSManaged public var dozeModeActive: Bool
|
||||
@NSManaged public var exactAlarmPermission: Bool
|
||||
@NSManaged public var notificationPermission: Bool
|
||||
@NSManaged public var metadata: String?
|
||||
}
|
||||
```
|
||||
|
||||
**Required Core Data Model XML**:
|
||||
```xml
|
||||
<entity name="NotificationDelivery" representedClassName="NotificationDelivery" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="id" optional="NO" attributeType="String"/>
|
||||
<attribute name="notificationId" optional="YES" attributeType="String"/>
|
||||
<attribute name="timesafariDid" optional="YES" attributeType="String"/>
|
||||
<attribute name="deliveryTimestamp" optional="NO" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="deliveryStatus" optional="YES" attributeType="String"/>
|
||||
<attribute name="deliveryMethod" optional="YES" attributeType="String"/>
|
||||
<attribute name="deliveryAttemptNumber" optional="YES" attributeType="Integer 32" defaultValueString="1" usesScalarValueType="YES"/>
|
||||
<attribute name="deliveryDurationMs" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="userInteractionType" optional="YES" attributeType="String"/>
|
||||
<attribute name="userInteractionTimestamp" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="userInteractionDurationMs" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="errorCode" optional="YES" attributeType="String"/>
|
||||
<attribute name="errorMessage" optional="YES" attributeType="String"/>
|
||||
<attribute name="deviceInfo" optional="YES" attributeType="String"/>
|
||||
<attribute name="networkInfo" optional="YES" attributeType="String"/>
|
||||
<attribute name="batteryLevel" optional="YES" attributeType="Integer 32" defaultValueString="-1" usesScalarValueType="YES"/>
|
||||
<attribute name="dozeModeActive" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="exactAlarmPermission" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="notificationPermission" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="metadata" optional="YES" attributeType="String"/>
|
||||
<relationship name="notificationContent" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="NotificationContent" inverseName="deliveries" inverseEntity="NotificationContent"/>
|
||||
<index name="index_notification_delivery_notification_id">
|
||||
<indexElement value="notificationId"/>
|
||||
</index>
|
||||
<index name="index_notification_delivery_delivery_timestamp">
|
||||
<indexElement value="deliveryTimestamp"/>
|
||||
</index>
|
||||
</entity>
|
||||
```
|
||||
|
||||
**Mapping Notes**:
|
||||
- Foreign key relationship should be modeled as Core Data relationship
|
||||
- `Long` (epoch ms) → `Date` conversion required
|
||||
- `Int` → `Int32` conversion
|
||||
- `Long` (duration) → `Int64` conversion
|
||||
- Cascade delete should be configured
|
||||
|
||||
**Migration Status**: ❌ **Not Implemented**
|
||||
|
||||
---
|
||||
|
||||
### 2.7 NotificationConfig Entity
|
||||
|
||||
**Status**: ❌ **Missing - Medium Priority**
|
||||
|
||||
**Android Room**:
|
||||
```kotlin
|
||||
@Entity(tableName = "notification_config")
|
||||
data class NotificationConfigEntity(
|
||||
@PrimaryKey val id: String,
|
||||
val timesafariDid: String?,
|
||||
val configType: String?,
|
||||
val configKey: String?,
|
||||
val configValue: String?,
|
||||
val configDataType: String?,
|
||||
val isEncrypted: Boolean,
|
||||
val encryptionKeyId: String?,
|
||||
val createdAt: Long, // epoch ms
|
||||
val updatedAt: Long, // epoch ms
|
||||
val ttlSeconds: Long,
|
||||
val isActive: Boolean,
|
||||
val metadata: String?
|
||||
)
|
||||
```
|
||||
|
||||
**Required iOS Core Data Entity**:
|
||||
```swift
|
||||
@objc(NotificationConfig)
|
||||
public class NotificationConfig: NSManagedObject {
|
||||
@NSManaged public var id: String?
|
||||
@NSManaged public var timesafariDid: String?
|
||||
@NSManaged public var configType: String?
|
||||
@NSManaged public var configKey: String?
|
||||
@NSManaged public var configValue: String?
|
||||
@NSManaged public var configDataType: String?
|
||||
@NSManaged public var isEncrypted: Bool
|
||||
@NSManaged public var encryptionKeyId: String?
|
||||
@NSManaged public var createdAt: Date?
|
||||
@NSManaged public var updatedAt: Date?
|
||||
@NSManaged public var ttlSeconds: Int64
|
||||
@NSManaged public var isActive: Bool
|
||||
@NSManaged public var metadata: String?
|
||||
}
|
||||
```
|
||||
|
||||
**Mapping Notes**:
|
||||
- `Long` (epoch ms) → `Date` conversion required
|
||||
- `Long` (ttlSeconds) → `Int64` conversion
|
||||
- Indexes should be added for performance
|
||||
|
||||
**Migration Status**: ❌ **Not Implemented**
|
||||
|
||||
---
|
||||
|
||||
## 3. Data Type Conversions
|
||||
|
||||
### 3.1 Time Conversions
|
||||
|
||||
**Android → iOS**:
|
||||
- `Long` (epoch milliseconds) → `Date`
|
||||
- Conversion: `Date(timeIntervalSince1970: Double(milliseconds) / 1000.0)`
|
||||
|
||||
**iOS → Android**:
|
||||
- `Date` → `Long` (epoch milliseconds)
|
||||
- Conversion: `Int64(date.timeIntervalSince1970 * 1000)`
|
||||
|
||||
### 3.2 Numeric Conversions
|
||||
|
||||
| Android Type | iOS Type | Notes |
|
||||
| ------------ | -------- | ----- |
|
||||
| `Int` | `Int32` | Direct mapping |
|
||||
| `Long` | `Int64` | For large values |
|
||||
| `Boolean` | `Bool` | Direct mapping |
|
||||
| `ByteArray` | `Data` | Binary data |
|
||||
|
||||
### 3.3 String Conversions
|
||||
|
||||
- `String?` → `String?` (direct mapping)
|
||||
- JSON fields: `String?` → `String?` (parse as needed)
|
||||
|
||||
---
|
||||
|
||||
## 4. Index Mapping
|
||||
|
||||
### 4.1 Required Indexes
|
||||
|
||||
**NotificationContent**:
|
||||
- `timesafariDid` (for user queries)
|
||||
- `notificationType` (for type filtering)
|
||||
- `scheduledTime` (for time-based queries)
|
||||
- `createdAt` (for chronological queries)
|
||||
|
||||
**NotificationDelivery**:
|
||||
- `notificationId` (for foreign key lookups)
|
||||
- `deliveryTimestamp` (for time-based queries)
|
||||
- `deliveryStatus` (for status filtering)
|
||||
- `timesafariDid` (for user queries)
|
||||
|
||||
**NotificationConfig**:
|
||||
- `timesafariDid` (for user queries)
|
||||
- `configType` (for type filtering)
|
||||
- `updatedAt` (for chronological queries)
|
||||
|
||||
---
|
||||
|
||||
## 5. Relationship Mapping
|
||||
|
||||
### 5.1 Foreign Key Relationships
|
||||
|
||||
**Android Room**:
|
||||
```kotlin
|
||||
@ForeignKey(
|
||||
entity = NotificationContentEntity::class,
|
||||
parentColumns = ["id"],
|
||||
childColumns = ["notification_id"],
|
||||
onDelete = ForeignKey.CASCADE
|
||||
)
|
||||
```
|
||||
|
||||
**iOS Core Data**:
|
||||
```xml
|
||||
<relationship
|
||||
name="notificationContent"
|
||||
optional="YES"
|
||||
maxCount="1"
|
||||
deletionRule="Cascade"
|
||||
destinationEntity="NotificationContent"
|
||||
inverseName="deliveries"
|
||||
inverseEntity="NotificationContent"/>
|
||||
```
|
||||
|
||||
**Inverse Relationship** (NotificationContent → NotificationDelivery):
|
||||
```xml
|
||||
<relationship
|
||||
name="deliveries"
|
||||
optional="YES"
|
||||
toMany="YES"
|
||||
deletionRule="Nullify"
|
||||
destinationEntity="NotificationDelivery"
|
||||
inverseName="notificationContent"
|
||||
inverseEntity="NotificationDelivery"/>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Implementation Checklist
|
||||
|
||||
### 6.1 High Priority (Required for Feature Parity)
|
||||
|
||||
- [ ] Add `NotificationContent` entity to Core Data model
|
||||
- [ ] Add `NotificationDelivery` entity to Core Data model
|
||||
- [ ] Configure foreign key relationship between `NotificationContent` and `NotificationDelivery`
|
||||
- [ ] Add required indexes for performance
|
||||
- [ ] Implement Swift extensions for entity classes
|
||||
- [ ] Add data conversion helpers (Date ↔ Long)
|
||||
- [ ] Test entity creation and relationships
|
||||
- [ ] Test cascade delete behavior
|
||||
|
||||
### 6.2 Medium Priority (Configuration Management)
|
||||
|
||||
- [ ] Add `NotificationConfig` entity to Core Data model
|
||||
- [ ] Add required indexes
|
||||
- [ ] Implement Swift extensions
|
||||
- [ ] Test configuration CRUD operations
|
||||
|
||||
### 6.3 Low Priority (Optimization)
|
||||
|
||||
- [ ] Add migration policies for schema changes
|
||||
- [ ] Add data validation rules
|
||||
- [ ] Optimize fetch requests with predicates
|
||||
- [ ] Add batch operations support
|
||||
|
||||
---
|
||||
|
||||
## 7. Migration Steps
|
||||
|
||||
### 7.1 Step 1: Update Core Data Model
|
||||
|
||||
1. Open `DailyNotificationModel.xcdatamodeld` in Xcode
|
||||
2. Add `NotificationContent` entity with all attributes
|
||||
3. Add `NotificationDelivery` entity with all attributes
|
||||
4. Add `NotificationConfig` entity with all attributes
|
||||
5. Configure relationships between entities
|
||||
6. Add indexes for performance
|
||||
7. Set code generation to "Class Definition"
|
||||
|
||||
### 7.2 Step 2: Create Swift Extensions
|
||||
|
||||
1. Create `NotificationContent+CoreDataClass.swift`
|
||||
2. Create `NotificationContent+CoreDataProperties.swift`
|
||||
3. Create `NotificationDelivery+CoreDataClass.swift`
|
||||
4. Create `NotificationDelivery+CoreDataProperties.swift`
|
||||
5. Create `NotificationConfig+CoreDataClass.swift`
|
||||
6. Create `NotificationConfig+CoreDataProperties.swift`
|
||||
|
||||
### 7.3 Step 3: Implement Data Access Layer
|
||||
|
||||
1. Create DAO classes for each entity
|
||||
2. Implement CRUD operations
|
||||
3. Add data conversion helpers
|
||||
4. Add query methods with predicates
|
||||
|
||||
### 7.4 Step 4: Update Persistence Controller
|
||||
|
||||
1. Update `PersistenceController` to handle new entities
|
||||
2. Add migration policies if needed
|
||||
3. Test database initialization
|
||||
|
||||
### 7.5 Step 5: Testing
|
||||
|
||||
1. Test entity creation
|
||||
2. Test relationships
|
||||
3. Test cascade delete
|
||||
4. Test data conversion (Date ↔ Long)
|
||||
5. Test query performance with indexes
|
||||
|
||||
---
|
||||
|
||||
## 8. Data Migration (If Needed)
|
||||
|
||||
### 8.1 Migration from SQLite to Core Data
|
||||
|
||||
If migrating existing SQLite data to Core Data:
|
||||
|
||||
1. Read data from SQLite database
|
||||
2. Convert data types (Long → Date, etc.)
|
||||
3. Create Core Data entities
|
||||
4. Save to Core Data store
|
||||
5. Verify data integrity
|
||||
|
||||
### 8.2 Migration Script Example
|
||||
|
||||
```swift
|
||||
func migrateSQLiteToCoreData(sqlitePath: String, coreDataStack: NSPersistentContainer) {
|
||||
// 1. Open SQLite database
|
||||
// 2. Query all tables
|
||||
// 3. Convert each row to Core Data entity
|
||||
// 4. Save to Core Data store
|
||||
// 5. Verify migration success
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. References
|
||||
|
||||
- [Android Database Schema](../android/src/main/java/com/timesafari/dailynotification/DatabaseSchema.kt)
|
||||
- [iOS Core Data Model](../ios/Plugin/DailyNotificationModel.xcdatamodeld)
|
||||
- [Database Consolidation Plan](../android/DATABASE_CONSOLIDATION_PLAN.md)
|
||||
- [Core Data Programming Guide](https://developer.apple.com/documentation/coredata)
|
||||
|
||||
---
|
||||
|
||||
**Document Version**: 1.0.0
|
||||
**Last Updated**: 2025-12-08
|
||||
**Next Review**: After missing entities are implemented
|
||||
|
||||
395
docs/ios-implementation-directive.md
Normal file
395
docs/ios-implementation-directive.md
Normal file
@@ -0,0 +1,395 @@
|
||||
# iOS Implementation Directive: App Launch Recovery & Missed Notification Detection
|
||||
|
||||
**Author**: Matthew Raymer
|
||||
**Date**: 2025-12-08
|
||||
**Status**: Active Implementation Directive - iOS Only
|
||||
**Version**: 1.0.0
|
||||
**Last Synced With Plugin Version**: v1.1.0
|
||||
|
||||
## Purpose
|
||||
|
||||
This directive provides **descriptive overview and integration guidance** for iOS-specific recovery and missed notification detection:
|
||||
|
||||
1. App Launch Recovery (cold/warm/terminated)
|
||||
2. Missed Notification Detection
|
||||
3. App Termination Detection
|
||||
4. Background Task Registration for Boot Recovery
|
||||
|
||||
**⚠️ CRITICAL**: This document is **descriptive and integrative**. The **normative implementation instructions** are in the Phase 1–3 directives below. **If any code or behavior in this file conflicts with a Phase directive, the Phase directive wins.**
|
||||
|
||||
**Reference**: See [Plugin Requirements](./alarms/03-plugin-requirements.md) for requirements that Phase directives implement.
|
||||
|
||||
**Reference**: See [Platform Capability Reference](./alarms/01-platform-capability-reference.md) for iOS OS-level facts.
|
||||
|
||||
**⚠️ IMPORTANT**: For implementation, use the phase-specific directives (these are the canonical source of truth):
|
||||
|
||||
- **[Phase 1: Cold Start Recovery](./ios-implementation-directive-phase1.md)** - Minimal viable recovery
|
||||
- Implements: [Plugin Requirements §3.1.2](./alarms/03-plugin-requirements.md#312-app-cold-start) (iOS equivalent)
|
||||
- Explicit acceptance criteria, rollback safety, data integrity checks
|
||||
- **Start here** for fastest implementation
|
||||
|
||||
- **[Phase 2: App Termination Detection & Recovery](./ios-implementation-directive-phase2.md)** - Comprehensive termination handling
|
||||
- Implements: iOS-specific app termination scenarios
|
||||
- Prerequisite: Phase 1 complete
|
||||
|
||||
- **[Phase 3: Background Task Registration & Boot Recovery](./ios-implementation-directive-phase3.md)** - Background task enhancement
|
||||
- Implements: BGTaskScheduler registration for boot recovery
|
||||
- Prerequisites: Phase 1 and Phase 2 complete
|
||||
|
||||
**See Also**: [Unified Alarm Directive](./alarms/000-UNIFIED-ALARM-DIRECTIVE.md) for master coordination document.
|
||||
|
||||
---
|
||||
|
||||
## 1. Implementation Overview
|
||||
|
||||
### 1.1 What Needs to Be Implemented
|
||||
|
||||
| Feature | Status | Priority | Location |
|
||||
| ------- | ------ | -------- | -------- |
|
||||
| App Launch Recovery | ❌ Missing | **High** | `DailyNotificationPlugin.swift` - `load()` method |
|
||||
| Missed Notification Detection | ⚠️ Partial | **High** | `DailyNotificationPlugin.swift` - new method |
|
||||
| App Termination Detection | ❌ Missing | **High** | `DailyNotificationPlugin.swift` - recovery logic |
|
||||
| Background Task Registration | ⚠️ Partial | **Medium** | `AppDelegate.swift` - BGTaskScheduler registration |
|
||||
|
||||
### 1.2 Implementation Strategy
|
||||
|
||||
**Phase 1** – Cold start recovery only
|
||||
- Missed notification detection + future notification verification
|
||||
- No termination detection, no boot handling
|
||||
- **See [Phase 1 directive](./ios-implementation-directive-phase1.md) for implementation**
|
||||
|
||||
**Phase 2** – App termination detection & full recovery
|
||||
- Termination detection via UNUserNotificationCenter state comparison
|
||||
- Comprehensive recovery of all schedules (notify + fetch)
|
||||
- Past notifications marked as missed, future notifications rescheduled
|
||||
- **See [Phase 2 directive](./ios-implementation-directive-phase2.md) for implementation**
|
||||
|
||||
**Phase 3** – Background task registration & boot recovery
|
||||
- BGTaskScheduler registration for boot recovery
|
||||
- Next occurrence rescheduled for repeating schedules
|
||||
- **See [Phase 3 directive](./ios-implementation-directive-phase3.md) for implementation**
|
||||
|
||||
---
|
||||
|
||||
## 2. iOS-Specific Considerations
|
||||
|
||||
### 2.1 Key Differences from Android
|
||||
|
||||
**iOS Advantages**:
|
||||
- ✅ Notifications persist across app termination (OS-guaranteed)
|
||||
- ✅ Notifications persist across device reboot (OS-guaranteed)
|
||||
- ✅ No force stop equivalent (iOS doesn't have user-facing force stop)
|
||||
|
||||
**iOS Challenges**:
|
||||
- ❌ App code does NOT run when notification fires (only if user taps)
|
||||
- ❌ Background execution severely limited (BGTaskScheduler only)
|
||||
- ❌ Cannot rely on background execution for recovery
|
||||
- ❌ Must detect missed notifications on app launch
|
||||
|
||||
**Platform Reference**: See [Platform Capability Reference §3](./alarms/01-platform-capability-reference.md#3-ios-notification-capability-matrix) for complete iOS behavior matrix.
|
||||
|
||||
### 2.2 Recovery Scenario Mapping
|
||||
|
||||
**Android → iOS Mapping**:
|
||||
|
||||
| Android Scenario | iOS Equivalent | Detection Method |
|
||||
| ---------------- | -------------- | --------------- |
|
||||
| `COLD_START` | App Launch After Termination | Check if notifications exist vs DB state |
|
||||
| `FORCE_STOP` | App Terminated by System | Check if notifications missing vs DB state |
|
||||
| `BOOT` | Device Reboot | BGTaskScheduler registration (Phase 3) |
|
||||
| `WARM_START` | App Resume (Foreground) | Check app state on resume |
|
||||
|
||||
**Note**: iOS doesn't have a user-facing "force stop" equivalent. System termination is detected by comparing UNUserNotificationCenter state with database state.
|
||||
|
||||
### 2.3 iOS APIs Used
|
||||
|
||||
**Notification Management**:
|
||||
- `UNUserNotificationCenter.current()` - Notification center
|
||||
- `UNUserNotificationCenter.getPendingNotificationRequests()` - Check scheduled notifications
|
||||
- `UNUserNotificationCenter.add()` - Schedule notifications
|
||||
|
||||
**Background Tasks**:
|
||||
- `BGTaskScheduler.shared` - Background task scheduler
|
||||
- `BGTaskScheduler.register()` - Register background task handlers
|
||||
- `BGAppRefreshTaskRequest` - Background fetch requests
|
||||
|
||||
**App Lifecycle**:
|
||||
- `applicationWillTerminate` - App termination notification
|
||||
- `applicationDidBecomeActive` - App foreground notification
|
||||
- `applicationDidEnterBackground` - App background notification
|
||||
|
||||
---
|
||||
|
||||
## 3. Implementation: ReactivationManager (iOS)
|
||||
|
||||
**⚠️ Illustrative only** – See Phase 1 and Phase 2 directives for canonical implementation.
|
||||
|
||||
**ReactivationManager Responsibilities by Phase**:
|
||||
|
||||
| Phase | Responsibilities |
|
||||
| ----- | ---------------- |
|
||||
| 1 | Cold start only (missed detection + verify/reschedule future) |
|
||||
| 2 | Adds termination detection & recovery |
|
||||
| 3 | Background task registration & boot recovery |
|
||||
|
||||
**For implementation details, see**:
|
||||
- [Phase 1: ReactivationManager creation](./ios-implementation-directive-phase1.md#2-implementation-reactivationmanager)
|
||||
- [Phase 2: Termination detection](./ios-implementation-directive-phase2.md#2-implementation-termination-detection)
|
||||
|
||||
### 3.1 Create New File
|
||||
|
||||
**File**: `ios/Plugin/DailyNotificationReactivationManager.swift`
|
||||
|
||||
**Purpose**: Centralized recovery logic for app launch scenarios
|
||||
|
||||
### 3.2 Class Structure
|
||||
|
||||
**⚠️ Illustrative only** – See Phase 1 for canonical implementation.
|
||||
|
||||
```swift
|
||||
import Foundation
|
||||
import UserNotifications
|
||||
|
||||
/**
|
||||
* Manages recovery of notifications on app launch
|
||||
* Handles cold start, warm start, and termination recovery scenarios
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
* @version 1.0.0
|
||||
*/
|
||||
class DailyNotificationReactivationManager {
|
||||
|
||||
private static let TAG = "DNP-REACTIVATION"
|
||||
private let notificationCenter = UNUserNotificationCenter.current()
|
||||
private let database: DailyNotificationDatabase
|
||||
private let storage: DailyNotificationStorage
|
||||
|
||||
init(database: DailyNotificationDatabase, storage: DailyNotificationStorage) {
|
||||
self.database = database
|
||||
self.storage = storage
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform recovery on app launch
|
||||
* Detects scenario (cold/warm/termination) and handles accordingly
|
||||
*/
|
||||
func performRecovery() async {
|
||||
do {
|
||||
NSLog("\(Self.TAG): Starting app launch recovery")
|
||||
|
||||
// Step 1: Detect scenario
|
||||
let scenario = try await detectScenario()
|
||||
NSLog("\(Self.TAG): Detected scenario: \(scenario)")
|
||||
|
||||
// Step 2: Handle based on scenario
|
||||
switch scenario {
|
||||
case .termination:
|
||||
try await handleTerminationRecovery()
|
||||
case .coldStart:
|
||||
try await handleColdStartRecovery()
|
||||
case .warmStart:
|
||||
try await handleWarmStartRecovery()
|
||||
case .none:
|
||||
NSLog("\(Self.TAG): No recovery needed")
|
||||
}
|
||||
|
||||
NSLog("\(Self.TAG): App launch recovery completed")
|
||||
} catch {
|
||||
NSLog("\(Self.TAG): Error during app launch recovery: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
// ... implementation methods below ...
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Recovery Scenario Detection
|
||||
|
||||
### 4.1 Scenario Detection Algorithm
|
||||
|
||||
**Platform Reference**: [iOS §3.1.1](./alarms/01-platform-capability-reference.md#311-notifications-survive-app-termination) - Notifications survive app termination
|
||||
|
||||
**Detection Logic**:
|
||||
|
||||
```swift
|
||||
enum RecoveryScenario {
|
||||
case none // No recovery needed (first launch or warm resume)
|
||||
case coldStart // App launched after termination, notifications may exist
|
||||
case termination // App terminated, notifications missing vs DB
|
||||
case warmStart // App resumed from background (optimization only)
|
||||
}
|
||||
|
||||
func detectScenario() async throws -> RecoveryScenario {
|
||||
// Step 1: Check if database has schedules
|
||||
let schedules = try database.getEnabledSchedules()
|
||||
if schedules.isEmpty {
|
||||
return .none // First launch
|
||||
}
|
||||
|
||||
// Step 2: Get pending notifications from UNUserNotificationCenter
|
||||
let pendingNotifications = try await notificationCenter.pendingNotificationRequests()
|
||||
|
||||
// Step 3: Compare DB state with notification center state
|
||||
let dbNotificationIds = Set(schedules.flatMap { $0.getScheduledNotificationIds() })
|
||||
let pendingIds = Set(pendingNotifications.map { $0.identifier })
|
||||
|
||||
// Step 4: Determine scenario
|
||||
if pendingIds.isEmpty && !dbNotificationIds.isEmpty {
|
||||
// DB has schedules but no notifications scheduled
|
||||
return .termination
|
||||
} else if !pendingIds.isEmpty && !dbNotificationIds.isEmpty {
|
||||
// Both have data - check if they match
|
||||
if dbNotificationIds != pendingIds {
|
||||
return .coldStart // Mismatch indicates recovery needed
|
||||
} else {
|
||||
return .warmStart // Match indicates warm resume
|
||||
}
|
||||
}
|
||||
|
||||
return .none
|
||||
}
|
||||
```
|
||||
|
||||
**For complete implementation, see**: [Phase 1 directive](./ios-implementation-directive-phase1.md#3-scenario-detection)
|
||||
|
||||
---
|
||||
|
||||
## 5. Missed Notification Detection
|
||||
|
||||
### 5.1 Detection Logic
|
||||
|
||||
**Platform Reference**: [iOS §3.2.1](./alarms/01-platform-capability-reference.md#321-app-code-does-not-run-when-notification-fires) - App code does not run when notification fires
|
||||
|
||||
**iOS Behavior**: When a notification fires, the app code does NOT execute. The notification is displayed, but the app must detect missed notifications on the next app launch.
|
||||
|
||||
**Detection Steps**:
|
||||
|
||||
1. Query database for notifications with `scheduled_time < currentTime`
|
||||
2. Filter for notifications with `delivery_status != 'delivered'`
|
||||
3. Mark as `'missed'` in database
|
||||
4. Record in history table
|
||||
|
||||
**For complete implementation, see**: [Phase 1 directive](./ios-implementation-directive-phase1.md#4-missed-notification-detection)
|
||||
|
||||
---
|
||||
|
||||
## 6. Background Task Registration
|
||||
|
||||
### 6.1 BGTaskScheduler Registration
|
||||
|
||||
**Platform Reference**: [iOS §3.1.3](./alarms/01-platform-capability-reference.md#313-background-tasks-for-prefetching) - Background tasks for prefetching
|
||||
|
||||
**iOS Limitation**: BGTaskScheduler cannot be used for critical scheduling. It's system-controlled and not guaranteed.
|
||||
|
||||
**Use Case**: BGTaskScheduler is used for:
|
||||
- Prefetching content (not critical timing)
|
||||
- Boot recovery (system may defer)
|
||||
- Background maintenance (best effort)
|
||||
|
||||
**Registration Location**: `AppDelegate.swift` or `SceneDelegate.swift`
|
||||
|
||||
**For complete implementation, see**: [Phase 3 directive](./ios-implementation-directive-phase3.md#2-background-task-registration)
|
||||
|
||||
---
|
||||
|
||||
## 7. Testing Strategy
|
||||
|
||||
### 7.1 iOS Testing Tools
|
||||
|
||||
**Simulator Testing**:
|
||||
- `xcrun simctl` - Simulator control
|
||||
- Xcode Instruments - Performance profiling
|
||||
- Console.app - System log viewing
|
||||
|
||||
**Device Testing**:
|
||||
- Xcode Device Console - Real device logs
|
||||
- Settings → Developer → Background App Refresh - Control background execution
|
||||
|
||||
### 7.2 Test Scenarios
|
||||
|
||||
**Phase 1 Tests**:
|
||||
- Cold start recovery
|
||||
- Missed notification detection
|
||||
- Future notification verification
|
||||
|
||||
**Phase 2 Tests**:
|
||||
- App termination detection
|
||||
- Comprehensive recovery
|
||||
- Multiple schedules recovery
|
||||
|
||||
**Phase 3 Tests**:
|
||||
- Background task registration
|
||||
- Boot recovery (simulated)
|
||||
- Background task execution
|
||||
|
||||
**For complete test procedures, see**: [iOS Test Scripts](../test-apps/ios-test-app/test-phase1.sh)
|
||||
|
||||
---
|
||||
|
||||
## 8. Platform-Specific Notes
|
||||
|
||||
### 8.1 Notification Persistence
|
||||
|
||||
**iOS Advantage**: Notifications persist automatically across:
|
||||
- App termination
|
||||
- Device reboot (for calendar/time triggers)
|
||||
|
||||
**App Responsibility**: Must still:
|
||||
- Detect missed notifications on app launch
|
||||
- Reschedule future notifications if needed
|
||||
- Track delivery status in database
|
||||
|
||||
### 8.2 Background Execution Limits
|
||||
|
||||
**iOS Limitation**: Background execution is severely limited:
|
||||
- BGTaskScheduler is system-controlled
|
||||
- Cannot rely on background execution for recovery
|
||||
- Must handle recovery on app launch
|
||||
|
||||
**Workaround**: Use BGTaskScheduler for prefetching only, not for critical scheduling.
|
||||
|
||||
### 8.3 Timing Tolerance
|
||||
|
||||
**iOS Limitation**: Calendar-based notifications have ±180 second tolerance.
|
||||
|
||||
**Impact**: Notifications may fire up to 3 minutes early or late.
|
||||
|
||||
**Mitigation**: Account for tolerance in missed notification detection logic.
|
||||
|
||||
---
|
||||
|
||||
## 9. Next Steps
|
||||
|
||||
1. **Start with Phase 1**: Implement cold start recovery
|
||||
- See [Phase 1 directive](./ios-implementation-directive-phase1.md)
|
||||
- Focus on missed notification detection
|
||||
- Verify future notifications are scheduled
|
||||
|
||||
2. **Proceed to Phase 2**: Add termination detection
|
||||
- See [Phase 2 directive](./ios-implementation-directive-phase2.md)
|
||||
- Implement comprehensive recovery
|
||||
- Handle multiple schedules
|
||||
|
||||
3. **Complete Phase 3**: Background task registration
|
||||
- See [Phase 3 directive](./ios-implementation-directive-phase3.md)
|
||||
- Register BGTaskScheduler handlers
|
||||
- Implement boot recovery
|
||||
|
||||
---
|
||||
|
||||
## 10. References
|
||||
|
||||
- [Platform Capability Reference](./alarms/01-platform-capability-reference.md) - iOS OS-level facts
|
||||
- [Plugin Requirements](./alarms/03-plugin-requirements.md) - Requirements this directive implements
|
||||
- [Android Implementation Directive](./android-implementation-directive.md) - Android equivalent for comparison
|
||||
- [iOS Recovery Scenario Mapping](./ios-recovery-scenario-mapping.md) - Detailed scenario mapping
|
||||
- [iOS Core Data Migration Guide](./ios-core-data-migration.md) - Database migration guide
|
||||
|
||||
---
|
||||
|
||||
**Document Version**: 1.0.0
|
||||
**Last Updated**: 2025-12-08
|
||||
**Next Review**: After Phase 1 implementation
|
||||
|
||||
423
docs/ios-recovery-scenario-mapping.md
Normal file
423
docs/ios-recovery-scenario-mapping.md
Normal file
@@ -0,0 +1,423 @@
|
||||
# iOS Recovery Scenario Mapping: Android → iOS Equivalents
|
||||
|
||||
**Author**: Matthew Raymer
|
||||
**Date**: 2025-12-08
|
||||
**Status**: 🎯 **ACTIVE** - Recovery Scenario Mapping Reference
|
||||
**Version**: 1.0.0
|
||||
**Last Synced With Plugin Version**: v1.1.0
|
||||
|
||||
## Purpose
|
||||
|
||||
This document maps Android recovery scenarios to their iOS equivalents, providing a clear translation guide for implementing iOS recovery logic based on Android patterns.
|
||||
|
||||
**Reference**:
|
||||
- [Android Implementation Directive](./android-implementation-directive.md) - Android scenarios
|
||||
- [iOS Implementation Directive](./ios-implementation-directive.md) - iOS scenarios
|
||||
- [Platform Capability Reference](./alarms/01-platform-capability-reference.md) - OS-level facts
|
||||
|
||||
---
|
||||
|
||||
## 1. Scenario Mapping Overview
|
||||
|
||||
### 1.1 Direct Mappings
|
||||
|
||||
| Android Scenario | iOS Equivalent | Detection Method | Recovery Action |
|
||||
| ---------------- | -------------- | ---------------- | --------------- |
|
||||
| `COLD_START` | App Launch After Termination | Compare UNUserNotificationCenter vs DB | Detect missed, verify future |
|
||||
| `FORCE_STOP` | App Terminated by System | DB has schedules, no notifications | Full recovery of all schedules |
|
||||
| `BOOT` | Device Reboot | BGTaskScheduler registration | Reschedule all notifications |
|
||||
| `WARM_START` | App Resume (Foreground) | Notifications match DB state | No recovery needed (optimization) |
|
||||
| `NONE` | First Launch / No Recovery | Empty database | No action needed |
|
||||
|
||||
### 1.2 Key Differences
|
||||
|
||||
**iOS Advantages**:
|
||||
- ✅ Notifications persist across termination (OS-guaranteed)
|
||||
- ✅ Notifications persist across reboot (OS-guaranteed)
|
||||
- ❌ No user-facing "force stop" equivalent
|
||||
|
||||
**iOS Challenges**:
|
||||
- ❌ App code does NOT run when notification fires
|
||||
- ❌ Must detect missed notifications on app launch
|
||||
- ❌ Background execution severely limited
|
||||
|
||||
---
|
||||
|
||||
## 2. Detailed Scenario Mappings
|
||||
|
||||
### 2.1 COLD_START → App Launch After Termination
|
||||
|
||||
**Android Definition**:
|
||||
- Process killed, alarms may or may not exist
|
||||
- Database still populated
|
||||
- Alarms may have been cleared by OS
|
||||
|
||||
**iOS Equivalent**:
|
||||
- App terminated by system or user
|
||||
- Notifications may still exist (OS-guaranteed persistence)
|
||||
- Database still populated
|
||||
- Need to verify notification state matches database
|
||||
|
||||
**Detection Logic**:
|
||||
|
||||
**Android**:
|
||||
```kotlin
|
||||
// Check if alarms exist in AlarmManager
|
||||
val alarmsExist = alarmManager.hasAlarm(pendingIntent)
|
||||
if (alarmsExist && dbHasSchedules) {
|
||||
return COLD_START
|
||||
}
|
||||
```
|
||||
|
||||
**iOS**:
|
||||
```swift
|
||||
// Check if notifications exist in UNUserNotificationCenter
|
||||
let pendingNotifications = try await notificationCenter.pendingNotificationRequests()
|
||||
let dbSchedules = try database.getEnabledSchedules()
|
||||
|
||||
if !pendingNotifications.isEmpty && !dbSchedules.isEmpty {
|
||||
// Compare notification IDs with DB state
|
||||
let dbIds = Set(dbSchedules.flatMap { $0.getScheduledNotificationIds() })
|
||||
let pendingIds = Set(pendingNotifications.map { $0.identifier })
|
||||
|
||||
if dbIds != pendingIds {
|
||||
return .coldStart // Mismatch indicates recovery needed
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Recovery Actions**:
|
||||
1. Detect missed notifications (scheduled_time < now, not delivered)
|
||||
2. Mark missed notifications in database
|
||||
3. Verify future notifications are scheduled
|
||||
4. Reschedule missing future notifications
|
||||
|
||||
**Platform Reference**: [iOS §3.1.1](./alarms/01-platform-capability-reference.md#311-notifications-survive-app-termination)
|
||||
|
||||
---
|
||||
|
||||
### 2.2 FORCE_STOP → App Terminated by System
|
||||
|
||||
**Android Definition**:
|
||||
- User force-stopped app via Settings
|
||||
- All alarms cleared
|
||||
- Database still populated
|
||||
- Boot receiver blocked until user launches app
|
||||
|
||||
**iOS Equivalent**:
|
||||
- App terminated by system (low memory, etc.)
|
||||
- Notifications may be missing (system cleared them)
|
||||
- Database still populated
|
||||
- No user-facing force stop equivalent
|
||||
|
||||
**Key Difference**: iOS doesn't have a user-facing "force stop" option. System termination is the closest equivalent.
|
||||
|
||||
**Detection Logic**:
|
||||
|
||||
**Android**:
|
||||
```kotlin
|
||||
// Check if alarms exist
|
||||
val alarmsExist = alarmManager.hasAlarm(pendingIntent)
|
||||
if (!alarmsExist && dbHasSchedules && !isBootRecent) {
|
||||
return FORCE_STOP
|
||||
}
|
||||
```
|
||||
|
||||
**iOS**:
|
||||
```swift
|
||||
// Check if notifications exist
|
||||
let pendingNotifications = try await notificationCenter.pendingNotificationRequests()
|
||||
let dbSchedules = try database.getEnabledSchedules()
|
||||
|
||||
if pendingNotifications.isEmpty && !dbSchedules.isEmpty {
|
||||
// DB has schedules but no notifications scheduled
|
||||
return .termination
|
||||
}
|
||||
```
|
||||
|
||||
**Recovery Actions**:
|
||||
1. Detect all missed notifications
|
||||
2. Mark all missed notifications in database
|
||||
3. Reschedule all future notifications
|
||||
4. Reschedule all fetch schedules (if applicable)
|
||||
|
||||
**Platform Reference**: [iOS §3.2.1](./alarms/01-platform-capability-reference.md#321-app-code-does-not-run-when-notification-fires)
|
||||
|
||||
---
|
||||
|
||||
### 2.3 BOOT → Device Reboot
|
||||
|
||||
**Android Definition**:
|
||||
- Device rebooted
|
||||
- All alarms wiped (OS behavior)
|
||||
- Database still populated
|
||||
- Boot receiver executes after boot completes
|
||||
|
||||
**iOS Equivalent**:
|
||||
- Device rebooted
|
||||
- Notifications persist automatically (OS-guaranteed)
|
||||
- Database still populated
|
||||
- BGTaskScheduler may execute (system-controlled)
|
||||
|
||||
**Key Difference**: iOS automatically persists notifications across reboot. Android requires manual rescheduling.
|
||||
|
||||
**Detection Logic**:
|
||||
|
||||
**Android**:
|
||||
```kotlin
|
||||
// Check boot flag (set by BootReceiver)
|
||||
val bootFlag = sharedPreferences.getLong("last_boot_time", 0)
|
||||
val currentTime = System.currentTimeMillis()
|
||||
if (bootFlag > 0 && (currentTime - bootFlag) < 60000) {
|
||||
return BOOT
|
||||
}
|
||||
```
|
||||
|
||||
**iOS**:
|
||||
```swift
|
||||
// BGTaskScheduler registration handles boot
|
||||
// Check if this is a boot-triggered background task
|
||||
if isBootBackgroundTask {
|
||||
return .boot
|
||||
}
|
||||
|
||||
// Or detect on app launch after reboot
|
||||
let lastLaunchTime = UserDefaults.standard.double(forKey: "last_launch_time")
|
||||
let bootTime = ProcessInfo.processInfo.systemUptime
|
||||
if lastLaunchTime > 0 && bootTime < 60 {
|
||||
return .boot
|
||||
}
|
||||
```
|
||||
|
||||
**Recovery Actions**:
|
||||
1. Verify notifications still exist (iOS usually handles this)
|
||||
2. Detect any missed notifications during reboot window
|
||||
3. Reschedule any missing notifications
|
||||
4. Update next run times for repeating schedules
|
||||
|
||||
**Platform Reference**: [iOS §3.1.2](./alarms/01-platform-capability-reference.md#312-notifications-persist-across-device-reboot)
|
||||
|
||||
---
|
||||
|
||||
### 2.4 WARM_START → App Resume (Foreground)
|
||||
|
||||
**Android Definition**:
|
||||
- App resumed from background
|
||||
- Alarms still exist
|
||||
- Database matches alarm state
|
||||
- No recovery needed (optimization)
|
||||
|
||||
**iOS Equivalent**:
|
||||
- App resumed from background
|
||||
- Notifications still exist
|
||||
- Database matches notification state
|
||||
- No recovery needed (optimization)
|
||||
|
||||
**Detection Logic**:
|
||||
|
||||
**Android**:
|
||||
```kotlin
|
||||
// Check if alarms exist and match DB
|
||||
val alarmsExist = alarmManager.hasAlarm(pendingIntent)
|
||||
if (alarmsExist && dbMatchesAlarms) {
|
||||
return WARM_START
|
||||
}
|
||||
```
|
||||
|
||||
**iOS**:
|
||||
```swift
|
||||
// Check if notifications exist and match DB
|
||||
let pendingNotifications = try await notificationCenter.pendingNotificationRequests()
|
||||
let dbSchedules = try database.getEnabledSchedules()
|
||||
|
||||
let dbIds = Set(dbSchedules.flatMap { $0.getScheduledNotificationIds() })
|
||||
let pendingIds = Set(pendingNotifications.map { $0.identifier })
|
||||
|
||||
if dbIds == pendingIds {
|
||||
return .warmStart // Match indicates warm resume
|
||||
}
|
||||
```
|
||||
|
||||
**Recovery Actions**:
|
||||
- None (optimization only)
|
||||
- May perform lightweight verification
|
||||
- May update metrics
|
||||
|
||||
---
|
||||
|
||||
### 2.5 NONE → First Launch / No Recovery
|
||||
|
||||
**Android Definition**:
|
||||
- First app launch
|
||||
- Empty database
|
||||
- No schedules configured
|
||||
- No recovery needed
|
||||
|
||||
**iOS Equivalent**:
|
||||
- First app launch
|
||||
- Empty database
|
||||
- No schedules configured
|
||||
- No recovery needed
|
||||
|
||||
**Detection Logic**:
|
||||
|
||||
**Android**:
|
||||
```kotlin
|
||||
// Check if database is empty
|
||||
val schedules = database.scheduleDao().getEnabled()
|
||||
if (schedules.isEmpty()) {
|
||||
return NONE
|
||||
}
|
||||
```
|
||||
|
||||
**iOS**:
|
||||
```swift
|
||||
// Check if database is empty
|
||||
let schedules = try database.getEnabledSchedules()
|
||||
if schedules.isEmpty {
|
||||
return .none
|
||||
}
|
||||
```
|
||||
|
||||
**Recovery Actions**:
|
||||
- None
|
||||
|
||||
---
|
||||
|
||||
## 3. Recovery Action Mapping
|
||||
|
||||
### 3.1 Missed Notification Detection
|
||||
|
||||
**Android**:
|
||||
- Query AlarmManager for past alarms
|
||||
- Check database for undelivered notifications
|
||||
- Mark as missed in database
|
||||
|
||||
**iOS**:
|
||||
- Query database for past scheduled notifications
|
||||
- Check delivery status
|
||||
- Mark as missed in database
|
||||
|
||||
**Key Difference**: iOS cannot query past notifications from UNUserNotificationCenter. Must rely on database state.
|
||||
|
||||
### 3.2 Future Notification Verification
|
||||
|
||||
**Android**:
|
||||
- Query AlarmManager for future alarms
|
||||
- Compare with database schedules
|
||||
- Reschedule missing alarms
|
||||
|
||||
**iOS**:
|
||||
- Query UNUserNotificationCenter for pending notifications
|
||||
- Compare with database schedules
|
||||
- Reschedule missing notifications
|
||||
|
||||
**Key Difference**: iOS uses UNUserNotificationCenter instead of AlarmManager.
|
||||
|
||||
### 3.3 Full Recovery
|
||||
|
||||
**Android**:
|
||||
- Reschedule all notify schedules
|
||||
- Reschedule all fetch schedules (WorkManager)
|
||||
- Mark past notifications as missed
|
||||
|
||||
**iOS**:
|
||||
- Reschedule all notify schedules
|
||||
- Reschedule all fetch schedules (BGTaskScheduler)
|
||||
- Mark past notifications as missed
|
||||
|
||||
**Key Difference**: iOS uses BGTaskScheduler instead of WorkManager.
|
||||
|
||||
---
|
||||
|
||||
## 4. Implementation Checklist
|
||||
|
||||
### 4.1 Phase 1: Cold Start Recovery
|
||||
|
||||
- [ ] Implement scenario detection (cold start)
|
||||
- [ ] Implement missed notification detection
|
||||
- [ ] Implement future notification verification
|
||||
- [ ] Test cold start recovery
|
||||
|
||||
### 4.2 Phase 2: Termination Detection
|
||||
|
||||
- [ ] Implement termination detection
|
||||
- [ ] Implement full recovery logic
|
||||
- [ ] Test termination recovery
|
||||
|
||||
### 4.3 Phase 3: Boot Recovery
|
||||
|
||||
- [ ] Implement BGTaskScheduler registration
|
||||
- [ ] Implement boot detection
|
||||
- [ ] Test boot recovery
|
||||
|
||||
---
|
||||
|
||||
## 5. Platform-Specific Notes
|
||||
|
||||
### 5.1 iOS Advantages
|
||||
|
||||
1. **Notification Persistence**: iOS automatically persists notifications across termination and reboot
|
||||
2. **No Force Stop**: iOS doesn't have user-facing force stop, reducing complexity
|
||||
3. **Simplified Recovery**: Less recovery needed due to OS persistence
|
||||
|
||||
### 5.2 iOS Challenges
|
||||
|
||||
1. **No Code Execution on Fire**: App code doesn't run when notification fires
|
||||
2. **Background Limits**: Severely limited background execution
|
||||
3. **Timing Tolerance**: ±180 second tolerance for calendar triggers
|
||||
|
||||
### 5.3 Android Advantages
|
||||
|
||||
1. **Code Execution on Fire**: PendingIntent can execute code when alarm fires
|
||||
2. **WorkManager**: More reliable background execution
|
||||
3. **Exact Timing**: Can achieve exact timing with permission
|
||||
|
||||
### 5.4 Android Challenges
|
||||
|
||||
1. **No Persistence**: Alarms don't persist across reboot
|
||||
2. **Force Stop**: Hard kill that cannot be bypassed
|
||||
3. **Boot Recovery**: Must implement boot receiver
|
||||
|
||||
---
|
||||
|
||||
## 6. Testing Strategy
|
||||
|
||||
### 6.1 Scenario Testing
|
||||
|
||||
**Cold Start**:
|
||||
1. Terminate app (swipe away)
|
||||
2. Wait for notification time to pass
|
||||
3. Launch app
|
||||
4. Verify missed notification detection
|
||||
5. Verify future notifications rescheduled
|
||||
|
||||
**Termination**:
|
||||
1. Schedule notifications
|
||||
2. Terminate app
|
||||
3. Clear notifications (simulate system clearing)
|
||||
4. Launch app
|
||||
5. Verify full recovery
|
||||
|
||||
**Boot**:
|
||||
1. Schedule notifications
|
||||
2. Reboot device (or simulate)
|
||||
3. Launch app
|
||||
4. Verify notifications still exist
|
||||
5. Verify any missed notifications detected
|
||||
|
||||
---
|
||||
|
||||
## 7. References
|
||||
|
||||
- [Android Implementation Directive](./android-implementation-directive.md) - Android scenarios
|
||||
- [iOS Implementation Directive](./ios-implementation-directive.md) - iOS scenarios
|
||||
- [Platform Capability Reference](./alarms/01-platform-capability-reference.md) - OS-level facts
|
||||
- [Plugin Requirements](./alarms/03-plugin-requirements.md) - Requirements
|
||||
|
||||
---
|
||||
|
||||
**Document Version**: 1.0.0
|
||||
**Last Updated**: 2025-12-08
|
||||
**Next Review**: After Phase 1 implementation
|
||||
|
||||
574
docs/ios-troubleshooting-guide.md
Normal file
574
docs/ios-troubleshooting-guide.md
Normal file
@@ -0,0 +1,574 @@
|
||||
# iOS Troubleshooting Guide
|
||||
|
||||
**Author**: Matthew Raymer
|
||||
**Date**: 2025-12-08
|
||||
**Status**: 🎯 **ACTIVE** - iOS Troubleshooting Reference
|
||||
**Version**: 1.0.0
|
||||
|
||||
## Purpose
|
||||
|
||||
This guide provides solutions to common iOS-specific issues when using the Daily Notification Plugin. It covers debugging techniques, common problems, and their solutions.
|
||||
|
||||
**Reference**:
|
||||
- [iOS Implementation Directive](./ios-implementation-directive.md) - Implementation details
|
||||
- [Platform Capability Reference](./alarms/01-platform-capability-reference.md) - iOS OS behaviors
|
||||
- [iOS Logging Guide](../doc/test-app-ios/IOS_LOGGING_GUIDE.md) - How to view logs
|
||||
|
||||
---
|
||||
|
||||
## 1. Common Issues
|
||||
|
||||
### 1.1 Notifications Not Firing
|
||||
|
||||
**Symptoms:**
|
||||
- Notifications scheduled but don't appear
|
||||
- No notification at scheduled time
|
||||
- Notifications work in simulator but not on device
|
||||
|
||||
**Diagnosis Steps:**
|
||||
|
||||
1. **Check Notification Permission:**
|
||||
```swift
|
||||
// In app code
|
||||
UNUserNotificationCenter.current().getNotificationSettings { settings in
|
||||
print("Authorization status: \(settings.authorizationStatus)")
|
||||
}
|
||||
```
|
||||
|
||||
Or check in Xcode Console:
|
||||
```
|
||||
DNP-PLUGIN: Notification permission status: authorized
|
||||
```
|
||||
|
||||
2. **Check Pending Notifications:**
|
||||
```swift
|
||||
UNUserNotificationCenter.current().getPendingNotificationRequests { requests in
|
||||
print("Pending notifications: \(requests.count)")
|
||||
}
|
||||
```
|
||||
|
||||
3. **Check Background App Refresh:**
|
||||
- Settings → [Your App] → Background App Refresh
|
||||
- Must be enabled for background tasks
|
||||
|
||||
4. **Check Notification Limit:**
|
||||
- iOS limits to 64 pending notifications
|
||||
- Check if limit is exceeded
|
||||
|
||||
**Solutions:**
|
||||
|
||||
- **Permission Denied:**
|
||||
- Request permission: `DailyNotification.requestNotificationPermission()`
|
||||
- Guide user to Settings → [Your App] → Notifications
|
||||
|
||||
- **Background App Refresh Disabled:**
|
||||
- Guide user to enable: Settings → [Your App] → Background App Refresh
|
||||
- Or use: `DailyNotification.openBackgroundAppRefreshSettings()`
|
||||
|
||||
- **Notification Limit Exceeded:**
|
||||
- Reduce number of scheduled notifications
|
||||
- Implement notification cleanup logic
|
||||
- Check rolling window implementation
|
||||
|
||||
- **Simulator vs Device:**
|
||||
- Simulator may not fire notifications reliably
|
||||
- Test on physical device for accurate behavior
|
||||
|
||||
---
|
||||
|
||||
### 1.2 Background Tasks Not Executing
|
||||
|
||||
**Symptoms:**
|
||||
- Prefetch tasks don't run
|
||||
- Background fetch never executes
|
||||
- BGTaskScheduler tasks not firing
|
||||
|
||||
**Diagnosis Steps:**
|
||||
|
||||
1. **Check BGTaskScheduler Registration:**
|
||||
```swift
|
||||
// Verify registration in AppDelegate
|
||||
BGTaskScheduler.shared.register(forTaskWithIdentifier: "com.timesafari.dailynotification.fetch", using: nil) { task in
|
||||
// Handler should be registered
|
||||
}
|
||||
```
|
||||
|
||||
2. **Check Info.plist:**
|
||||
```xml
|
||||
<key>BGTaskSchedulerPermittedIdentifiers</key>
|
||||
<array>
|
||||
<string>com.timesafari.dailynotification.fetch</string>
|
||||
</array>
|
||||
```
|
||||
|
||||
3. **Check Background App Refresh:**
|
||||
- Must be enabled in Settings
|
||||
- System-controlled timing (not guaranteed)
|
||||
|
||||
4. **Check Logs:**
|
||||
```
|
||||
DNP-FETCH-START: Background fetch task started
|
||||
```
|
||||
|
||||
**Solutions:**
|
||||
|
||||
- **Not Registered:**
|
||||
- Verify registration in `AppDelegate.application(_:didFinishLaunchingWithOptions:)`
|
||||
- Check Info.plist has correct identifiers
|
||||
|
||||
- **Background App Refresh Disabled:**
|
||||
- User must enable in Settings
|
||||
- Cannot be programmatically enabled
|
||||
|
||||
- **System Not Executing:**
|
||||
- BGTaskScheduler is system-controlled
|
||||
- Execution timing is not guaranteed
|
||||
- System may defer or skip tasks
|
||||
- Use for prefetching only, not critical scheduling
|
||||
|
||||
- **Simulator Limitations:**
|
||||
- Background tasks may not execute in simulator
|
||||
- Test on physical device
|
||||
|
||||
---
|
||||
|
||||
### 1.3 Notifications Disappear After App Termination
|
||||
|
||||
**Symptoms:**
|
||||
- Notifications scheduled but disappear when app is terminated
|
||||
- Notifications don't persist across app restarts
|
||||
|
||||
**Diagnosis:**
|
||||
|
||||
**This should NOT happen on iOS** - notifications persist automatically (OS-guaranteed).
|
||||
|
||||
**If it happens, check:**
|
||||
|
||||
1. **Notification Trigger Type:**
|
||||
- Calendar/time triggers persist ✅
|
||||
- Location triggers do NOT persist ❌
|
||||
|
||||
2. **Notification Content:**
|
||||
- Ensure notification has valid content
|
||||
- Check for invalid trigger dates
|
||||
|
||||
3. **System Storage:**
|
||||
- iOS may clear notifications if device storage is full
|
||||
- Check available storage
|
||||
|
||||
**Solutions:**
|
||||
|
||||
- **Use Calendar Triggers:**
|
||||
```swift
|
||||
// ✅ Persists across reboot
|
||||
let trigger = UNCalendarNotificationTrigger(dateMatching: components, repeats: true)
|
||||
|
||||
// ❌ Does NOT persist
|
||||
let trigger = UNLocationNotificationTrigger(region: region, repeats: true)
|
||||
```
|
||||
|
||||
- **Check Device Storage:**
|
||||
- Free up storage if device is full
|
||||
- iOS may clear notifications when storage is critical
|
||||
|
||||
---
|
||||
|
||||
### 1.4 Recovery Not Working
|
||||
|
||||
**Symptoms:**
|
||||
- Missed notifications not detected on app launch
|
||||
- Future notifications not verified/rescheduled
|
||||
- No recovery activity in logs
|
||||
|
||||
**Diagnosis:**
|
||||
|
||||
**Recovery features are NOT yet implemented** (as of 2025-12-08).
|
||||
|
||||
**Expected Behavior (Once Implemented):**
|
||||
|
||||
1. **Check Logs for Recovery:**
|
||||
```
|
||||
DNP-REACTIVATION: Starting app launch recovery
|
||||
DNP-REACTIVATION: Detected scenario: coldStart
|
||||
DNP-REACTIVATION: Missed notifications detected: 2
|
||||
DNP-REACTIVATION: Future notifications verified: 1
|
||||
```
|
||||
|
||||
2. **Verify Recovery Logic:**
|
||||
- Should run in `DailyNotificationPlugin.load()`
|
||||
- Should detect missed notifications
|
||||
- Should verify future notifications
|
||||
|
||||
**Solutions:**
|
||||
|
||||
- **Implementation Pending:**
|
||||
- See [iOS Implementation Directive Phase 1](./ios-implementation-directive-phase1.md)
|
||||
- Recovery features need to be implemented
|
||||
|
||||
- **Manual Workaround:**
|
||||
- Check pending notifications manually
|
||||
- Reschedule if needed
|
||||
|
||||
---
|
||||
|
||||
### 1.5 Database/Storage Issues
|
||||
|
||||
**Symptoms:**
|
||||
- Database errors in logs
|
||||
- Data not persisting
|
||||
- Core Data errors
|
||||
|
||||
**Diagnosis Steps:**
|
||||
|
||||
1. **Check Database Path:**
|
||||
```swift
|
||||
let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
||||
let dbPath = documentsPath.appendingPathComponent("daily_notifications.db")
|
||||
print("Database path: \(dbPath.path)")
|
||||
```
|
||||
|
||||
2. **Check Core Data Stack:**
|
||||
- Verify `NSPersistentContainer` initialization
|
||||
- Check for migration errors
|
||||
|
||||
3. **Check Logs:**
|
||||
```
|
||||
DNP-STORAGE: Database opened successfully
|
||||
DNP-STORAGE: Error opening database: [error]
|
||||
```
|
||||
|
||||
**Solutions:**
|
||||
|
||||
- **Database Path Issues:**
|
||||
- Verify app has write permissions
|
||||
- Check Documents directory is accessible
|
||||
- Ensure path is correct
|
||||
|
||||
- **Core Data Errors:**
|
||||
- Check Core Data model version
|
||||
- Verify migration policies
|
||||
- Check for schema mismatches
|
||||
|
||||
- **Storage Full:**
|
||||
- Free up device storage
|
||||
- iOS may clear app data if storage is critical
|
||||
|
||||
---
|
||||
|
||||
### 1.6 Permission Issues
|
||||
|
||||
**Symptoms:**
|
||||
- Permission requests fail
|
||||
- Permission status incorrect
|
||||
- Cannot schedule notifications
|
||||
|
||||
**Diagnosis:**
|
||||
|
||||
1. **Check Current Status:**
|
||||
```swift
|
||||
UNUserNotificationCenter.current().getNotificationSettings { settings in
|
||||
switch settings.authorizationStatus {
|
||||
case .authorized: // ✅ Can schedule
|
||||
case .denied: // ❌ User denied
|
||||
case .notDetermined: // ⚠️ Not requested yet
|
||||
case .provisional: // ⚠️ Provisional (iOS 12+)
|
||||
@unknown default: break
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. **Check Logs:**
|
||||
```
|
||||
DNP-PLUGIN: Notification permission status: denied
|
||||
```
|
||||
|
||||
**Solutions:**
|
||||
|
||||
- **Permission Denied:**
|
||||
- Cannot request again programmatically
|
||||
- Guide user to Settings → [Your App] → Notifications
|
||||
- Use: `DailyNotification.openNotificationSettings()`
|
||||
|
||||
- **Not Determined:**
|
||||
- Request permission: `DailyNotification.requestNotificationPermission()`
|
||||
- Show explanation before requesting
|
||||
|
||||
- **Provisional:**
|
||||
- iOS 12+ feature
|
||||
- Notifications delivered quietly
|
||||
- User can upgrade to full permission
|
||||
|
||||
---
|
||||
|
||||
## 2. Debugging Techniques
|
||||
|
||||
### 2.1 Viewing Logs
|
||||
|
||||
**Xcode Console (Recommended):**
|
||||
1. Run app in Xcode (Cmd+R)
|
||||
2. Open Debug Area (Cmd+Shift+Y)
|
||||
3. Filter by: `DNP-` or `DailyNotification`
|
||||
|
||||
**Console.app:**
|
||||
1. Open Console.app
|
||||
2. Select device/simulator
|
||||
3. Filter by process: `ios-test-app`
|
||||
|
||||
**Command Line:**
|
||||
```bash
|
||||
# Simulator logs
|
||||
xcrun simctl spawn <device-id> log stream --level=debug --predicate 'processImagePath contains "ios-test-app"'
|
||||
|
||||
# Device logs (requires device connected)
|
||||
xcrun devicectl device process monitor --device <device-id> --filter "ios-test-app"
|
||||
```
|
||||
|
||||
### 2.2 Checking Pending Notifications
|
||||
|
||||
**Via Plugin Method:**
|
||||
```typescript
|
||||
const result = await DailyNotification.getPendingNotifications();
|
||||
console.log(`Pending: ${result.count}`);
|
||||
```
|
||||
|
||||
**Via Swift Code:**
|
||||
```swift
|
||||
UNUserNotificationCenter.current().getPendingNotificationRequests { requests in
|
||||
print("Pending notifications: \(requests.count)")
|
||||
for request in requests {
|
||||
print(" - \(request.identifier): \(request.content.title)")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.3 Checking Background Task Status
|
||||
|
||||
**Via Plugin Method:**
|
||||
```typescript
|
||||
const status = await DailyNotification.getBackgroundTaskStatus();
|
||||
console.log(`Fetch task registered: ${status.fetchTaskRegistered}`);
|
||||
console.log(`Background refresh enabled: ${status.backgroundRefreshEnabled}`);
|
||||
```
|
||||
|
||||
**Via Swift Code:**
|
||||
```swift
|
||||
// Check registration
|
||||
let registered = BGTaskScheduler.shared.registeredTaskIdentifiers
|
||||
print("Registered tasks: \(registered)")
|
||||
|
||||
// Check Background App Refresh (requires entitlement)
|
||||
// Cannot check programmatically - must guide user to Settings
|
||||
```
|
||||
|
||||
### 2.4 Simulating Background Tasks (Simulator Only)
|
||||
|
||||
**LLDB Command in Xcode:**
|
||||
```lldb
|
||||
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"com.timesafari.dailynotification.fetch"]
|
||||
```
|
||||
|
||||
**Note:** This only works in simulator, not on physical devices.
|
||||
|
||||
---
|
||||
|
||||
## 3. Platform-Specific Considerations
|
||||
|
||||
### 3.1 Simulator vs Device
|
||||
|
||||
**Simulator Limitations:**
|
||||
- Background tasks may not execute reliably
|
||||
- Notifications may not fire at exact time
|
||||
- Some features require physical device
|
||||
|
||||
**Device Testing:**
|
||||
- More accurate behavior
|
||||
- Background tasks execute (system-controlled)
|
||||
- Notifications fire reliably
|
||||
|
||||
**Recommendation:** Test critical features on physical device.
|
||||
|
||||
### 3.2 iOS Version Differences
|
||||
|
||||
**iOS 12+:**
|
||||
- Provisional notification authorization
|
||||
- Background task improvements
|
||||
|
||||
**iOS 13+:**
|
||||
- State actor support (concurrency)
|
||||
- Improved background execution
|
||||
|
||||
**iOS 14+:**
|
||||
- Notification interruption levels
|
||||
- Focus modes (may affect notifications)
|
||||
|
||||
**iOS 15+:**
|
||||
- Notification summary
|
||||
- Focus mode integration
|
||||
|
||||
### 3.3 Background Execution Limits
|
||||
|
||||
**iOS Constraints:**
|
||||
- BGTaskScheduler is system-controlled
|
||||
- Execution timing not guaranteed
|
||||
- Minimum intervals between tasks (hours)
|
||||
- Tasks may be deferred or skipped
|
||||
|
||||
**Workaround:**
|
||||
- Use BGTaskScheduler for prefetching only
|
||||
- Don't rely on it for critical scheduling
|
||||
- Use UNUserNotificationCenter for notifications (more reliable)
|
||||
|
||||
---
|
||||
|
||||
## 4. Error Codes
|
||||
|
||||
### 4.1 Common Error Codes
|
||||
|
||||
| Error Code | Description | Solution |
|
||||
| ---------- | ----------- | -------- |
|
||||
| `NOTIFICATION_PERMISSION_DENIED` | User denied notification permission | Guide user to Settings |
|
||||
| `BACKGROUND_REFRESH_DISABLED` | Background App Refresh disabled | Guide user to enable in Settings |
|
||||
| `PENDING_NOTIFICATION_LIMIT_EXCEEDED` | Exceeded 64 notification limit | Reduce scheduled notifications |
|
||||
| `BG_TASK_NOT_REGISTERED` | Background task not registered | Check Info.plist and AppDelegate |
|
||||
| `BG_TASK_EXECUTION_FAILED` | Background task execution failed | Check logs for specific error |
|
||||
|
||||
### 4.2 Checking Error Details
|
||||
|
||||
**Via Logs:**
|
||||
```
|
||||
DNP-ERROR: [Error Code] [Error Message]
|
||||
DNP-ERROR: NOTIFICATION_PERMISSION_DENIED: User denied notification permission
|
||||
```
|
||||
|
||||
**Via Plugin:**
|
||||
```typescript
|
||||
try {
|
||||
await DailyNotification.scheduleDailyNotification({...});
|
||||
} catch (error) {
|
||||
console.error('Error code:', error.code);
|
||||
console.error('Error message:', error.message);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Performance Issues
|
||||
|
||||
### 5.1 Slow Notification Scheduling
|
||||
|
||||
**Symptoms:**
|
||||
- Scheduling takes too long
|
||||
- App freezes during scheduling
|
||||
|
||||
**Solutions:**
|
||||
- Schedule notifications asynchronously
|
||||
- Batch operations when possible
|
||||
- Use background queue for heavy operations
|
||||
|
||||
### 5.2 High Memory Usage
|
||||
|
||||
**Symptoms:**
|
||||
- App memory usage high
|
||||
- Memory warnings in logs
|
||||
|
||||
**Solutions:**
|
||||
- Implement notification cleanup
|
||||
- Limit cached notifications
|
||||
- Use efficient data structures
|
||||
|
||||
### 5.3 Battery Drain
|
||||
|
||||
**Symptoms:**
|
||||
- Battery drains quickly
|
||||
- Background activity high
|
||||
|
||||
**Solutions:**
|
||||
- Limit background task frequency
|
||||
- Optimize prefetch operations
|
||||
- Use efficient scheduling algorithms
|
||||
|
||||
---
|
||||
|
||||
## 6. Getting Help
|
||||
|
||||
### 6.1 Log Collection
|
||||
|
||||
**Collect Logs:**
|
||||
1. Reproduce the issue
|
||||
2. Collect logs from Xcode Console or Console.app
|
||||
3. Filter by `DNP-` prefix
|
||||
4. Include relevant error messages
|
||||
|
||||
**Log Format:**
|
||||
```
|
||||
DNP-PLUGIN: [Message]
|
||||
DNP-ERROR: [Error Code] [Error Message]
|
||||
DNP-REACTIVATION: [Recovery Activity]
|
||||
```
|
||||
|
||||
### 6.2 Issue Reporting
|
||||
|
||||
**Include:**
|
||||
- iOS version
|
||||
- Device model (or simulator)
|
||||
- Plugin version
|
||||
- Steps to reproduce
|
||||
- Relevant logs
|
||||
- Expected vs actual behavior
|
||||
|
||||
### 6.3 Documentation References
|
||||
|
||||
- [iOS Implementation Directive](./ios-implementation-directive.md)
|
||||
- [Platform Capability Reference](./alarms/01-platform-capability-reference.md)
|
||||
- [API Reference](../API.md)
|
||||
- [iOS Logging Guide](../doc/test-app-ios/IOS_LOGGING_GUIDE.md)
|
||||
|
||||
---
|
||||
|
||||
## 7. Quick Reference
|
||||
|
||||
### 7.1 Common Commands
|
||||
|
||||
**Check Pending Notifications:**
|
||||
```bash
|
||||
# Via plugin method (recommended)
|
||||
# Or check logs for scheduling activity
|
||||
```
|
||||
|
||||
**View Logs:**
|
||||
```bash
|
||||
# Xcode Console (Cmd+Shift+Y)
|
||||
# Filter: DNP-
|
||||
|
||||
# Console.app
|
||||
# Filter: ios-test-app
|
||||
```
|
||||
|
||||
**Check Permissions:**
|
||||
```typescript
|
||||
const status = await DailyNotification.getNotificationPermissionStatus();
|
||||
```
|
||||
|
||||
**Open Settings:**
|
||||
```typescript
|
||||
await DailyNotification.openNotificationSettings();
|
||||
await DailyNotification.openBackgroundAppRefreshSettings();
|
||||
```
|
||||
|
||||
### 7.2 Checklist
|
||||
|
||||
**Before Reporting Issue:**
|
||||
- [ ] Checked notification permissions
|
||||
- [ ] Verified Background App Refresh is enabled
|
||||
- [ ] Checked pending notification count (< 64)
|
||||
- [ ] Reviewed logs for errors
|
||||
- [ ] Tested on physical device (not just simulator)
|
||||
- [ ] Verified iOS version compatibility
|
||||
- [ ] Checked device storage availability
|
||||
|
||||
---
|
||||
|
||||
**Document Version**: 1.0.0
|
||||
**Last Updated**: 2025-12-08
|
||||
**Next Review**: After Phase 1 implementation
|
||||
|
||||
436
test-apps/ios-test-app/test-phase1.sh
Executable file
436
test-apps/ios-test-app/test-phase1.sh
Executable file
@@ -0,0 +1,436 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Phase 1 Testing Script - iOS Interactive Test Runner
|
||||
# Guides through all Phase 1 tests with clear prompts for UI interaction
|
||||
# Adapted from Android test-phase1.sh for iOS testing
|
||||
|
||||
set -e # Exit on error
|
||||
|
||||
# Source shared library (if exists)
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
if [ -f "${SCRIPT_DIR}/ios-test-lib.sh" ]; then
|
||||
source "${SCRIPT_DIR}/ios-test-lib.sh"
|
||||
fi
|
||||
|
||||
# Phase 1 specific configuration
|
||||
APP_BUNDLE_ID="com.timesafari.ios-test-app"
|
||||
SIMULATOR_DEVICE="iPhone 15"
|
||||
LOG_PREFIX="DNP"
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Helper functions
|
||||
print_header() {
|
||||
echo ""
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
echo -e "${BLUE}$1${NC}"
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
print_step() {
|
||||
echo -e "${GREEN}[STEP $1]${NC} $2"
|
||||
echo ""
|
||||
}
|
||||
|
||||
print_info() {
|
||||
echo -e "${BLUE}ℹ️ $1${NC}"
|
||||
}
|
||||
|
||||
print_success() {
|
||||
echo -e "${GREEN}✅ $1${NC}"
|
||||
}
|
||||
|
||||
print_warn() {
|
||||
echo -e "${YELLOW}⚠️ $1${NC}"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "${RED}❌ $1${NC}"
|
||||
}
|
||||
|
||||
wait_for_user() {
|
||||
echo ""
|
||||
read -p "Press Enter to continue..."
|
||||
echo ""
|
||||
}
|
||||
|
||||
wait_for_ui_action() {
|
||||
echo ""
|
||||
echo -e "${YELLOW}$1${NC}"
|
||||
echo ""
|
||||
read -p "Press Enter when done..."
|
||||
echo ""
|
||||
}
|
||||
|
||||
# iOS-specific helper functions
|
||||
get_simulator_id() {
|
||||
xcrun simctl list devices available | grep "${SIMULATOR_DEVICE}" | head -1 | sed -E 's/.*\(([^)]+)\).*/\1/'
|
||||
}
|
||||
|
||||
get_app_logs() {
|
||||
local device_id=$1
|
||||
local lines=${2:-50}
|
||||
xcrun simctl spawn "${device_id}" log stream --level=debug --predicate 'processImagePath contains "ios-test-app"' --style=compact 2>/dev/null | head -n "${lines}" || echo ""
|
||||
}
|
||||
|
||||
check_plugin_configured() {
|
||||
print_info "Checking if plugin is configured..."
|
||||
|
||||
local device_id=$(get_simulator_id)
|
||||
if [ -z "${device_id}" ]; then
|
||||
print_error "Simulator not found: ${SIMULATOR_DEVICE}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check if app has been launched (indicates configuration may exist)
|
||||
local app_data=$(xcrun simctl get_app_container "${device_id}" "${APP_BUNDLE_ID}" 2>/dev/null || echo "")
|
||||
|
||||
if [ -n "${app_data}" ]; then
|
||||
print_success "App data exists (plugin may be configured)"
|
||||
print_info "Please verify in the app UI that you see:"
|
||||
echo " ⚙️ Plugin Settings: ✅ Configured"
|
||||
echo " 🔌 Native Fetcher: ✅ Configured"
|
||||
echo ""
|
||||
echo "If both show ✅, the plugin is configured and you can skip configuration."
|
||||
return 0
|
||||
else
|
||||
print_info "Plugin not configured (no app data found)"
|
||||
print_info "You will need to click 'Configure Plugin' in the app UI"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
check_permissions() {
|
||||
print_info "Checking notification permissions..."
|
||||
|
||||
# Note: iOS permissions are checked at runtime, not via command line
|
||||
# We can only check if the app has been granted permission by checking logs
|
||||
print_info "iOS notification permissions are checked at runtime."
|
||||
print_info "Please verify in the app UI that notifications are authorized."
|
||||
print_info "If not authorized, you'll need to grant permission in the app."
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
ensure_permissions() {
|
||||
if check_permissions; then
|
||||
print_success "Permissions check passed"
|
||||
return 0
|
||||
else
|
||||
print_info "Notification permissions needed"
|
||||
wait_for_ui_action "In the app UI, click the 'Request Permissions' button.
|
||||
|
||||
This will show a system permission dialog.
|
||||
|
||||
Steps:
|
||||
1. Click 'Request Permissions' button
|
||||
2. In the system dialog, tap 'Allow' to grant notification permission
|
||||
3. Return to the app and verify the status shows:
|
||||
- 🔔 Notifications: ✅ Granted (or similar)
|
||||
|
||||
Once permission is granted, press Enter to continue."
|
||||
|
||||
return 0
|
||||
fi
|
||||
}
|
||||
|
||||
launch_app() {
|
||||
print_info "Launching app on simulator..."
|
||||
|
||||
local device_id=$(get_simulator_id)
|
||||
if [ -z "${device_id}" ]; then
|
||||
print_error "Simulator not found: ${SIMULATOR_DEVICE}"
|
||||
print_info "Available simulators:"
|
||||
xcrun simctl list devices available | grep "iPhone" | head -5
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Boot simulator if not running
|
||||
local booted=$(xcrun simctl list devices | grep "${device_id}" | grep -c "Booted" || echo "0")
|
||||
if [ "${booted}" -eq "0" ]; then
|
||||
print_info "Booting simulator..."
|
||||
xcrun simctl boot "${device_id}" 2>/dev/null || true
|
||||
sleep 3
|
||||
fi
|
||||
|
||||
# Launch app
|
||||
xcrun simctl launch "${device_id}" "${APP_BUNDLE_ID}" 2>/dev/null || {
|
||||
print_warn "App may already be running or needs to be built first"
|
||||
print_info "Please build and run the app in Xcode first:"
|
||||
echo " 1. Open: test-apps/ios-test-app/ios/App/App.xcworkspace"
|
||||
echo " 2. Select simulator: ${SIMULATOR_DEVICE}"
|
||||
echo " 3. Press Cmd+R to build and run"
|
||||
echo ""
|
||||
wait_for_user
|
||||
}
|
||||
|
||||
sleep 2 # Give app time to launch
|
||||
print_success "App launched"
|
||||
}
|
||||
|
||||
get_pending_notifications() {
|
||||
local device_id=$(get_simulator_id)
|
||||
if [ -z "${device_id}" ]; then
|
||||
echo "0"
|
||||
return
|
||||
fi
|
||||
|
||||
# Note: iOS doesn't provide direct command-line access to pending notifications
|
||||
# This would need to be implemented via a plugin method
|
||||
# For now, we'll check logs for notification scheduling
|
||||
local count=$(get_app_logs "${device_id}" 100 | grep -c "scheduled notification" || echo "0")
|
||||
echo "${count}"
|
||||
}
|
||||
|
||||
should_run_test() {
|
||||
local test_id=$1
|
||||
shift
|
||||
local selected_tests=("$@")
|
||||
|
||||
if [ ${#selected_tests[@]} -eq 0 ]; then
|
||||
return 0 # Run all tests if none specified
|
||||
fi
|
||||
|
||||
for selected in "${selected_tests[@]}"; do
|
||||
if [ "${selected}" = "${test_id}" ]; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# TEST 0: Daily Rollover (Core Contract Verification)
|
||||
# ============================================
|
||||
if should_run_test "0" "$@"; then
|
||||
print_header "TEST 0: Daily Rollover Verification"
|
||||
echo "Purpose: Verify that after a notification fires, the next day's"
|
||||
echo " schedule is correctly computed and only ONE notification exists."
|
||||
echo ""
|
||||
echo "Note: This test verifies the core 'one notification per day' contract."
|
||||
echo " It requires either:"
|
||||
echo " 1. Scheduling a notification for 'now + N seconds' and waiting, OR"
|
||||
echo " 2. Manipulating the simulator clock to cross the fire boundary."
|
||||
echo ""
|
||||
wait_for_user
|
||||
|
||||
print_step "1" "Schedule a test notification for near-future..."
|
||||
launch_app
|
||||
check_plugin_configured || {
|
||||
wait_for_ui_action "In the app UI, click 'Configure Plugin' to set up the plugin.
|
||||
|
||||
This will initialize the database and storage.
|
||||
|
||||
Once configured, press Enter to continue."
|
||||
}
|
||||
|
||||
INITIAL_COUNT=$(get_pending_notifications)
|
||||
print_info "Current pending notifications: ${INITIAL_COUNT}"
|
||||
|
||||
wait_for_ui_action "In the app UI, schedule a daily notification.
|
||||
|
||||
For this test, you may want to schedule it for a time very soon
|
||||
(e.g., 1-2 minutes from now) to observe the rollover behavior.
|
||||
|
||||
This will schedule:
|
||||
- 1 notification (UNUserNotificationCenter) for the specified time
|
||||
- 1 prefetch task (BGTaskScheduler) for 2 minutes before that time"
|
||||
|
||||
sleep 3 # Give notification time to be registered
|
||||
|
||||
POST_SCHEDULE_COUNT=$(get_pending_notifications)
|
||||
print_info "Pending notifications after scheduling: ${POST_SCHEDULE_COUNT}"
|
||||
|
||||
print_step "2" "Manual verification steps..."
|
||||
echo ""
|
||||
echo "To complete this test, you need to:"
|
||||
echo " 1. Wait for the notification to fire (or advance simulator clock)"
|
||||
echo " 2. Check that the plugin:"
|
||||
echo " - Computed the next day's time (24 hours later)"
|
||||
echo " - Scheduled exactly ONE notification for tomorrow"
|
||||
echo " - Did NOT create duplicate notifications"
|
||||
echo " 3. Verify in logs (Xcode Console or Console.app):"
|
||||
echo " - Next run time calculation shows tomorrow's time"
|
||||
echo " - Only one notification scheduled"
|
||||
echo ""
|
||||
echo "Expected log patterns:"
|
||||
echo " DNP-SCHEDULE: Scheduling next daily notification: ... source=ROLLOVER_ON_FIRE"
|
||||
echo " DNP-NOTIFY: Scheduling notification: triggerTime=<tomorrow's time>"
|
||||
echo ""
|
||||
|
||||
wait_for_ui_action "After the notification fires (or you advance the clock),
|
||||
check the logs and verify:
|
||||
|
||||
1. Only ONE notification exists (one per day)
|
||||
2. The notification time is for tomorrow (24 hours later)
|
||||
3. No duplicate notifications were created
|
||||
|
||||
Press Enter when verification is complete."
|
||||
|
||||
print_success "TEST 0: Daily Rollover Verification - Manual verification required"
|
||||
fi
|
||||
|
||||
# ============================================
|
||||
# TEST 1: Cold Start Recovery
|
||||
# ============================================
|
||||
if should_run_test "1" "$@"; then
|
||||
print_header "TEST 1: Cold Start Recovery"
|
||||
echo "Purpose: Verify that when the app launches after termination,"
|
||||
echo " missed notifications are detected and future notifications"
|
||||
echo " are verified/rescheduled."
|
||||
echo ""
|
||||
wait_for_user
|
||||
|
||||
print_step "1" "Schedule a notification for future time..."
|
||||
launch_app
|
||||
ensure_permissions
|
||||
|
||||
wait_for_ui_action "In the app UI, schedule a daily notification for a future time
|
||||
(e.g., 1 hour from now).
|
||||
|
||||
This creates a notification that should persist across app termination."
|
||||
|
||||
sleep 2
|
||||
|
||||
print_step "2" "Terminate app (simulate cold start)..."
|
||||
print_info "Terminating app to simulate cold start scenario..."
|
||||
|
||||
local device_id=$(get_simulator_id)
|
||||
xcrun simctl terminate "${device_id}" "${APP_BUNDLE_ID}" 2>/dev/null || true
|
||||
|
||||
print_info "App terminated. Waiting 5 seconds..."
|
||||
sleep 5
|
||||
|
||||
print_step "3" "Launch app and verify recovery..."
|
||||
launch_app
|
||||
|
||||
print_info "Checking logs for recovery activity..."
|
||||
sleep 3
|
||||
|
||||
local device_id=$(get_simulator_id)
|
||||
local logs=$(get_app_logs "${device_id}" 100)
|
||||
|
||||
if echo "${logs}" | grep -q "DNP-REACTIVATION\|recovery\|missed"; then
|
||||
print_success "Recovery activity detected in logs"
|
||||
else
|
||||
print_warn "No recovery activity detected in logs"
|
||||
print_info "This may indicate recovery is not yet implemented (expected for Phase 1)"
|
||||
fi
|
||||
|
||||
wait_for_ui_action "Verify in the app UI that:
|
||||
1. The notification is still scheduled (check 'Scheduled Notifications' screen)
|
||||
2. Any missed notifications are marked as missed
|
||||
3. Future notifications are verified/rescheduled
|
||||
|
||||
Press Enter when verification is complete."
|
||||
|
||||
print_success "TEST 1: Cold Start Recovery - Manual verification required"
|
||||
fi
|
||||
|
||||
# ============================================
|
||||
# TEST 2: Notification Persistence (Swipe from App Switcher)
|
||||
# ============================================
|
||||
if should_run_test "2" "$@"; then
|
||||
print_header "TEST 2: Notification Persistence (App Termination)"
|
||||
echo "Purpose: Verify that notifications persist when app is terminated"
|
||||
echo " (iOS OS-guaranteed behavior)."
|
||||
echo ""
|
||||
wait_for_user
|
||||
|
||||
print_step "1" "Schedule a notification..."
|
||||
launch_app
|
||||
ensure_permissions
|
||||
|
||||
wait_for_ui_action "In the app UI, schedule a daily notification for a future time."
|
||||
|
||||
sleep 2
|
||||
|
||||
print_step "2" "Terminate app..."
|
||||
print_info "Terminating app (simulating swipe from app switcher)..."
|
||||
|
||||
local device_id=$(get_simulator_id)
|
||||
xcrun simctl terminate "${device_id}" "${APP_BUNDLE_ID}" 2>/dev/null || true
|
||||
|
||||
print_info "App terminated. Waiting 3 seconds..."
|
||||
sleep 3
|
||||
|
||||
print_step "3" "Verify notification still exists..."
|
||||
print_info "On iOS, notifications persist automatically (OS-guaranteed)."
|
||||
print_info "The notification should still fire even though the app is terminated."
|
||||
|
||||
wait_for_ui_action "Verify that:
|
||||
1. The notification fires at the scheduled time (even though app is terminated)
|
||||
2. When you tap the notification, the app launches
|
||||
3. The notification is marked as delivered
|
||||
|
||||
Press Enter when verification is complete."
|
||||
|
||||
print_success "TEST 2: Notification Persistence - iOS OS-guaranteed behavior verified"
|
||||
fi
|
||||
|
||||
# ============================================
|
||||
# TEST 3: Invalid Data Handling
|
||||
# ============================================
|
||||
if should_run_test "3" "$@"; then
|
||||
print_header "TEST 3: Invalid Data Handling"
|
||||
echo "Purpose: Verify that the plugin handles invalid data gracefully"
|
||||
echo " without crashing."
|
||||
echo ""
|
||||
wait_for_user
|
||||
|
||||
print_step "1" "Test invalid notification time..."
|
||||
launch_app
|
||||
ensure_permissions
|
||||
|
||||
wait_for_ui_action "In the app UI, try to schedule a notification with invalid data:
|
||||
1. Empty time string
|
||||
2. Invalid time format (e.g., '25:00' or '12:99')
|
||||
3. Negative time values
|
||||
|
||||
The app should show an error message and NOT crash."
|
||||
|
||||
print_info "Checking logs for error handling..."
|
||||
sleep 2
|
||||
|
||||
local device_id=$(get_simulator_id)
|
||||
local logs=$(get_app_logs "${device_id}" 50)
|
||||
|
||||
if echo "${logs}" | grep -q "error\|invalid\|Error"; then
|
||||
print_success "Error handling detected in logs"
|
||||
else
|
||||
print_info "No errors in recent logs (may indicate graceful handling)"
|
||||
fi
|
||||
|
||||
wait_for_ui_action "Verify that:
|
||||
1. Invalid data is rejected with clear error messages
|
||||
2. The app does NOT crash
|
||||
3. Valid notifications can still be scheduled after errors
|
||||
|
||||
Press Enter when verification is complete."
|
||||
|
||||
print_success "TEST 3: Invalid Data Handling - Manual verification required"
|
||||
fi
|
||||
|
||||
# ============================================
|
||||
# Summary
|
||||
# ============================================
|
||||
print_header "Phase 1 Testing Complete"
|
||||
echo "All Phase 1 tests have been executed."
|
||||
echo ""
|
||||
echo "Note: iOS recovery features (ReactivationManager) are NOT yet implemented."
|
||||
echo " Tests 1 and 2 will show expected behavior once recovery is implemented."
|
||||
echo ""
|
||||
echo "Next Steps:"
|
||||
echo " 1. Review test results"
|
||||
echo " 2. Check logs for any errors"
|
||||
echo " 3. Implement recovery features (Phase 1 directive)"
|
||||
echo " 4. Re-run tests after implementation"
|
||||
echo ""
|
||||
|
||||
58
test-apps/ios-test-app/test-phase2.sh
Executable file
58
test-apps/ios-test-app/test-phase2.sh
Executable file
@@ -0,0 +1,58 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Phase 2 Testing Script - iOS App Termination Recovery
|
||||
# Tests app termination detection and recovery (iOS equivalent of Android force stop)
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
if [ -f "${SCRIPT_DIR}/test-phase1.sh" ]; then
|
||||
source "${SCRIPT_DIR}/test-phase1.sh" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
print_header "Phase 2: App Termination Recovery Testing"
|
||||
echo "Note: iOS doesn't have user-facing 'force stop' like Android."
|
||||
echo " This tests system termination scenarios and recovery."
|
||||
echo ""
|
||||
|
||||
# Note: Phase 2 features are NOT yet implemented
|
||||
print_warn "⚠️ Phase 2 recovery features (termination detection) are NOT yet implemented."
|
||||
print_info "These tests will verify expected behavior once implementation is complete."
|
||||
echo ""
|
||||
|
||||
wait_for_user
|
||||
|
||||
print_header "TEST 1: App Termination Detection"
|
||||
echo "Purpose: Verify that when app is terminated by system,"
|
||||
echo " recovery detects termination and reschedules notifications."
|
||||
echo ""
|
||||
|
||||
launch_app
|
||||
check_plugin_configured
|
||||
|
||||
wait_for_ui_action "Schedule a notification for future time."
|
||||
|
||||
print_info "Terminating app to simulate system termination..."
|
||||
local device_id=$(get_simulator_id)
|
||||
xcrun simctl terminate "${device_id}" "${APP_BUNDLE_ID}" 2>/dev/null || true
|
||||
|
||||
sleep 3
|
||||
|
||||
print_info "Launching app to trigger recovery..."
|
||||
launch_app
|
||||
sleep 5
|
||||
|
||||
print_info "Checking logs for termination detection..."
|
||||
local device_id=$(get_simulator_id)
|
||||
local logs=$(get_app_logs "${device_id}" 100)
|
||||
|
||||
if echo "${logs}" | grep -q "termination\|DNP-REACTIVATION"; then
|
||||
print_success "Recovery activity detected"
|
||||
else
|
||||
print_warn "No recovery activity detected (expected - not yet implemented)"
|
||||
fi
|
||||
|
||||
wait_for_ui_action "Verify notifications are rescheduled after termination."
|
||||
|
||||
print_success "Phase 2 testing complete (implementation pending)"
|
||||
|
||||
52
test-apps/ios-test-app/test-phase3.sh
Executable file
52
test-apps/ios-test-app/test-phase3.sh
Executable file
@@ -0,0 +1,52 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Phase 3 Testing Script - iOS Boot Recovery
|
||||
# Tests boot recovery and background task registration
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
if [ -f "${SCRIPT_DIR}/test-phase1.sh" ]; then
|
||||
source "${SCRIPT_DIR}/test-phase1.sh" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
print_header "Phase 3: Boot Recovery Testing"
|
||||
echo "Note: iOS automatically persists notifications across reboot (OS-guaranteed)."
|
||||
echo " This tests BGTaskScheduler registration and boot recovery logic."
|
||||
echo ""
|
||||
|
||||
# Note: Phase 3 features are NOT yet implemented
|
||||
print_warn "⚠️ Phase 3 recovery features (BGTaskScheduler boot recovery) are NOT yet implemented."
|
||||
print_info "These tests will verify expected behavior once implementation is complete."
|
||||
echo ""
|
||||
|
||||
wait_for_user
|
||||
|
||||
print_header "TEST 1: Boot Recovery with Future Notifications"
|
||||
echo "Purpose: Verify notifications persist across reboot and recovery logic runs."
|
||||
echo ""
|
||||
|
||||
launch_app
|
||||
check_plugin_configured
|
||||
|
||||
wait_for_ui_action "Schedule a notification for future time."
|
||||
|
||||
print_info "On iOS, notifications persist automatically across reboot."
|
||||
print_info "We'll verify BGTaskScheduler registration and recovery logic."
|
||||
|
||||
print_warn "⚠️ Simulator reboot testing requires manual steps:"
|
||||
echo " 1. Schedule notification"
|
||||
echo " 2. Reboot simulator (Device → Restart in Simulator menu)"
|
||||
echo " 3. Launch app after reboot"
|
||||
echo " 4. Verify notifications still exist"
|
||||
echo " 5. Check logs for recovery activity"
|
||||
echo ""
|
||||
|
||||
wait_for_ui_action "After rebooting simulator and launching app,
|
||||
verify that:
|
||||
1. Notifications still exist (iOS OS-guaranteed)
|
||||
2. Recovery logic runs (once implemented)
|
||||
3. Any missed notifications are detected"
|
||||
|
||||
print_success "Phase 3 testing complete (implementation pending)"
|
||||
|
||||
Reference in New Issue
Block a user