fix(ios): use deterministic predictive notification identifiers

Replace reminder_* and random test alarm IDs with predictive_<Int>
so UNUserNotificationCenter replaces pending requests instead of
stacking. Daily reminders key off local HH:mm; testAlarm uses the
scheduled fire second. Dual notification id unchanged.
This commit is contained in:
Jose Olarte III
2026-05-05 14:53:06 +08:00
parent a5395082f6
commit 26f1ff0e08

View File

@@ -32,7 +32,9 @@ public class DailyNotificationPlugin: CAPPlugin {
// Background task identifiers
private let fetchTaskIdentifier = "org.timesafari.dailynotification.fetch"
private let notifyTaskIdentifier = "org.timesafari.dailynotification.notify"
/// Stable identifier for the dual (New Activity) user notification. Used for replace and cancelDualSchedule; must not conflict with Daily Reminder (`reminder_<id>`).
/// Prefix for deterministic UNNotificationRequest identifiers built as `predictive_\(Int(timestamp))` so rescheduling the same slot replaces instead of stacking.
private let predictiveNotificationPrefix = "predictive_"
/// Stable identifier for the dual (New Activity) user notification. Used for replace and cancelDualSchedule; must not conflict with Daily Reminder (`predictive_*`).
private let dualNotificationRequestIdentifier = "org.timesafari.dailynotification.dual"
/// UserDefaults key for persisted dual schedule config (userNotification + relationship) for relationship resolution when fetch completes.
private let dualScheduleConfigKey = "dual_schedule_config"
@@ -428,7 +430,7 @@ public class DailyNotificationPlugin: CAPPlugin {
}
}
/// Cancel the dual (New Activity) schedule only. Does not affect Daily Reminder (reminder_*).
/// Cancel the dual (New Activity) schedule only. Does not affect Daily Reminder (`predictive_*`).
@objc func cancelDualSchedule(_ call: CAPPluginCall) {
do {
performCancelDualSchedule()
@@ -1083,6 +1085,28 @@ public class DailyNotificationPlugin: CAPPlugin {
return interval > 0 ? interval : (24 * 60 * 60)
}
/// Seconds since local midnight for the daily reminder slot; stable for the same HH:mm so re-schedules overwrite.
private func predictiveReminderTimestamp(hour: Int, minute: Int) -> Int {
hour * 3600 + minute * 60
}
private func predictiveReminderRequestId(hour: Int, minute: Int) -> String {
let timestamp = predictiveReminderTimestamp(hour: hour, minute: minute)
return "\(predictiveNotificationPrefix)\(timestamp)"
}
private func predictiveReminderRequestId(fromTimeHHmm time: String) -> String? {
let timeComponents = time.components(separatedBy: ":")
guard timeComponents.count == 2,
let hour = Int(timeComponents[0]),
let minute = Int(timeComponents[1]),
hour >= 0, hour <= 23,
minute >= 0, minute <= 59 else {
return nil
}
return predictiveReminderRequestId(hour: hour, minute: minute)
}
// MARK: - Static Daily Reminder Methods
@objc func scheduleDailyReminder(_ call: CAPPluginCall) {
@@ -1140,8 +1164,9 @@ public class DailyNotificationPlugin: CAPPlugin {
repeats: repeatDaily
)
let requestId = predictiveReminderRequestId(hour: hour, minute: minute)
let request = UNNotificationRequest(
identifier: "reminder_\(id)",
identifier: requestId,
content: content,
trigger: trigger
)
@@ -1177,8 +1202,13 @@ public class DailyNotificationPlugin: CAPPlugin {
return
}
// Cancel the notification
notificationCenter.removePendingNotificationRequests(withIdentifiers: ["reminder_\(reminderId)"])
// Cancel the notification (deterministic id from stored HH:mm)
let reminders = getRemindersFromUserDefaults()
if let stored = reminders.first(where: { ($0["id"] as? String) == reminderId }),
let time = stored["time"] as? String,
let requestId = predictiveReminderRequestId(fromTimeHHmm: time) {
notificationCenter.removePendingNotificationRequests(withIdentifiers: [requestId])
}
// Delegate to storage for reminder removal
removeReminderFromUserDefaults(id: reminderId)
@@ -1191,14 +1221,18 @@ public class DailyNotificationPlugin: CAPPlugin {
// Get pending notifications
notificationCenter.getPendingNotificationRequests { requests in
let reminderRequests = requests.filter { $0.identifier.hasPrefix("reminder_") }
let reminderRequests = requests.filter { $0.identifier.hasPrefix(self.predictiveNotificationPrefix) }
// Get stored reminder data from UserDefaults
let reminders = self.getRemindersFromUserDefaults()
var result: [[String: Any]] = []
for reminder in reminders {
let isScheduled = reminderRequests.contains { $0.identifier == "reminder_\(reminder["id"] as! String)" }
let expectedId: String? = {
guard let time = reminder["time"] as? String else { return nil }
return self.predictiveReminderRequestId(fromTimeHHmm: time)
}()
let isScheduled = reminderRequests.contains { expectedId != nil && $0.identifier == expectedId }
var reminderInfo = reminder
reminderInfo["isScheduled"] = isScheduled
result.append(reminderInfo)
@@ -1216,8 +1250,13 @@ public class DailyNotificationPlugin: CAPPlugin {
return
}
// Cancel existing reminder
notificationCenter.removePendingNotificationRequests(withIdentifiers: ["reminder_\(reminderId)"])
// Cancel existing reminder (slot before UserDefaults update)
let remindersBeforeUpdate = getRemindersFromUserDefaults()
if let stored = remindersBeforeUpdate.first(where: { ($0["id"] as? String) == reminderId }),
let oldTime = stored["time"] as? String,
let oldRequestId = predictiveReminderRequestId(fromTimeHHmm: oldTime) {
notificationCenter.removePendingNotificationRequests(withIdentifiers: [oldRequestId])
}
// Update in UserDefaults
let title = call.getString("title")
@@ -1282,8 +1321,9 @@ public class DailyNotificationPlugin: CAPPlugin {
repeats: repeatDaily ?? true
)
let requestId = predictiveReminderRequestId(hour: hour, minute: minute)
let request = UNNotificationRequest(
identifier: "reminder_\(reminderId)",
identifier: requestId,
content: content,
trigger: trigger
)
@@ -1540,10 +1580,11 @@ public class DailyNotificationPlugin: CAPPlugin {
// Create time interval trigger (fires in X seconds)
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: TimeInterval(validSeconds), repeats: false)
// Create notification request with unique ID
let notificationId = "test_alarm_\(Date().timeIntervalSince1970)"
// Deterministic id from scheduled fire time (Unix seconds) so repeat scheduling replaces instead of stacking
let timestamp = Int(Date().timeIntervalSince1970) + validSeconds
let requestId = "\(predictiveNotificationPrefix)\(timestamp)"
let request = UNNotificationRequest(
identifier: notificationId,
identifier: requestId,
content: notificationContent,
trigger: trigger
)