fix(ios): separate Daily Reminder identifiers from api_ namespace
Daily Reminders now use daily_<reminderId> for UNNotificationCenter requests instead of sharing api_<epochMillis> with batch API notifications. clearApiNotifications no longer removes new daily reminders. Add legacy api_* cleanup on schedule/cancel/update and transitional getScheduledReminders matching via stored predictiveEpochMillis. Update TypeScript JSDoc for iOS identifier families.
This commit is contained in:
@@ -32,9 +32,11 @@ public class DailyNotificationPlugin: CAPPlugin {
|
||||
// Background task identifiers
|
||||
private let fetchTaskIdentifier = "org.timesafari.dailynotification.fetch"
|
||||
private let notifyTaskIdentifier = "org.timesafari.dailynotification.notify"
|
||||
/// Prefix for deterministic UNNotificationRequest identifiers: `api_\(Int(timestamp))` with `timestamp` in epoch milliseconds.
|
||||
/// Prefix for API-managed batch notification identifiers: `api_\(epochMillis)`.
|
||||
private let apiNotificationPrefix = "api_"
|
||||
/// Stable identifier for the dual (New Activity) user notification. Used for replace and cancelDualSchedule; must not conflict with Daily Reminder (`api_*`).
|
||||
/// Prefix for Daily Reminder UNNotificationRequest identifiers: `daily_\(logicalReminderId)`.
|
||||
private let dailyReminderNotificationPrefix = "daily_"
|
||||
/// Stable identifier for the dual (New Activity) user notification. Used for replace and cancelDualSchedule; must not conflict with Daily Reminder (`daily_*`).
|
||||
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"
|
||||
@@ -430,7 +432,7 @@ public class DailyNotificationPlugin: CAPPlugin {
|
||||
}
|
||||
}
|
||||
|
||||
/// Cancel the dual (New Activity) schedule only. Does not affect Daily Reminder (`api_*`).
|
||||
/// Cancel the dual (New Activity) schedule only. Does not affect Daily Reminder (`daily_*`).
|
||||
@objc func cancelDualSchedule(_ call: CAPPluginCall) {
|
||||
do {
|
||||
performCancelDualSchedule()
|
||||
@@ -1085,7 +1087,7 @@ 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 API notification IDs from HH:mm schedules).
|
||||
/// Epoch milliseconds for the next local occurrence of `hour`:`minute` (persisted as `predictiveEpochMillis` for reminder alignment).
|
||||
private func epochMillisNextDailyOccurrence(hour: Int, minute: Int) -> Int64 {
|
||||
var comp = DateComponents()
|
||||
comp.hour = hour
|
||||
@@ -1101,26 +1103,29 @@ public class DailyNotificationPlugin: CAPPlugin {
|
||||
"\(apiNotificationPrefix)\(epochMillis)"
|
||||
}
|
||||
|
||||
/// Reads persisted millis for a reminder, or derives from stored `time` (HH:mm) for legacy rows.
|
||||
private func apiEpochMillis(from reminder: [String: Any]) -> Int64? {
|
||||
if let n = reminder["predictiveEpochMillis"] as? NSNumber {
|
||||
return n.int64Value
|
||||
private func dailyReminderNotificationId(reminderId: String) -> String {
|
||||
"\(dailyReminderNotificationPrefix)\(reminderId)"
|
||||
}
|
||||
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
|
||||
|
||||
/// Pre-separation Daily Reminders used `api_<epochMillis>`. Used only to cancel stale pending requests.
|
||||
private func legacyApiReminderNotificationId(from reminder: [String: Any]) -> String? {
|
||||
guard let n = reminder["predictiveEpochMillis"] as? NSNumber else { return nil }
|
||||
return apiNotificationId(epochMillis: n.int64Value)
|
||||
}
|
||||
return epochMillisNextDailyOccurrence(hour: hour, minute: minute)
|
||||
|
||||
private func cancelDailyReminderNotificationRequests(reminderId: String, storedReminder: [String: Any]?) {
|
||||
var ids = [dailyReminderNotificationId(reminderId: reminderId)]
|
||||
if let storedReminder = storedReminder,
|
||||
let legacyId = legacyApiReminderNotificationId(from: storedReminder),
|
||||
!ids.contains(legacyId) {
|
||||
ids.append(legacyId)
|
||||
}
|
||||
notificationCenter.removePendingNotificationRequests(withIdentifiers: ids)
|
||||
}
|
||||
|
||||
// MARK: - API batch notifications (replace-all UNUserNotificationCenter)
|
||||
|
||||
/// Removes only pending and delivered notifications whose identifiers begin with `api_`. Does not touch dual/org IDs or other stacks.
|
||||
/// Removes only pending and delivered API-managed notifications (`api_*`). Does not touch Daily Reminders (`daily_*`), dual/org IDs, or other stacks.
|
||||
@objc func clearApiNotifications(_ call: CAPPluginCall) {
|
||||
NSLog("DNP-BATCH: clearApiNotifications — removing api_* pending and delivered only")
|
||||
print("DNP-BATCH: clearApiNotifications — removing api_* pending and delivered only")
|
||||
@@ -1262,7 +1267,13 @@ public class DailyNotificationPlugin: CAPPlugin {
|
||||
)
|
||||
|
||||
let epochMillis = epochMillisNextDailyOccurrence(hour: hour, minute: minute)
|
||||
let requestId = apiNotificationId(epochMillis: epochMillis)
|
||||
let requestId = dailyReminderNotificationId(reminderId: id)
|
||||
|
||||
// Cancel any pre-separation `api_*` pending request for this logical reminder id.
|
||||
if let existing = getRemindersFromUserDefaults().first(where: { ($0["id"] as? String) == id }),
|
||||
let legacyId = legacyApiReminderNotificationId(from: existing) {
|
||||
notificationCenter.removePendingNotificationRequests(withIdentifiers: [legacyId])
|
||||
}
|
||||
|
||||
let request = UNNotificationRequest(
|
||||
identifier: requestId,
|
||||
@@ -1302,13 +1313,10 @@ public class DailyNotificationPlugin: CAPPlugin {
|
||||
return
|
||||
}
|
||||
|
||||
// Cancel the notification (ID from stored epoch millis)
|
||||
// Cancel pending Daily Reminder notification(s) for this logical id.
|
||||
let reminders = getRemindersFromUserDefaults()
|
||||
if let stored = reminders.first(where: { ($0["id"] as? String) == reminderId }),
|
||||
let epochMillis = apiEpochMillis(from: stored) {
|
||||
let requestId = apiNotificationId(epochMillis: epochMillis)
|
||||
notificationCenter.removePendingNotificationRequests(withIdentifiers: [requestId])
|
||||
}
|
||||
let stored = reminders.first(where: { ($0["id"] as? String) == reminderId })
|
||||
cancelDailyReminderNotificationRequests(reminderId: reminderId, storedReminder: stored)
|
||||
|
||||
// Delegate to storage for reminder removal
|
||||
removeReminderFromUserDefaults(id: reminderId)
|
||||
@@ -1321,17 +1329,23 @@ public class DailyNotificationPlugin: CAPPlugin {
|
||||
|
||||
// Get pending notifications
|
||||
notificationCenter.getPendingNotificationRequests { requests in
|
||||
let reminderRequests = requests.filter { $0.identifier.hasPrefix(self.apiNotificationPrefix) }
|
||||
let pendingIds = Set(requests.map { $0.identifier })
|
||||
|
||||
// Get stored reminder data from UserDefaults
|
||||
let reminders = self.getRemindersFromUserDefaults()
|
||||
|
||||
var result: [[String: Any]] = []
|
||||
for reminder in reminders {
|
||||
let expectedId: String? = self.apiEpochMillis(from: reminder).map {
|
||||
self.apiNotificationId(epochMillis: $0)
|
||||
guard let logicalId = reminder["id"] as? String else {
|
||||
var reminderInfo = reminder
|
||||
reminderInfo["isScheduled"] = false
|
||||
result.append(reminderInfo)
|
||||
continue
|
||||
}
|
||||
let isScheduled = reminderRequests.contains { expectedId != nil && $0.identifier == expectedId }
|
||||
let expectedId = self.dailyReminderNotificationId(reminderId: logicalId)
|
||||
let legacyId = self.legacyApiReminderNotificationId(from: reminder)
|
||||
let isScheduled = pendingIds.contains(expectedId)
|
||||
|| (legacyId != nil && pendingIds.contains(legacyId!))
|
||||
var reminderInfo = reminder
|
||||
reminderInfo["isScheduled"] = isScheduled
|
||||
result.append(reminderInfo)
|
||||
@@ -1351,11 +1365,8 @@ public class DailyNotificationPlugin: CAPPlugin {
|
||||
|
||||
// Cancel existing reminder (before UserDefaults update)
|
||||
let remindersBeforeUpdate = getRemindersFromUserDefaults()
|
||||
if let stored = remindersBeforeUpdate.first(where: { ($0["id"] as? String) == reminderId }),
|
||||
let epochMillis = apiEpochMillis(from: stored) {
|
||||
let oldRequestId = apiNotificationId(epochMillis: epochMillis)
|
||||
notificationCenter.removePendingNotificationRequests(withIdentifiers: [oldRequestId])
|
||||
}
|
||||
let storedBeforeUpdate = remindersBeforeUpdate.first(where: { ($0["id"] as? String) == reminderId })
|
||||
cancelDailyReminderNotificationRequests(reminderId: reminderId, storedReminder: storedBeforeUpdate)
|
||||
|
||||
// Update in UserDefaults
|
||||
let title = call.getString("title")
|
||||
@@ -1421,7 +1432,7 @@ public class DailyNotificationPlugin: CAPPlugin {
|
||||
)
|
||||
|
||||
let epochMillis = epochMillisNextDailyOccurrence(hour: hour, minute: minute)
|
||||
let requestId = apiNotificationId(epochMillis: epochMillis)
|
||||
let requestId = dailyReminderNotificationId(reminderId: reminderId)
|
||||
|
||||
let request = UNNotificationRequest(
|
||||
identifier: requestId,
|
||||
|
||||
@@ -608,8 +608,9 @@ export interface DailyNotificationPlugin {
|
||||
*/
|
||||
scheduleApiNotifications(options: { timestamps: number[] }): Promise<void>;
|
||||
/**
|
||||
* Cancel API-managed notifications (`api_*`) without affecting Daily Reminders,
|
||||
* user schedules, dual schedules, or fetch jobs.
|
||||
* Cancel API-managed notifications (`api_*`) without affecting Daily Reminders
|
||||
* (iOS: `daily_*`; Android: `daily_notification` / caller id), user schedules,
|
||||
* dual schedules, or fetch jobs.
|
||||
*/
|
||||
clearApiNotifications(): Promise<void>;
|
||||
getNotificationStatus(): Promise<NotificationStatus>;
|
||||
@@ -1044,8 +1045,9 @@ export interface DailyNotificationPlugin {
|
||||
|
||||
// Static Daily Reminder Methods
|
||||
/**
|
||||
* Schedule a simple daily reminder notification
|
||||
* No network content required - just static text
|
||||
* Schedule a simple daily reminder notification.
|
||||
* No network content required — just static text.
|
||||
* iOS uses `daily_<reminderId>` identifiers (separate from API-managed `api_*`).
|
||||
*/
|
||||
scheduleDailyReminder(options: DailyReminderOptions): Promise<void>;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user