docs: Consolidate documentation structure (139 files, zero information loss)
Consolidate all markdown documentation into organized structure per CONSOLIDATION_DIRECTIVE. All files preserved (canonical, merged, or archived). - docs/integration/ - Integration documentation (7 files) - docs/platform/ios/ - iOS platform docs (12 files) - docs/platform/android/ - Android platform docs (9 files) - docs/testing/ - Testing documentation (15 files) - docs/design/ - Design & research (5 files) - docs/ai/ - AI/ChatGPT artifacts (7 files) - docs/archive/2025-legacy-doc/ - Historical docs (17 files) - Integration: Root INTEGRATION_GUIDE.md → docs/integration/ - Platform: Separated iOS and Android into platform/ subdirectories - Testing: Consolidated all testing docs to docs/testing/ - Legacy: Archived entire doc/ directory to archive/ - AI: Moved all ChatGPT artifacts to docs/ai/ - Added docs/00-INDEX.md - Central navigation hub - Added docs/CONSOLIDATION_SOURCE_MAP.md - Complete audit trail - Added docs/CONSOLIDATION_COMPLETE.md - Consolidation summary - Updated README.md with links to documentation index - All 139 files have destinations (see CONSOLIDATION_SOURCE_MAP.md) - Zero information loss (all files preserved) - Archive preserves original structure - Index provides clear navigation - 87 files moved/created/updated - Root-level docs consolidated - Legacy doc/ directory archived - Test app docs remain with test apps (indexed) Ref: CONSOLIDATION_DIRECTIVE Author: Matthew Raymer
This commit is contained in:
633
docs/platform/ios/ROLLOVER_IMPLEMENTATION_REVIEW.md
Normal file
633
docs/platform/ios/ROLLOVER_IMPLEMENTATION_REVIEW.md
Normal file
@@ -0,0 +1,633 @@
|
||||
# iOS Rollover Implementation — Comprehensive Review
|
||||
|
||||
**Status**: Pre-Implementation Review
|
||||
**Date**: 2025-01-27
|
||||
**Priority**: Reliability-First
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Plan Overview](#plan-overview)
|
||||
2. [File Changes Summary](#file-changes-summary)
|
||||
3. [Detailed File Modifications](#detailed-file-modifications)
|
||||
4. [Integration Points](#integration-points)
|
||||
5. [Dependencies & Order](#dependencies--order)
|
||||
6. [Testing Strategy](#testing-strategy)
|
||||
7. [Open Questions](#open-questions)
|
||||
|
||||
---
|
||||
|
||||
## Plan Overview
|
||||
|
||||
### Objective
|
||||
Implement Android-like automatic rollover for iOS notifications with comprehensive edge case handling.
|
||||
|
||||
### Key Features
|
||||
- ✅ Automatic rollover when notification fires (24 hours later)
|
||||
- ✅ DST-safe time calculations
|
||||
- ✅ Multi-level duplicate prevention
|
||||
- ✅ Time/timezone change detection and recovery
|
||||
- ✅ Race condition prevention
|
||||
- ✅ Comprehensive edge case handling
|
||||
|
||||
### Architecture Components
|
||||
1. **TimeChangeDetector** — Detects time changes
|
||||
2. **TimezoneChangeDetector** — Detects timezone changes
|
||||
3. **RolloverCoordinator** — Coordinates rollover operations
|
||||
4. **Enhanced Recovery Manager** — Integrates all edge case handling
|
||||
|
||||
---
|
||||
|
||||
## File Changes Summary
|
||||
|
||||
| File | Change Type | Lines Added | Purpose |
|
||||
|------|-------------|-------------|---------|
|
||||
| `DailyNotificationScheduler.swift` | Add methods | ~150 | DST-safe calculation + rollover scheduling |
|
||||
| `DailyNotificationPlugin.swift` | Add method | ~50 | Rollover handler entry point |
|
||||
| `AppDelegate.swift` | Modify method | ~20 | Detect notification delivery (foreground) |
|
||||
| `DailyNotificationReactivationManager.swift` | Enhance | ~100 | Rollover on app launch recovery |
|
||||
| `DailyNotificationStorage.swift` | Add methods | ~30 | Rollover state tracking |
|
||||
| `DailyNotificationTimeChangeDetector.swift` | New file | ~200 | Time change detection |
|
||||
| `DailyNotificationTimezoneChangeDetector.swift` | New file | ~150 | Timezone change detection |
|
||||
| `DailyNotificationRolloverCoordinator.swift` | New file | ~250 | Rollover coordination |
|
||||
|
||||
**Total**: ~950 lines of new/modified code
|
||||
|
||||
---
|
||||
|
||||
## Detailed File Modifications
|
||||
|
||||
### 1. DailyNotificationScheduler.swift
|
||||
|
||||
**Location**: `ios/Plugin/DailyNotificationScheduler.swift`
|
||||
|
||||
#### Change 1.1: Add DST-Safe Next Time Calculation
|
||||
|
||||
**Insert after line 307** (after `calculateNextOccurrence` method):
|
||||
|
||||
```swift
|
||||
/**
|
||||
* Calculate next scheduled time from current scheduled time (24 hours later, DST-safe)
|
||||
*
|
||||
* Matches Android calculateNextScheduledTime() functionality
|
||||
* Handles DST transitions automatically using Calendar
|
||||
*
|
||||
* @param currentScheduledTime Current scheduled time in milliseconds
|
||||
* @return Next scheduled time in milliseconds (24 hours later)
|
||||
*/
|
||||
func calculateNextScheduledTime(_ currentScheduledTime: Int64) -> Int64 {
|
||||
let calendar = Calendar.current
|
||||
let currentDate = Date(timeIntervalSince1970: Double(currentScheduledTime) / 1000.0)
|
||||
|
||||
// Add 24 hours (handles DST transitions automatically)
|
||||
guard let nextDate = calendar.date(byAdding: .hour, value: 24, to: currentDate) else {
|
||||
// Fallback to simple 24-hour addition if calendar calculation fails
|
||||
print("\(Self.TAG): DST calculation failed, using fallback")
|
||||
return currentScheduledTime + (24 * 60 * 60 * 1000)
|
||||
}
|
||||
|
||||
// Validate: Log DST transitions for debugging
|
||||
let currentHour = calendar.component(.hour, from: currentDate)
|
||||
let currentMinute = calendar.component(.minute, from: currentDate)
|
||||
let nextHour = calendar.component(.hour, from: nextDate)
|
||||
let nextMinute = calendar.component(.minute, from: nextDate)
|
||||
|
||||
if currentHour != nextHour || currentMinute != nextMinute {
|
||||
print("\(Self.TAG): DST transition detected: \(currentHour):\(String(format: "%02d", currentMinute)) -> \(nextHour):\(String(format: "%02d", nextMinute))")
|
||||
}
|
||||
|
||||
return Int64(nextDate.timeIntervalSince1970 * 1000)
|
||||
}
|
||||
```
|
||||
|
||||
#### Change 1.2: Add Rollover Scheduling Method
|
||||
|
||||
**Insert after line 202** (after `scheduleNotification` method):
|
||||
|
||||
```swift
|
||||
/**
|
||||
* Schedule next notification after current one fires (rollover)
|
||||
*
|
||||
* Matches Android scheduleNextNotification() functionality
|
||||
* Implements multi-level duplicate prevention
|
||||
*
|
||||
* @param content Current notification content that just fired
|
||||
* @param storage Storage instance for duplicate checking
|
||||
* @param fetcher Optional fetcher for scheduling prefetch
|
||||
* @return true if next notification was scheduled successfully
|
||||
*/
|
||||
func scheduleNextNotification(
|
||||
_ content: NotificationContent,
|
||||
storage: DailyNotificationStorage?,
|
||||
fetcher: DailyNotificationFetcher? = nil
|
||||
) async -> Bool {
|
||||
print("\(Self.TAG): RESCHEDULE_START id=\(content.id)")
|
||||
|
||||
// Check 1: Rollover state tracking (prevent duplicate rollover attempts)
|
||||
if let storage = storage {
|
||||
let lastRolloverTime = await storage.getLastRolloverTime(for: content.id)
|
||||
let currentTime = Int64(Date().timeIntervalSince1970 * 1000)
|
||||
|
||||
// If rollover was processed recently (< 1 hour ago), skip
|
||||
if let lastTime = lastRolloverTime,
|
||||
(currentTime - lastTime) < (60 * 60 * 1000) {
|
||||
print("\(Self.TAG): RESCHEDULE_SKIP id=\(content.id) already_processed")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate next occurrence using DST-safe calculation
|
||||
let nextScheduledTime = calculateNextScheduledTime(content.scheduledTime)
|
||||
|
||||
// Check 2: Storage-level duplicate check (prevent duplicate notifications)
|
||||
if let storage = storage {
|
||||
let existingNotifications = storage.getAllNotifications()
|
||||
let toleranceMs: Int64 = 60 * 1000 // 1 minute tolerance for DST shifts
|
||||
|
||||
for existing in existingNotifications {
|
||||
if abs(existing.scheduledTime - nextScheduledTime) <= toleranceMs {
|
||||
print("\(Self.TAG): RESCHEDULE_DUPLICATE id=\(content.id) existing_id=\(existing.id) time_diff_ms=\(abs(existing.scheduledTime - nextScheduledTime))")
|
||||
return false // Skip rescheduling to prevent duplicate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check 3: System-level duplicate check (query UNUserNotificationCenter)
|
||||
let pendingNotifications = await notificationCenter.pendingNotificationRequests()
|
||||
for pending in pendingNotifications {
|
||||
if let trigger = pending.trigger as? UNCalendarNotificationTrigger,
|
||||
let nextDate = trigger.nextTriggerDate() {
|
||||
let pendingTime = Int64(nextDate.timeIntervalSince1970 * 1000)
|
||||
let toleranceMs: Int64 = 60 * 1000
|
||||
|
||||
if abs(pendingTime - nextScheduledTime) <= toleranceMs {
|
||||
print("\(Self.TAG): RESCHEDULE_DUPLICATE id=\(content.id) system_pending_id=\(pending.identifier)")
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extract hour:minute from current scheduled time for logging
|
||||
let calendar = Calendar.current
|
||||
let scheduledDate = content.getScheduledTimeAsDate()
|
||||
let hour = calendar.component(.hour, from: scheduledDate)
|
||||
let minute = calendar.component(.minute, from: scheduledDate)
|
||||
|
||||
// Create new notification content for next occurrence
|
||||
// Note: Content will be refreshed by prefetch, but we need placeholder
|
||||
let nextId = "daily_rollover_\(Int64(Date().timeIntervalSince1970 * 1000))"
|
||||
let nextContent = NotificationContent(
|
||||
id: nextId,
|
||||
title: content.title, // Will be updated by prefetch
|
||||
body: content.body, // Will be updated by prefetch
|
||||
scheduledTime: nextScheduledTime,
|
||||
fetchedAt: Int64(Date().timeIntervalSince1970 * 1000),
|
||||
url: content.url,
|
||||
payload: content.payload,
|
||||
etag: content.etag
|
||||
)
|
||||
|
||||
// Schedule the next notification
|
||||
let scheduled = await scheduleNotification(nextContent)
|
||||
|
||||
if scheduled {
|
||||
let nextTimeStr = formatTime(nextScheduledTime)
|
||||
print("\(Self.TAG): RESCHEDULE_OK id=\(content.id) next=\(nextTimeStr) nextId=\(nextId)")
|
||||
|
||||
// Schedule background fetch for next notification (5 minutes before scheduled time)
|
||||
// Note: DailyNotificationFetcher integration deferred to Phase 2
|
||||
if let fetcher = fetcher {
|
||||
let fetchTime = nextScheduledTime - (5 * 60 * 1000) // 5 minutes before
|
||||
let currentTime = Int64(Date().timeIntervalSince1970 * 1000)
|
||||
|
||||
if fetchTime > currentTime {
|
||||
// TODO: Phase 2 - Implement fetcher.scheduleFetch(fetchTime)
|
||||
print("\(Self.TAG): RESCHEDULE_PREFETCH_SCHEDULED id=\(content.id) next_fetch=\(fetchTime) next_notification=\(nextScheduledTime)")
|
||||
} else {
|
||||
// TODO: Phase 2 - Implement fetcher.scheduleImmediateFetch()
|
||||
print("\(Self.TAG): RESCHEDULE_PREFETCH_PAST id=\(content.id) fetch_time=\(fetchTime) current=\(currentTime)")
|
||||
}
|
||||
} else {
|
||||
print("\(Self.TAG): RESCHEDULE_PREFETCH_SKIP id=\(content.id) fetcher_not_available")
|
||||
}
|
||||
|
||||
// Mark rollover as processed
|
||||
await storage?.saveLastRolloverTime(for: content.id, time: Int64(Date().timeIntervalSince1970 * 1000))
|
||||
|
||||
return true
|
||||
} else {
|
||||
print("\(Self.TAG): RESCHEDULE_ERR id=\(content.id) scheduling_failed")
|
||||
return false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Note**: The `formatTime` method already exists (line 273), so no change needed there.
|
||||
|
||||
---
|
||||
|
||||
### 2. DailyNotificationPlugin.swift
|
||||
|
||||
**Location**: `ios/Plugin/DailyNotificationPlugin.swift`
|
||||
|
||||
#### Change 2.1: Add Rollover Handler Method + Notification Observer
|
||||
|
||||
**Insert after line 77** (in `load()` method, after recovery manager initialization):
|
||||
|
||||
```swift
|
||||
// Register for notification delivery events (Notification Center pattern)
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(handleNotificationDelivery(_:)),
|
||||
name: NSNotification.Name("DailyNotificationDelivered"),
|
||||
object: nil
|
||||
)
|
||||
```
|
||||
|
||||
**Insert after line 1242** (after `getNotificationStatus` method):
|
||||
|
||||
```swift
|
||||
/**
|
||||
* Handle notification delivery event (from Notification Center)
|
||||
*
|
||||
* This is called when AppDelegate posts notification delivery event
|
||||
* Matches Android's scheduleNextNotification() behavior
|
||||
*
|
||||
* @param notification NSNotification with userInfo containing notification_id and scheduled_time
|
||||
*/
|
||||
@objc private func handleNotificationDelivery(_ notification: Notification) {
|
||||
guard let userInfo = notification.userInfo,
|
||||
let notificationId = userInfo["notification_id"] as? String,
|
||||
let scheduledTime = userInfo["scheduled_time"] as? Int64 else {
|
||||
print("DNP-ROLLOVER: Invalid notification data")
|
||||
return
|
||||
}
|
||||
|
||||
Task {
|
||||
await processRollover(notificationId: notificationId, scheduledTime: scheduledTime)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process rollover for delivered notification
|
||||
*
|
||||
* @param notificationId ID of notification that was delivered
|
||||
* @param scheduledTime Scheduled time of delivered notification
|
||||
*/
|
||||
private func processRollover(notificationId: String, scheduledTime: Int64) async {
|
||||
guard let scheduler = scheduler, let storage = storage else {
|
||||
print("DNP-ROLLOVER: Plugin not initialized")
|
||||
return
|
||||
}
|
||||
|
||||
// Get the notification content that was delivered
|
||||
guard let content = storage.getNotificationContent(id: notificationId) else {
|
||||
print("DNP-ROLLOVER: Could not find notification content for id=\(notificationId)")
|
||||
return
|
||||
}
|
||||
|
||||
// Schedule next notification
|
||||
// Note: DailyNotificationFetcher integration deferred to Phase 2
|
||||
let scheduled = await scheduler.scheduleNextNotification(
|
||||
content,
|
||||
storage: storage,
|
||||
fetcher: nil // TODO: Phase 2 - Add fetcher instance
|
||||
)
|
||||
|
||||
if scheduled {
|
||||
print("DNP-ROLLOVER: Successfully scheduled next notification for id=\(notificationId)")
|
||||
// Log success (non-fatal, background operation)
|
||||
} else {
|
||||
print("DNP-ROLLOVER: Failed to schedule next notification for id=\(notificationId)")
|
||||
// Log failure but continue (recovery will handle on next launch)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Change 2.2: Update getNotificationStatus to Include Rollover Info
|
||||
|
||||
**Modify line 1229-1236** (in `getNotificationStatus` method):
|
||||
|
||||
```swift
|
||||
// Calculate next notification time
|
||||
let nextNotificationTime = await scheduler.getNextNotificationTime() ?? 0
|
||||
|
||||
// Get rollover status
|
||||
let lastRolloverTime = await storage?.getLastRolloverTime() ?? 0
|
||||
|
||||
var result: [String: Any] = [
|
||||
"isEnabled": isEnabled,
|
||||
"isScheduled": pendingCount > 0,
|
||||
"lastNotificationTime": lastNotification?.scheduledTime ?? 0,
|
||||
"nextNotificationTime": nextNotificationTime,
|
||||
"pending": pendingCount,
|
||||
"rolloverEnabled": true, // Indicate rollover is active
|
||||
"lastRolloverTime": lastRolloverTime, // When last rollover occurred
|
||||
"settings": settings
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. AppDelegate.swift
|
||||
|
||||
**Location**: `test-apps/ios-test-app/ios/App/App/AppDelegate.swift`
|
||||
|
||||
#### Change 3.1: Modify willPresent to Trigger Rollover
|
||||
|
||||
**Replace lines 136-152** with:
|
||||
|
||||
```swift
|
||||
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
|
||||
NSLog("DNP-DEBUG: ✅ userNotificationCenter willPresent called!")
|
||||
NSLog("DNP-DEBUG: Notification received in foreground: %@", notification.request.identifier)
|
||||
NSLog("DNP-DEBUG: Notification title: %@", notification.request.content.title)
|
||||
NSLog("DNP-DEBUG: Notification body: %@", notification.request.content.body)
|
||||
|
||||
// Extract notification info from userInfo for rollover
|
||||
let userInfo = notification.request.content.userInfo
|
||||
if let notificationId = userInfo["notification_id"] as? String,
|
||||
let scheduledTime = userInfo["scheduled_time"] as? Int64 {
|
||||
|
||||
// Trigger rollover scheduling (async, non-blocking)
|
||||
Task {
|
||||
await handleNotificationRollover(notificationId: notificationId, scheduledTime: scheduledTime)
|
||||
}
|
||||
}
|
||||
|
||||
// Show notification with banner, sound, and badge
|
||||
// Use .banner for iOS 14+, fallback to .alert for iOS 13
|
||||
if #available(iOS 14.0, *) {
|
||||
completionHandler([.banner, .sound, .badge])
|
||||
} else {
|
||||
completionHandler([.alert, .sound, .badge])
|
||||
}
|
||||
|
||||
NSLog("DNP-DEBUG: ✅ Completion handler called with presentation options")
|
||||
}
|
||||
```
|
||||
|
||||
#### Change 3.2: Post Notification for Rollover (Notification Center Pattern)
|
||||
|
||||
**Insert after line 152** (after `willPresent` completion handler):
|
||||
|
||||
```swift
|
||||
// Post notification to trigger rollover (decoupled pattern)
|
||||
NotificationCenter.default.post(
|
||||
name: NSNotification.Name("DailyNotificationDelivered"),
|
||||
object: nil,
|
||||
userInfo: [
|
||||
"notification_id": notificationId,
|
||||
"scheduled_time": scheduledTime
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
**Note**: This uses Notification Center pattern for decoupling. Plugin will observe this notification.
|
||||
|
||||
---
|
||||
|
||||
### 4. DailyNotificationStorage.swift
|
||||
|
||||
**Location**: `ios/Plugin/DailyNotificationStorage.swift`
|
||||
|
||||
#### Change 4.1: Add Rollover State Tracking Methods
|
||||
|
||||
**Insert after line 148** (after `getAllNotifications` method):
|
||||
|
||||
```swift
|
||||
/**
|
||||
* Get last rollover time for a notification ID
|
||||
*
|
||||
* @param notificationId Notification ID
|
||||
* @return Last rollover time in milliseconds, or nil if never rolled over
|
||||
*/
|
||||
func getLastRolloverTime(for notificationId: String) async -> Int64? {
|
||||
let key = "rollover_\(notificationId)"
|
||||
let lastTime = userDefaults.object(forKey: key) as? Int64
|
||||
return lastTime
|
||||
}
|
||||
|
||||
/**
|
||||
* Save last rollover time for a notification ID
|
||||
*
|
||||
* @param notificationId Notification ID
|
||||
* @param time Rollover time in milliseconds
|
||||
*/
|
||||
func saveLastRolloverTime(for notificationId: String, time: Int64) async {
|
||||
let key = "rollover_\(notificationId)"
|
||||
userDefaults.set(time, forKey: key)
|
||||
userDefaults.synchronize()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get last rollover time (any notification)
|
||||
*
|
||||
* @return Last rollover time in milliseconds, or 0 if never rolled over
|
||||
*/
|
||||
func getLastRolloverTime() -> Int64 {
|
||||
let key = "rollover_last"
|
||||
return Int64(userDefaults.integer(forKey: key))
|
||||
}
|
||||
|
||||
/**
|
||||
* Save last rollover time (any notification)
|
||||
*
|
||||
* @param time Rollover time in milliseconds
|
||||
*/
|
||||
func saveLastRolloverTime(_ time: Int64) {
|
||||
let key = "rollover_last"
|
||||
userDefaults.set(time, forKey: key)
|
||||
userDefaults.synchronize()
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. DailyNotificationReactivationManager.swift
|
||||
|
||||
**Location**: `ios/Plugin/DailyNotificationReactivationManager.swift`
|
||||
|
||||
#### Change 5.1: Add Rollover Check to Recovery
|
||||
|
||||
**Insert after line 338** (in `performColdStartRecovery` method, after detecting missed notifications):
|
||||
|
||||
```swift
|
||||
// Step 4.5: Check for delivered notifications and trigger rollover
|
||||
// This handles notifications that were delivered while app was not running
|
||||
await checkAndProcessDeliveredNotifications()
|
||||
```
|
||||
|
||||
#### Change 5.2: Add Delivered Notifications Check Method
|
||||
|
||||
**Insert at end of class** (before closing brace):
|
||||
|
||||
```swift
|
||||
/**
|
||||
* Check for delivered notifications and trigger rollover
|
||||
*
|
||||
* This ensures rollover happens on app launch if notifications were delivered
|
||||
* while the app was not running
|
||||
*/
|
||||
private func checkAndProcessDeliveredNotifications() async {
|
||||
print("\(Self.TAG): Checking for delivered notifications to trigger rollover")
|
||||
|
||||
// Get delivered notifications from system
|
||||
let deliveredNotifications = await notificationCenter.deliveredNotifications()
|
||||
|
||||
// Get last processed rollover time from storage
|
||||
let lastProcessedTime = storage.getLastRolloverTime()
|
||||
|
||||
for notification in deliveredNotifications {
|
||||
let userInfo = notification.request.content.userInfo
|
||||
|
||||
guard let notificationId = userInfo["notification_id"] as? String,
|
||||
let scheduledTime = userInfo["scheduled_time"] as? Int64 else {
|
||||
continue
|
||||
}
|
||||
|
||||
// Only process if this notification hasn't been processed yet
|
||||
if scheduledTime > lastProcessedTime {
|
||||
print("\(Self.TAG): Found delivered notification id=\(notificationId) scheduledTime=\(scheduledTime)")
|
||||
|
||||
// Get notification content
|
||||
guard let content = storage.getNotificationContent(id: notificationId) else {
|
||||
print("\(Self.TAG): Could not find content for delivered notification id=\(notificationId)")
|
||||
continue
|
||||
}
|
||||
|
||||
// Trigger rollover
|
||||
let scheduled = await scheduler.scheduleNextNotification(
|
||||
content,
|
||||
storage: storage,
|
||||
fetcher: nil // TODO: Add fetcher in Phase 2
|
||||
)
|
||||
|
||||
if scheduled {
|
||||
print("\(Self.TAG): Successfully rolled over delivered notification id=\(notificationId)")
|
||||
// Update last processed time
|
||||
storage.saveLastRolloverTime(scheduledTime)
|
||||
} else {
|
||||
print("\(Self.TAG): Failed to roll over delivered notification id=\(notificationId)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Integration Points
|
||||
|
||||
### 1. AppDelegate → Plugin (Notification Center Pattern)
|
||||
- **Flow**: AppDelegate detects notification → posts Notification Center event → plugin observes and handles
|
||||
- **Challenge**: Decoupling AppDelegate from plugin
|
||||
- **Solution**: Use Notification Center for decoupled communication
|
||||
|
||||
### 2. Plugin → Scheduler
|
||||
- **Flow**: Plugin receives rollover request → calls scheduler method
|
||||
- **Challenge**: Passing storage and fetcher instances
|
||||
- **Solution**: Plugin maintains references, passes to scheduler
|
||||
|
||||
### 3. Scheduler → Storage
|
||||
- **Flow**: Scheduler checks duplicates → queries storage
|
||||
- **Challenge**: Thread safety
|
||||
- **Solution**: Storage methods are already thread-safe (UserDefaults)
|
||||
|
||||
### 4. Recovery Manager → Scheduler
|
||||
- **Flow**: Recovery detects delivered notifications → triggers rollover
|
||||
- **Challenge**: Ensuring rollover happens on app launch
|
||||
- **Solution**: Integrate into existing recovery flow
|
||||
|
||||
---
|
||||
|
||||
## Dependencies & Order
|
||||
|
||||
### Implementation Order
|
||||
|
||||
1. **Phase 1: Core Infrastructure**
|
||||
- ✅ Add `calculateNextScheduledTime` to Scheduler
|
||||
- ✅ Add `scheduleNextNotification` to Scheduler
|
||||
- ✅ Add rollover state tracking to Storage
|
||||
- ✅ Add `handleNotificationRollover` to Plugin
|
||||
|
||||
2. **Phase 2: Detection Mechanisms**
|
||||
- ✅ Modify AppDelegate `willPresent` method
|
||||
- ✅ Add rollover check to Recovery Manager
|
||||
- ✅ Test foreground delivery
|
||||
|
||||
3. **Phase 3: Edge Case Handling** (Future)
|
||||
- Add TimeChangeDetector
|
||||
- Add TimezoneChangeDetector
|
||||
- Add RolloverCoordinator
|
||||
|
||||
4. **Phase 4: Integration** (Future)
|
||||
- Integrate fetcher for prefetch scheduling
|
||||
- Add comprehensive logging
|
||||
- Performance optimization
|
||||
|
||||
---
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Test 1: Foreground Delivery
|
||||
- **Setup**: App running, notification fires
|
||||
- **Expected**: Rollover triggers via AppDelegate → Notification Center → Plugin
|
||||
- **Verify**: Next notification scheduled, logs show rollover success
|
||||
|
||||
### Test 2: Background Delivery
|
||||
- **Setup**: App not running, notification fires
|
||||
- **Expected**: Rollover triggers on app launch via Recovery Manager
|
||||
- **Verify**: Next notification scheduled, recovery logs show rollover
|
||||
|
||||
### Test 3: Duplicate Prevention
|
||||
- **Setup**: Trigger rollover multiple times (rapid fire)
|
||||
- **Expected**: Only one notification scheduled
|
||||
- **Verify**: No duplicates in system, logs show duplicate prevention
|
||||
|
||||
### Test 4: DST Transition
|
||||
- **Setup**: Schedule notification on DST transition day
|
||||
- **Expected**: 24-hour calculation handles DST correctly
|
||||
- **Verify**: Notification fires at correct time, logs show DST transition
|
||||
|
||||
### Test 5: Error Handling
|
||||
- **Setup**: Simulate failure (e.g., invalid notification ID)
|
||||
- **Expected**: Error logged, app continues, no crash
|
||||
- **Verify**: Logs show error, recovery handles on next launch
|
||||
|
||||
---
|
||||
|
||||
## Open Questions — RESOLVED
|
||||
|
||||
**See**: `docs/ios-rollover-open-questions-answers.md` for detailed answers
|
||||
|
||||
### Summary of Decisions:
|
||||
|
||||
1. **Fetcher Integration**: ✅ Defer to Phase 2, use optional parameter pattern
|
||||
2. **AppDelegate Access**: ✅ Use Notification Center pattern (decoupling, flexibility)
|
||||
3. **Background Task**: ✅ Rely on existing recovery + AppDelegate (no dedicated task)
|
||||
4. **Error Handling**: ✅ Log + Continue (non-fatal), no retry, no user notification
|
||||
5. **Performance**: ✅ Process individually (low volume, simplicity)
|
||||
6. **Testing**: ✅ Manual testing for Phase 1, automated tests for Phase 2
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Review this document** ✅ (Current step)
|
||||
2. **Address open questions**
|
||||
3. **Create implementation tasks**
|
||||
4. **Implement Phase 1** (Core rollover)
|
||||
5. **Test Phase 1**
|
||||
6. **Implement Phase 2** (Edge case detection)
|
||||
7. **Final testing and validation**
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- Edge Case Plan: `docs/ios-rollover-edge-case-plan.md`
|
||||
- Android Implementation: `android/src/main/java/com/timesafari/dailynotification/DailyNotificationWorker.java`
|
||||
- iOS Scheduler: `ios/Plugin/DailyNotificationScheduler.swift`
|
||||
- iOS Plugin: `ios/Plugin/DailyNotificationPlugin.swift`
|
||||
Reference in New Issue
Block a user