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
+132
View File
@@ -72,6 +72,14 @@ public class DailyNotificationPlugin: CAPPlugin {
// Perform recovery on app launch (async, non-blocking)
reactivationManager?.performRecovery()
// Register for notification delivery events (Notification Center pattern)
NotificationCenter.default.addObserver(
self,
selector: #selector(handleNotificationDelivery(_:)),
name: NSNotification.Name("DailyNotificationDelivered"),
object: nil
)
NSLog("DNP-DEBUG: DailyNotificationPlugin.load() completed - initialization done")
print("DNP-PLUGIN: Daily Notification Plugin loaded on iOS")
}
@@ -1068,6 +1076,38 @@ public class DailyNotificationPlugin: CAPPlugin {
// Store notification content via state actor (thread-safe)
Task {
// Reset: Cancel all existing notifications and clear rollover state
// This ensures clicking "Test Notification" starts fresh
// Cancel all pending notifications (including rollovers)
await scheduler.cancelAllNotifications()
NSLog("DNP-PLUGIN: Cleared all pending notifications for fresh schedule")
print("DNP-PLUGIN: Cleared all pending notifications for fresh schedule")
// Clear all stored notification content
if let storage = self.storage {
storage.clearAllNotifications()
NSLog("DNP-PLUGIN: Cleared all stored notification content")
print("DNP-PLUGIN: Cleared all stored notification content")
}
// Clear rollover state from UserDefaults
// Clear global rollover time
if let storage = self.storage {
storage.saveLastRolloverTime(0)
}
// Clear per-notification rollover times
// We need to clear all rollover_* keys from UserDefaults
let userDefaults = UserDefaults.standard
let allKeys = userDefaults.dictionaryRepresentation().keys
for key in allKeys {
if key.hasPrefix("rollover_") {
userDefaults.removeObject(forKey: key)
}
}
userDefaults.synchronize()
NSLog("DNP-PLUGIN: Cleared all rollover state")
print("DNP-PLUGIN: Cleared all rollover state")
if #available(iOS 13.0, *) {
if let stateActor = await self.stateActor {
await stateActor.saveNotificationContent(content)
@@ -1226,12 +1266,17 @@ public class DailyNotificationPlugin: CAPPlugin {
// Calculate next notification time
let nextNotificationTime = await scheduler.getNextNotificationTime() ?? 0
// Get rollover status
let lastRolloverTime = 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
]
@@ -1241,6 +1286,93 @@ public class DailyNotificationPlugin: CAPPlugin {
}
}
/**
* 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 {
NSLog("DNP-ROLLOVER: INVALID_DATA userInfo=\(String(describing: notification.userInfo))")
print("DNP-ROLLOVER: INVALID_DATA userInfo=\(String(describing: notification.userInfo))")
return
}
let scheduledTimeStr = formatTime(scheduledTime)
NSLog("DNP-ROLLOVER: DELIVERY_DETECTED id=\(notificationId) scheduled_time=\(scheduledTimeStr)")
print("DNP-ROLLOVER: DELIVERY_DETECTED id=\(notificationId) scheduled_time=\(scheduledTimeStr)")
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 {
let scheduledTimeStr = formatTime(scheduledTime)
NSLog("DNP-ROLLOVER: PROCESS_START id=\(notificationId) scheduled_time=\(scheduledTimeStr)")
print("DNP-ROLLOVER: PROCESS_START id=\(notificationId) scheduled_time=\(scheduledTimeStr)")
guard let scheduler = scheduler, let storage = storage else {
NSLog("DNP-ROLLOVER: ERROR id=\(notificationId) plugin_not_initialized")
print("DNP-ROLLOVER: ERROR id=\(notificationId) plugin_not_initialized")
return
}
// Get the notification content that was delivered
guard let content = storage.getNotificationContent(id: notificationId) else {
NSLog("DNP-ROLLOVER: ERROR id=\(notificationId) content_not_found")
print("DNP-ROLLOVER: ERROR id=\(notificationId) content_not_found")
return
}
let contentTimeStr = formatTime(content.scheduledTime)
NSLog("DNP-ROLLOVER: CONTENT_FOUND id=\(notificationId) content_scheduled_time=\(contentTimeStr)")
print("DNP-ROLLOVER: CONTENT_FOUND id=\(notificationId) content_scheduled_time=\(contentTimeStr)")
// 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 {
NSLog("DNP-ROLLOVER: PROCESS_SUCCESS id=\(notificationId) next_notification_scheduled")
print("DNP-ROLLOVER: PROCESS_SUCCESS id=\(notificationId) next_notification_scheduled")
// Log success (non-fatal, background operation)
} else {
NSLog("DNP-ROLLOVER: PROCESS_FAILED id=\(notificationId) next_notification_not_scheduled")
print("DNP-ROLLOVER: PROCESS_FAILED id=\(notificationId) next_notification_not_scheduled")
// Log failure but continue (recovery will handle on next launch)
}
}
/**
* 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)
}
/**
* Check permission status
* Returns boolean flags for each permission type