fix(ios): correct next notification time and improve rollover UI refresh

- Fix getNextNotificationTime() to find earliest scheduled notification
  instead of using first request (pendingNotificationRequests doesn't
  guarantee order)
- Add comprehensive logging for rollover tracking with DNP-ROLLOVER
  prefix for Xcode console filtering
- Reset all notifications and rollover state when scheduling new
  notification via scheduleDailyNotification() to ensure clean test
  state
- Fix userInfo scope error in handleNotificationDelivery error handler
- Update test app UI to refresh status every 5-10 seconds and
  immediately after notification delivery to reflect rollover changes
- Add console logging in UI to debug getNotificationStatus() results

This ensures the UI correctly displays the next notification time after
rollover completes, and test notifications start with a clean slate.
This commit is contained in:
Jose Olarte III
2025-12-15 21:42:48 +08:00
parent 527c075941
commit 0a2cbf24f7
12 changed files with 2281 additions and 12 deletions

View File

@@ -393,6 +393,10 @@ class DailyNotificationReactivationManager {
}
}
// Step 4.5: Check for delivered notifications and trigger rollover
// This handles notifications that were delivered while app was not running
await checkAndProcessDeliveredNotifications()
// Record recovery in history
let result = RecoveryResult(
missedCount: missedCount,
@@ -1004,6 +1008,94 @@ class DailyNotificationReactivationManager {
// Don't throw - this is best effort
}
}
/**
* 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 {
NSLog("DNP-ROLLOVER: RECOVERY_CHECK_START")
print("DNP-ROLLOVER: RECOVERY_CHECK_START")
// Get delivered notifications from system
let deliveredNotifications = await notificationCenter.deliveredNotifications()
NSLog("DNP-ROLLOVER: RECOVERY_FOUND delivered_count=\(deliveredNotifications.count)")
print("DNP-ROLLOVER: RECOVERY_FOUND delivered_count=\(deliveredNotifications.count)")
// Get last processed rollover time from storage
let lastProcessedTime = storage.getLastRolloverTime()
let lastProcessedTimeStr = formatTime(lastProcessedTime)
NSLog("DNP-ROLLOVER: RECOVERY_LAST_PROCESSED time=\(lastProcessedTimeStr)")
print("DNP-ROLLOVER: RECOVERY_LAST_PROCESSED time=\(lastProcessedTimeStr)")
var processedCount = 0
var skippedCount = 0
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
}
let scheduledTimeStr = formatTime(scheduledTime)
// Only process if this notification hasn't been processed yet
if scheduledTime > lastProcessedTime {
NSLog("DNP-ROLLOVER: RECOVERY_PROCESS id=\(notificationId) scheduled_time=\(scheduledTimeStr)")
print("DNP-ROLLOVER: RECOVERY_PROCESS id=\(notificationId) scheduled_time=\(scheduledTimeStr)")
// Get notification content
guard let content = storage.getNotificationContent(id: notificationId) else {
NSLog("DNP-ROLLOVER: RECOVERY_ERROR id=\(notificationId) content_not_found")
print("DNP-ROLLOVER: RECOVERY_ERROR id=\(notificationId) content_not_found")
continue
}
// Trigger rollover
let scheduled = await scheduler.scheduleNextNotification(
content,
storage: storage,
fetcher: nil // TODO: Phase 2 - Add fetcher
)
if scheduled {
NSLog("DNP-ROLLOVER: RECOVERY_SUCCESS id=\(notificationId)")
print("DNP-ROLLOVER: RECOVERY_SUCCESS id=\(notificationId)")
// Update last processed time
storage.saveLastRolloverTime(scheduledTime)
processedCount += 1
} else {
NSLog("DNP-ROLLOVER: RECOVERY_FAILED id=\(notificationId)")
print("DNP-ROLLOVER: RECOVERY_FAILED id=\(notificationId)")
}
} else {
skippedCount += 1
NSLog("DNP-ROLLOVER: RECOVERY_SKIP id=\(notificationId) already_processed scheduled_time=\(scheduledTimeStr)")
print("DNP-ROLLOVER: RECOVERY_SKIP id=\(notificationId) already_processed scheduled_time=\(scheduledTimeStr)")
}
}
NSLog("DNP-ROLLOVER: RECOVERY_COMPLETE processed=\(processedCount) skipped=\(skippedCount)")
print("DNP-ROLLOVER: RECOVERY_COMPLETE processed=\(processedCount) skipped=\(skippedCount)")
}
/**
* Format time for logging
*
* @param timestamp Timestamp in milliseconds
* @return Formatted time string
*/
private func formatTime(_ timestamp: Int64) -> String {
let date = Date(timeIntervalSince1970: Double(timestamp) / 1000.0)
let formatter = DateFormatter()
formatter.dateStyle = .medium
formatter.timeStyle = .short
return formatter.string(from: date)
}
}
// MARK: - Supporting Types