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
|
// 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"
|
||||||
/// 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_"
|
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"
|
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"
|
||||||
@@ -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) {
|
@objc func cancelDualSchedule(_ call: CAPPluginCall) {
|
||||||
do {
|
do {
|
||||||
performCancelDualSchedule()
|
performCancelDualSchedule()
|
||||||
@@ -1085,7 +1087,7 @@ public class DailyNotificationPlugin: CAPPlugin {
|
|||||||
return interval > 0 ? interval : (24 * 60 * 60)
|
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 {
|
private func epochMillisNextDailyOccurrence(hour: Int, minute: Int) -> Int64 {
|
||||||
var comp = DateComponents()
|
var comp = DateComponents()
|
||||||
comp.hour = hour
|
comp.hour = hour
|
||||||
@@ -1101,26 +1103,29 @@ public class DailyNotificationPlugin: CAPPlugin {
|
|||||||
"\(apiNotificationPrefix)\(epochMillis)"
|
"\(apiNotificationPrefix)\(epochMillis)"
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reads persisted millis for a reminder, or derives from stored `time` (HH:mm) for legacy rows.
|
private func dailyReminderNotificationId(reminderId: String) -> String {
|
||||||
private func apiEpochMillis(from reminder: [String: Any]) -> Int64? {
|
"\(dailyReminderNotificationPrefix)\(reminderId)"
|
||||||
if let n = reminder["predictiveEpochMillis"] as? NSNumber {
|
}
|
||||||
return n.int64Value
|
|
||||||
|
/// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
guard let time = reminder["time"] as? String else { return nil }
|
notificationCenter.removePendingNotificationRequests(withIdentifiers: ids)
|
||||||
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: - API batch notifications (replace-all UNUserNotificationCenter)
|
// 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) {
|
@objc func clearApiNotifications(_ call: CAPPluginCall) {
|
||||||
NSLog("DNP-BATCH: clearApiNotifications — removing api_* pending and delivered only")
|
NSLog("DNP-BATCH: clearApiNotifications — removing api_* pending and delivered only")
|
||||||
print("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 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(
|
let request = UNNotificationRequest(
|
||||||
identifier: requestId,
|
identifier: requestId,
|
||||||
@@ -1302,13 +1313,10 @@ public class DailyNotificationPlugin: CAPPlugin {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cancel the notification (ID from stored epoch millis)
|
// Cancel pending Daily Reminder notification(s) for this logical id.
|
||||||
let reminders = getRemindersFromUserDefaults()
|
let reminders = getRemindersFromUserDefaults()
|
||||||
if let stored = reminders.first(where: { ($0["id"] as? String) == reminderId }),
|
let stored = reminders.first(where: { ($0["id"] as? String) == reminderId })
|
||||||
let epochMillis = apiEpochMillis(from: stored) {
|
cancelDailyReminderNotificationRequests(reminderId: reminderId, storedReminder: stored)
|
||||||
let requestId = apiNotificationId(epochMillis: epochMillis)
|
|
||||||
notificationCenter.removePendingNotificationRequests(withIdentifiers: [requestId])
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delegate to storage for reminder removal
|
// Delegate to storage for reminder removal
|
||||||
removeReminderFromUserDefaults(id: reminderId)
|
removeReminderFromUserDefaults(id: reminderId)
|
||||||
@@ -1321,17 +1329,23 @@ 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(self.apiNotificationPrefix) }
|
let pendingIds = Set(requests.map { $0.identifier })
|
||||||
|
|
||||||
// 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 expectedId: String? = self.apiEpochMillis(from: reminder).map {
|
guard let logicalId = reminder["id"] as? String else {
|
||||||
self.apiNotificationId(epochMillis: $0)
|
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
|
var reminderInfo = reminder
|
||||||
reminderInfo["isScheduled"] = isScheduled
|
reminderInfo["isScheduled"] = isScheduled
|
||||||
result.append(reminderInfo)
|
result.append(reminderInfo)
|
||||||
@@ -1351,11 +1365,8 @@ public class DailyNotificationPlugin: CAPPlugin {
|
|||||||
|
|
||||||
// Cancel existing reminder (before UserDefaults update)
|
// Cancel existing reminder (before UserDefaults update)
|
||||||
let remindersBeforeUpdate = getRemindersFromUserDefaults()
|
let remindersBeforeUpdate = getRemindersFromUserDefaults()
|
||||||
if let stored = remindersBeforeUpdate.first(where: { ($0["id"] as? String) == reminderId }),
|
let storedBeforeUpdate = remindersBeforeUpdate.first(where: { ($0["id"] as? String) == reminderId })
|
||||||
let epochMillis = apiEpochMillis(from: stored) {
|
cancelDailyReminderNotificationRequests(reminderId: reminderId, storedReminder: storedBeforeUpdate)
|
||||||
let oldRequestId = apiNotificationId(epochMillis: epochMillis)
|
|
||||||
notificationCenter.removePendingNotificationRequests(withIdentifiers: [oldRequestId])
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update in UserDefaults
|
// Update in UserDefaults
|
||||||
let title = call.getString("title")
|
let title = call.getString("title")
|
||||||
@@ -1421,7 +1432,7 @@ public class DailyNotificationPlugin: CAPPlugin {
|
|||||||
)
|
)
|
||||||
|
|
||||||
let epochMillis = epochMillisNextDailyOccurrence(hour: hour, minute: minute)
|
let epochMillis = epochMillisNextDailyOccurrence(hour: hour, minute: minute)
|
||||||
let requestId = apiNotificationId(epochMillis: epochMillis)
|
let requestId = dailyReminderNotificationId(reminderId: reminderId)
|
||||||
|
|
||||||
let request = UNNotificationRequest(
|
let request = UNNotificationRequest(
|
||||||
identifier: requestId,
|
identifier: requestId,
|
||||||
|
|||||||
@@ -608,8 +608,9 @@ export interface DailyNotificationPlugin {
|
|||||||
*/
|
*/
|
||||||
scheduleApiNotifications(options: { timestamps: number[] }): Promise<void>;
|
scheduleApiNotifications(options: { timestamps: number[] }): Promise<void>;
|
||||||
/**
|
/**
|
||||||
* Cancel API-managed notifications (`api_*`) without affecting Daily Reminders,
|
* Cancel API-managed notifications (`api_*`) without affecting Daily Reminders
|
||||||
* user schedules, dual schedules, or fetch jobs.
|
* (iOS: `daily_*`; Android: `daily_notification` / caller id), user schedules,
|
||||||
|
* dual schedules, or fetch jobs.
|
||||||
*/
|
*/
|
||||||
clearApiNotifications(): Promise<void>;
|
clearApiNotifications(): Promise<void>;
|
||||||
getNotificationStatus(): Promise<NotificationStatus>;
|
getNotificationStatus(): Promise<NotificationStatus>;
|
||||||
@@ -1044,8 +1045,9 @@ export interface DailyNotificationPlugin {
|
|||||||
|
|
||||||
// Static Daily Reminder Methods
|
// Static Daily Reminder Methods
|
||||||
/**
|
/**
|
||||||
* Schedule a simple daily reminder notification
|
* Schedule a simple daily reminder notification.
|
||||||
* No network content required - just static text
|
* No network content required — just static text.
|
||||||
|
* iOS uses `daily_<reminderId>` identifiers (separate from API-managed `api_*`).
|
||||||
*/
|
*/
|
||||||
scheduleDailyReminder(options: DailyReminderOptions): Promise<void>;
|
scheduleDailyReminder(options: DailyReminderOptions): Promise<void>;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user