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:
@@ -32,7 +32,9 @@ public class DailyNotificationPlugin: CAPPlugin {
|
|||||||
// Background task identifiers
|
// Background task identifiers
|
||||||
private let fetchTaskIdentifier = "org.timesafari.dailynotification.fetch"
|
private let fetchTaskIdentifier = "org.timesafari.dailynotification.fetch"
|
||||||
private let notifyTaskIdentifier = "org.timesafari.dailynotification.notify"
|
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"
|
private let dualNotificationRequestIdentifier = "org.timesafari.dailynotification.dual"
|
||||||
/// UserDefaults key for persisted dual schedule config (userNotification + relationship) for relationship resolution when fetch completes.
|
/// UserDefaults key for persisted dual schedule config (userNotification + relationship) for relationship resolution when fetch completes.
|
||||||
private let dualScheduleConfigKey = "dual_schedule_config"
|
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) {
|
@objc func cancelDualSchedule(_ call: CAPPluginCall) {
|
||||||
do {
|
do {
|
||||||
performCancelDualSchedule()
|
performCancelDualSchedule()
|
||||||
@@ -1083,6 +1085,28 @@ public class DailyNotificationPlugin: CAPPlugin {
|
|||||||
return interval > 0 ? interval : (24 * 60 * 60)
|
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
|
// MARK: - Static Daily Reminder Methods
|
||||||
|
|
||||||
@objc func scheduleDailyReminder(_ call: CAPPluginCall) {
|
@objc func scheduleDailyReminder(_ call: CAPPluginCall) {
|
||||||
@@ -1140,8 +1164,9 @@ public class DailyNotificationPlugin: CAPPlugin {
|
|||||||
repeats: repeatDaily
|
repeats: repeatDaily
|
||||||
)
|
)
|
||||||
|
|
||||||
|
let requestId = predictiveReminderRequestId(hour: hour, minute: minute)
|
||||||
let request = UNNotificationRequest(
|
let request = UNNotificationRequest(
|
||||||
identifier: "reminder_\(id)",
|
identifier: requestId,
|
||||||
content: content,
|
content: content,
|
||||||
trigger: trigger
|
trigger: trigger
|
||||||
)
|
)
|
||||||
@@ -1177,8 +1202,13 @@ public class DailyNotificationPlugin: CAPPlugin {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cancel the notification
|
// Cancel the notification (deterministic id from stored HH:mm)
|
||||||
notificationCenter.removePendingNotificationRequests(withIdentifiers: ["reminder_\(reminderId)"])
|
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
|
// Delegate to storage for reminder removal
|
||||||
removeReminderFromUserDefaults(id: reminderId)
|
removeReminderFromUserDefaults(id: reminderId)
|
||||||
@@ -1191,14 +1221,18 @@ public class DailyNotificationPlugin: CAPPlugin {
|
|||||||
|
|
||||||
// Get pending notifications
|
// Get pending notifications
|
||||||
notificationCenter.getPendingNotificationRequests { requests in
|
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
|
// Get stored reminder data from UserDefaults
|
||||||
let reminders = self.getRemindersFromUserDefaults()
|
let reminders = self.getRemindersFromUserDefaults()
|
||||||
|
|
||||||
var result: [[String: Any]] = []
|
var result: [[String: Any]] = []
|
||||||
for reminder in reminders {
|
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
|
var reminderInfo = reminder
|
||||||
reminderInfo["isScheduled"] = isScheduled
|
reminderInfo["isScheduled"] = isScheduled
|
||||||
result.append(reminderInfo)
|
result.append(reminderInfo)
|
||||||
@@ -1216,8 +1250,13 @@ public class DailyNotificationPlugin: CAPPlugin {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cancel existing reminder
|
// Cancel existing reminder (slot before UserDefaults update)
|
||||||
notificationCenter.removePendingNotificationRequests(withIdentifiers: ["reminder_\(reminderId)"])
|
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
|
// Update in UserDefaults
|
||||||
let title = call.getString("title")
|
let title = call.getString("title")
|
||||||
@@ -1282,8 +1321,9 @@ public class DailyNotificationPlugin: CAPPlugin {
|
|||||||
repeats: repeatDaily ?? true
|
repeats: repeatDaily ?? true
|
||||||
)
|
)
|
||||||
|
|
||||||
|
let requestId = predictiveReminderRequestId(hour: hour, minute: minute)
|
||||||
let request = UNNotificationRequest(
|
let request = UNNotificationRequest(
|
||||||
identifier: "reminder_\(reminderId)",
|
identifier: requestId,
|
||||||
content: content,
|
content: content,
|
||||||
trigger: trigger
|
trigger: trigger
|
||||||
)
|
)
|
||||||
@@ -1540,10 +1580,11 @@ public class DailyNotificationPlugin: CAPPlugin {
|
|||||||
// Create time interval trigger (fires in X seconds)
|
// Create time interval trigger (fires in X seconds)
|
||||||
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: TimeInterval(validSeconds), repeats: false)
|
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: TimeInterval(validSeconds), repeats: false)
|
||||||
|
|
||||||
// Create notification request with unique ID
|
// Deterministic id from scheduled fire time (Unix seconds) so repeat scheduling replaces instead of stacking
|
||||||
let notificationId = "test_alarm_\(Date().timeIntervalSince1970)"
|
let timestamp = Int(Date().timeIntervalSince1970) + validSeconds
|
||||||
|
let requestId = "\(predictiveNotificationPrefix)\(timestamp)"
|
||||||
let request = UNNotificationRequest(
|
let request = UNNotificationRequest(
|
||||||
identifier: notificationId,
|
identifier: requestId,
|
||||||
content: notificationContent,
|
content: notificationContent,
|
||||||
trigger: trigger
|
trigger: trigger
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user