|
|
|
|
@@ -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: `predictive_\(Int(timestamp))` with `timestamp` in epoch milliseconds.
|
|
|
|
|
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,125 @@ public class DailyNotificationPlugin: CAPPlugin {
|
|
|
|
|
return interval > 0 ? interval : (24 * 60 * 60)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Epoch milliseconds for the next local occurrence of `hour`:`minute` (used only to build predictive IDs from HH:mm schedules).
|
|
|
|
|
private func epochMillisNextDailyOccurrence(hour: Int, minute: Int) -> Int64 {
|
|
|
|
|
var comp = DateComponents()
|
|
|
|
|
comp.hour = hour
|
|
|
|
|
comp.minute = minute
|
|
|
|
|
comp.second = 0
|
|
|
|
|
guard let next = Calendar.current.nextDate(after: Date(), matching: comp, matchingPolicy: .nextTime) else {
|
|
|
|
|
return Int64(Date().timeIntervalSince1970 * 1000)
|
|
|
|
|
}
|
|
|
|
|
return Int64(next.timeIntervalSince1970 * 1000)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func predictiveNotificationId(epochMillis: Int64) -> String {
|
|
|
|
|
"\(predictiveNotificationPrefix)\(epochMillis)"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Reads persisted millis for a reminder, or derives from stored `time` (HH:mm) for legacy rows.
|
|
|
|
|
private func predictiveEpochMillis(from reminder: [String: Any]) -> Int64? {
|
|
|
|
|
if let n = reminder["predictiveEpochMillis"] as? NSNumber {
|
|
|
|
|
return n.int64Value
|
|
|
|
|
}
|
|
|
|
|
guard let time = reminder["time"] as? String else { return nil }
|
|
|
|
|
let parts = time.components(separatedBy: ":")
|
|
|
|
|
guard parts.count == 2,
|
|
|
|
|
let hour = Int(parts[0]),
|
|
|
|
|
let minute = Int(parts[1]),
|
|
|
|
|
hour >= 0, hour <= 23,
|
|
|
|
|
minute >= 0, minute <= 59 else {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return epochMillisNextDailyOccurrence(hour: hour, minute: minute)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: - Predictive batch API (replace-all UNUserNotificationCenter)
|
|
|
|
|
|
|
|
|
|
/// Removes only pending and delivered notifications whose identifiers begin with `predictive_`. Does not touch dual/org IDs or other stacks.
|
|
|
|
|
@objc func clearAllNotifications(_ call: CAPPluginCall) {
|
|
|
|
|
NSLog("DNP-BATCH: clearAllNotifications — removing predictive_* pending and delivered only")
|
|
|
|
|
print("DNP-BATCH: clearAllNotifications — removing predictive_* pending and delivered only")
|
|
|
|
|
let center = notificationCenter
|
|
|
|
|
let prefix = predictiveNotificationPrefix
|
|
|
|
|
let group = DispatchGroup()
|
|
|
|
|
|
|
|
|
|
group.enter()
|
|
|
|
|
center.getPendingNotificationRequests { requests in
|
|
|
|
|
let ids = requests.map { $0.identifier }.filter { $0.hasPrefix(prefix) }
|
|
|
|
|
center.removePendingNotificationRequests(withIdentifiers: ids)
|
|
|
|
|
NSLog("DNP-BATCH: cleared \(ids.count) pending predictive id(s)")
|
|
|
|
|
print("DNP-BATCH: cleared \(ids.count) pending predictive id(s)")
|
|
|
|
|
group.leave()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
group.enter()
|
|
|
|
|
center.getDeliveredNotifications { notifications in
|
|
|
|
|
let ids = notifications.map { $0.request.identifier }.filter { $0.hasPrefix(prefix) }
|
|
|
|
|
center.removeDeliveredNotifications(withIdentifiers: ids)
|
|
|
|
|
NSLog("DNP-BATCH: cleared \(ids.count) delivered predictive id(s)")
|
|
|
|
|
print("DNP-BATCH: cleared \(ids.count) delivered predictive id(s)")
|
|
|
|
|
group.leave()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
group.notify(queue: .main) {
|
|
|
|
|
call.resolve()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Add one-shot reminders at epoch-ms timestamps. Does not remove other requests; identical IDs replace pending entries. Caller should clear first if needed.
|
|
|
|
|
/// Adds/overwrites predictive notifications using deterministic IDs.
|
|
|
|
|
/// Does NOT clear existing notifications. Caller is responsible for lifecycle.
|
|
|
|
|
@objc func scheduleNotifications(_ call: CAPPluginCall) {
|
|
|
|
|
guard let timestamps = call.getArray("timestamps", Double.self) else {
|
|
|
|
|
call.reject("Missing timestamps")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
NSLog("DNP-BATCH: scheduleNotifications — additive scheduling for \(timestamps.count) timestamp(s)")
|
|
|
|
|
print("DNP-BATCH: scheduleNotifications — additive scheduling for \(timestamps.count) timestamp(s)")
|
|
|
|
|
|
|
|
|
|
for ts in timestamps {
|
|
|
|
|
let date = Date(timeIntervalSince1970: ts / 1000)
|
|
|
|
|
guard date.timeIntervalSinceNow > 0 else {
|
|
|
|
|
NSLog("DNP-BATCH: skip stale timestamp ts=\(ts) (not in the future)")
|
|
|
|
|
print("DNP-BATCH: skip stale timestamp ts=\(ts) (not in the future)")
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let interval = date.timeIntervalSinceNow
|
|
|
|
|
|
|
|
|
|
let trigger = UNTimeIntervalNotificationTrigger(
|
|
|
|
|
timeInterval: interval,
|
|
|
|
|
repeats: false
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
let content = UNMutableNotificationContent()
|
|
|
|
|
content.title = "Reminder"
|
|
|
|
|
content.body = "You have a scheduled notification"
|
|
|
|
|
|
|
|
|
|
let id = "\(predictiveNotificationPrefix)\(Int(ts))"
|
|
|
|
|
|
|
|
|
|
NSLog("DNP-BATCH: scheduling ts=\(ts) interval=\(interval)s id=\(id)")
|
|
|
|
|
print("DNP-BATCH: scheduling ts=\(ts) interval=\(interval)s id=\(id)")
|
|
|
|
|
|
|
|
|
|
let request = UNNotificationRequest(
|
|
|
|
|
identifier: id,
|
|
|
|
|
content: content,
|
|
|
|
|
trigger: trigger
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
notificationCenter.add(request) { error in
|
|
|
|
|
if let error = error {
|
|
|
|
|
NSLog("DNP-BATCH: add failed id=\(id) error=\(error.localizedDescription)")
|
|
|
|
|
print("DNP-BATCH: add failed id=\(id) error=\(error.localizedDescription)")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
call.resolve()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: - Static Daily Reminder Methods
|
|
|
|
|
|
|
|
|
|
@objc func scheduleDailyReminder(_ call: CAPPluginCall) {
|
|
|
|
|
@@ -1140,8 +1261,11 @@ public class DailyNotificationPlugin: CAPPlugin {
|
|
|
|
|
repeats: repeatDaily
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
let epochMillis = epochMillisNextDailyOccurrence(hour: hour, minute: minute)
|
|
|
|
|
let requestId = predictiveNotificationId(epochMillis: epochMillis)
|
|
|
|
|
|
|
|
|
|
let request = UNNotificationRequest(
|
|
|
|
|
identifier: "reminder_\(id)",
|
|
|
|
|
identifier: requestId,
|
|
|
|
|
content: content,
|
|
|
|
|
trigger: trigger
|
|
|
|
|
)
|
|
|
|
|
@@ -1156,7 +1280,8 @@ public class DailyNotificationPlugin: CAPPlugin {
|
|
|
|
|
vibration: vibration,
|
|
|
|
|
priority: priority,
|
|
|
|
|
repeatDaily: repeatDaily,
|
|
|
|
|
timezone: timezone
|
|
|
|
|
timezone: timezone,
|
|
|
|
|
predictiveEpochMillis: epochMillis
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Delegate to UNUserNotificationCenter to schedule notification
|
|
|
|
|
@@ -1177,8 +1302,13 @@ public class DailyNotificationPlugin: CAPPlugin {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Cancel the notification
|
|
|
|
|
notificationCenter.removePendingNotificationRequests(withIdentifiers: ["reminder_\(reminderId)"])
|
|
|
|
|
// Cancel the notification (ID from stored epoch millis)
|
|
|
|
|
let reminders = getRemindersFromUserDefaults()
|
|
|
|
|
if let stored = reminders.first(where: { ($0["id"] as? String) == reminderId }),
|
|
|
|
|
let epochMillis = predictiveEpochMillis(from: stored) {
|
|
|
|
|
let requestId = predictiveNotificationId(epochMillis: epochMillis)
|
|
|
|
|
notificationCenter.removePendingNotificationRequests(withIdentifiers: [requestId])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Delegate to storage for reminder removal
|
|
|
|
|
removeReminderFromUserDefaults(id: reminderId)
|
|
|
|
|
@@ -1191,14 +1321,17 @@ 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? = self.predictiveEpochMillis(from: reminder).map {
|
|
|
|
|
self.predictiveNotificationId(epochMillis: $0)
|
|
|
|
|
}
|
|
|
|
|
let isScheduled = reminderRequests.contains { expectedId != nil && $0.identifier == expectedId }
|
|
|
|
|
var reminderInfo = reminder
|
|
|
|
|
reminderInfo["isScheduled"] = isScheduled
|
|
|
|
|
result.append(reminderInfo)
|
|
|
|
|
@@ -1216,8 +1349,13 @@ public class DailyNotificationPlugin: CAPPlugin {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Cancel existing reminder
|
|
|
|
|
notificationCenter.removePendingNotificationRequests(withIdentifiers: ["reminder_\(reminderId)"])
|
|
|
|
|
// Cancel existing reminder (before UserDefaults update)
|
|
|
|
|
let remindersBeforeUpdate = getRemindersFromUserDefaults()
|
|
|
|
|
if let stored = remindersBeforeUpdate.first(where: { ($0["id"] as? String) == reminderId }),
|
|
|
|
|
let epochMillis = predictiveEpochMillis(from: stored) {
|
|
|
|
|
let oldRequestId = predictiveNotificationId(epochMillis: epochMillis)
|
|
|
|
|
notificationCenter.removePendingNotificationRequests(withIdentifiers: [oldRequestId])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update in UserDefaults
|
|
|
|
|
let title = call.getString("title")
|
|
|
|
|
@@ -1282,8 +1420,11 @@ public class DailyNotificationPlugin: CAPPlugin {
|
|
|
|
|
repeats: repeatDaily ?? true
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
let epochMillis = epochMillisNextDailyOccurrence(hour: hour, minute: minute)
|
|
|
|
|
let requestId = predictiveNotificationId(epochMillis: epochMillis)
|
|
|
|
|
|
|
|
|
|
let request = UNNotificationRequest(
|
|
|
|
|
identifier: "reminder_\(reminderId)",
|
|
|
|
|
identifier: requestId,
|
|
|
|
|
content: content,
|
|
|
|
|
trigger: trigger
|
|
|
|
|
)
|
|
|
|
|
@@ -1294,6 +1435,18 @@ public class DailyNotificationPlugin: CAPPlugin {
|
|
|
|
|
if let error = error {
|
|
|
|
|
call.reject("Failed to reschedule updated reminder: \(error.localizedDescription)")
|
|
|
|
|
} else {
|
|
|
|
|
self.updateReminderInUserDefaults(
|
|
|
|
|
id: reminderId,
|
|
|
|
|
title: nil,
|
|
|
|
|
body: nil,
|
|
|
|
|
time: nil,
|
|
|
|
|
sound: nil,
|
|
|
|
|
vibration: nil,
|
|
|
|
|
priority: nil,
|
|
|
|
|
repeatDaily: nil,
|
|
|
|
|
timezone: nil,
|
|
|
|
|
predictiveEpochMillis: epochMillis
|
|
|
|
|
)
|
|
|
|
|
call.resolve()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
@@ -1314,7 +1467,8 @@ public class DailyNotificationPlugin: CAPPlugin {
|
|
|
|
|
vibration: Bool,
|
|
|
|
|
priority: String,
|
|
|
|
|
repeatDaily: Bool,
|
|
|
|
|
timezone: String?
|
|
|
|
|
timezone: String?,
|
|
|
|
|
predictiveEpochMillis: Int64
|
|
|
|
|
) {
|
|
|
|
|
let reminderData: [String: Any] = [
|
|
|
|
|
"id": id,
|
|
|
|
|
@@ -1326,6 +1480,7 @@ public class DailyNotificationPlugin: CAPPlugin {
|
|
|
|
|
"priority": priority,
|
|
|
|
|
"repeatDaily": repeatDaily,
|
|
|
|
|
"timezone": timezone ?? "",
|
|
|
|
|
"predictiveEpochMillis": predictiveEpochMillis,
|
|
|
|
|
"createdAt": Date().timeIntervalSince1970,
|
|
|
|
|
"lastTriggered": 0
|
|
|
|
|
]
|
|
|
|
|
@@ -1358,7 +1513,8 @@ public class DailyNotificationPlugin: CAPPlugin {
|
|
|
|
|
vibration: Bool?,
|
|
|
|
|
priority: String?,
|
|
|
|
|
repeatDaily: Bool?,
|
|
|
|
|
timezone: String?
|
|
|
|
|
timezone: String?,
|
|
|
|
|
predictiveEpochMillis: Int64? = nil
|
|
|
|
|
) {
|
|
|
|
|
var reminders = UserDefaults.standard.array(forKey: "daily_reminders") as? [[String: Any]] ?? []
|
|
|
|
|
|
|
|
|
|
@@ -1372,6 +1528,9 @@ public class DailyNotificationPlugin: CAPPlugin {
|
|
|
|
|
if let priority = priority { reminders[i]["priority"] = priority }
|
|
|
|
|
if let repeatDaily = repeatDaily { reminders[i]["repeatDaily"] = repeatDaily }
|
|
|
|
|
if let timezone = timezone { reminders[i]["timezone"] = timezone }
|
|
|
|
|
if let predictiveEpochMillis = predictiveEpochMillis {
|
|
|
|
|
reminders[i]["predictiveEpochMillis"] = predictiveEpochMillis
|
|
|
|
|
}
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
@@ -1540,10 +1699,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)"
|
|
|
|
|
let fireDate = Date().addingTimeInterval(TimeInterval(validSeconds))
|
|
|
|
|
let epochMillis = Int64(fireDate.timeIntervalSince1970 * 1000)
|
|
|
|
|
let requestId = predictiveNotificationId(epochMillis: epochMillis)
|
|
|
|
|
let request = UNNotificationRequest(
|
|
|
|
|
identifier: notificationId,
|
|
|
|
|
identifier: requestId,
|
|
|
|
|
content: notificationContent,
|
|
|
|
|
trigger: trigger
|
|
|
|
|
)
|
|
|
|
|
@@ -2505,6 +2665,10 @@ public class DailyNotificationPlugin: CAPPlugin {
|
|
|
|
|
methods.append(CAPPluginMethod(name: "getScheduledReminders", returnType: CAPPluginReturnPromise))
|
|
|
|
|
methods.append(CAPPluginMethod(name: "updateDailyReminder", returnType: CAPPluginReturnPromise))
|
|
|
|
|
|
|
|
|
|
// Predictive batch API (replace-all UNUserNotificationCenter)
|
|
|
|
|
methods.append(CAPPluginMethod(name: "clearAllNotifications", returnType: CAPPluginReturnPromise))
|
|
|
|
|
methods.append(CAPPluginMethod(name: "scheduleNotifications", returnType: CAPPluginReturnPromise))
|
|
|
|
|
|
|
|
|
|
// Dual scheduling methods
|
|
|
|
|
methods.append(CAPPluginMethod(name: "scheduleContentFetch", returnType: CAPPluginReturnPromise))
|
|
|
|
|
methods.append(CAPPluginMethod(name: "scheduleUserNotification", returnType: CAPPluginReturnPromise))
|
|
|
|
|
|