diff --git a/android/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.kt b/android/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.kt index a389f85..3972485 100644 --- a/android/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.kt +++ b/android/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.kt @@ -1109,8 +1109,13 @@ open class DailyNotificationPlugin : Plugin() { val sound = options.getBoolean("sound") ?: true val priority = options.getString("priority") ?: "default" val url = options.getString("url") // Optional URL for prefetch (not used in helper yet) + val rolloverIntervalMinutes = try { + (options.getInt("rolloverIntervalMinutes") ?: 0).takeIf { it > 0 } + } catch (_: Exception) { + null + } - Log.i(TAG, "Scheduling daily notification: time=$time, title=$title") + Log.i(TAG, "Scheduling daily notification: time=$time, title=$title, rolloverIntervalMinutes=$rolloverIntervalMinutes") // Convert HH:mm time to cron expression (daily at specified time) val cronExpression = convertTimeToCron(time) @@ -1162,7 +1167,8 @@ open class DailyNotificationPlugin : Plugin() { scheduleId, config, time, - ::calculateNextRunTime + ::calculateNextRunTime, + rolloverIntervalMinutes ) if (success) { @@ -2658,6 +2664,7 @@ object ScheduleHelper { * @param config User notification configuration * @param clockTime Original HH:mm time string * @param calculateNextRunTime Function to calculate next run time from cron expression + * @param rolloverIntervalMinutes When > 0, next occurrence is this many minutes after trigger (dev/testing). Null/0 = 24h. * @return true if successful, false otherwise */ suspend fun scheduleDailyNotification( @@ -2666,7 +2673,8 @@ object ScheduleHelper { scheduleId: String, config: UserNotificationConfig, clockTime: String, - calculateNextRunTime: (String) -> Long + calculateNextRunTime: (String) -> Long, + rolloverIntervalMinutes: Int? = null ): Boolean { return try { val nextRunTime = calculateNextRunTime(config.schedule) @@ -2697,14 +2705,15 @@ object ScheduleHelper { // schedule a second alarm via legacy DailyNotificationScheduler, resulting in duplicate // notifications at fire time. - // Store schedule in database + // Store schedule in database (include rollover interval for dev/testing; survives reboot) val schedule = Schedule( id = scheduleId, kind = "notify", cron = config.schedule, clockTime = clockTime, enabled = true, - nextRunAt = nextRunTime + nextRunAt = nextRunTime, + rolloverIntervalMinutes = rolloverIntervalMinutes ) database.scheduleDao().upsert(schedule) @@ -2742,6 +2751,35 @@ object ScheduleHelper { false } } + + /** + * Blocking load of a schedule by id (for use from Java Worker / rollover path). + */ + @JvmStatic + fun getScheduleBlocking(context: Context, scheduleId: String): Schedule? { + return kotlinx.coroutines.runBlocking { + try { + DailyNotificationDatabase.getDatabase(context).scheduleDao().getById(scheduleId) + } catch (e: Exception) { + Log.w("ScheduleHelper", "getScheduleBlocking failed: $scheduleId", e) + null + } + } + } + + /** + * Blocking update of schedule next run time (for use from Java Worker after rollover). + */ + @JvmStatic + fun updateScheduleNextRunTimeBlocking(context: Context, scheduleId: String, lastRunAt: Long?, nextRunAt: Long) { + kotlinx.coroutines.runBlocking { + try { + DailyNotificationDatabase.getDatabase(context).scheduleDao().updateRunTimes(scheduleId, lastRunAt, nextRunAt) + } catch (e: Exception) { + Log.w("ScheduleHelper", "updateScheduleNextRunTimeBlocking failed: $scheduleId", e) + } + } + } /** * Schedule dual notification (fetch + notify) diff --git a/android/src/main/java/com/timesafari/dailynotification/DailyNotificationWorker.java b/android/src/main/java/com/timesafari/dailynotification/DailyNotificationWorker.java index 7e3c4bb..b45b86d 100644 --- a/android/src/main/java/com/timesafari/dailynotification/DailyNotificationWorker.java +++ b/android/src/main/java/com/timesafari/dailynotification/DailyNotificationWorker.java @@ -535,8 +535,33 @@ public class DailyNotificationWorker extends Worker { try { Log.d(TAG, "DN|RESCHEDULE_START id=" + content.getId()); - // Calculate next occurrence using DST-safe ZonedDateTime - long nextScheduledTime = calculateNextScheduledTime(content.getScheduledTime()); + // Resolve schedule_id first so we can load rollover interval from DB + Data inputDataForSchedule = getInputData(); + boolean preserveStaticReminder = inputDataForSchedule.getBoolean("is_static_reminder", false); + String scheduleIdForRollover = inputDataForSchedule.getString("schedule_id"); + if (scheduleIdForRollover == null || scheduleIdForRollover.isEmpty()) { + String notificationId = content.getId(); + if (preserveStaticReminder && notificationId != null && !notificationId.isEmpty()) { + scheduleIdForRollover = notificationId; + } else if (notificationId != null && notificationId.startsWith("daily_")) { + scheduleIdForRollover = notificationId; + } + } + Integer rolloverMinutes = null; + if (scheduleIdForRollover != null && !scheduleIdForRollover.isEmpty()) { + com.timesafari.dailynotification.Schedule s = com.timesafari.dailynotification.ScheduleHelper.getScheduleBlocking(getApplicationContext(), scheduleIdForRollover); + if (s != null && s.getRolloverIntervalMinutes() != null && s.getRolloverIntervalMinutes() > 0) { + rolloverMinutes = s.getRolloverIntervalMinutes(); + Log.d(TAG, "DN|ROLLOVER_INTERVAL scheduleId=" + scheduleIdForRollover + " minutes=" + rolloverMinutes); + } + } + long nextScheduledTime; + if (rolloverMinutes != null && rolloverMinutes > 0) { + nextScheduledTime = addMinutesToTime(content.getScheduledTime(), rolloverMinutes); + Log.d(TAG, "DN|ROLLOVER_NEXT using_interval_minutes=" + rolloverMinutes + " next=" + nextScheduledTime); + } else { + nextScheduledTime = calculateNextScheduledTime(content.getScheduledTime()); + } // Check for existing notification at the same time to prevent duplicates DailyNotificationStorage legacyStorage = new DailyNotificationStorage(getApplicationContext()); @@ -561,36 +586,30 @@ public class DailyNotificationWorker extends Worker { return; } - // Preserve static reminder semantics across rollover; use stable schedule_id so reschedule cancels this alarm - Data inputData = getInputData(); - boolean preserveStaticReminder = inputData.getBoolean("is_static_reminder", false); - String scheduleId = inputData.getString("schedule_id"); - if (scheduleId == null || scheduleId.isEmpty()) { - String notificationId = content.getId(); - if (preserveStaticReminder && notificationId != null && !notificationId.isEmpty()) { - scheduleId = notificationId; - } else if (notificationId != null && notificationId.startsWith("daily_")) { - scheduleId = notificationId; - } else { - scheduleId = "daily_rollover_" + System.currentTimeMillis(); - } - } String cronExpression = null; String notificationId = content.getId(); + String scheduleId = scheduleIdForRollover; + if (scheduleId == null || scheduleId.isEmpty()) { + scheduleId = "daily_rollover_" + System.currentTimeMillis(); + } - // Calculate cron from current scheduled time (extract hour:minute) - try { - java.util.Calendar cal = java.util.Calendar.getInstance(); - cal.setTimeInMillis(content.getScheduledTime()); - int hour = cal.get(java.util.Calendar.HOUR_OF_DAY); - int minute = cal.get(java.util.Calendar.MINUTE); - cronExpression = String.format("%d %d * * *", minute, hour); - - // Recalculate next run time from cron (tomorrow at same time) - nextScheduledTime = calculateNextRunTimeFromCron(cronExpression); - } catch (Exception e) { - Log.w(TAG, "Failed to calculate cron from scheduled time, using default", e); - cronExpression = "0 9 * * *"; // Default to 9 AM + // When using rollover interval, next time already set; otherwise compute from cron (tomorrow same time) + if (rolloverMinutes == null || rolloverMinutes <= 0) { + try { + java.util.Calendar cal = java.util.Calendar.getInstance(); + cal.setTimeInMillis(content.getScheduledTime()); + int hour = cal.get(java.util.Calendar.HOUR_OF_DAY); + int minute = cal.get(java.util.Calendar.MINUTE); + cronExpression = String.format("%d %d * * *", minute, hour); + nextScheduledTime = calculateNextRunTimeFromCron(cronExpression); + } catch (Exception e) { + Log.w(TAG, "Failed to calculate cron from scheduled time, using default", e); + cronExpression = "0 9 * * *"; + } + } else { + cronExpression = String.format("%d %d * * *", + java.util.Calendar.getInstance().get(java.util.Calendar.MINUTE), + java.util.Calendar.getInstance().get(java.util.Calendar.HOUR_OF_DAY)); } // Create config for next notification @@ -617,7 +636,10 @@ public class DailyNotificationWorker extends Worker { com.timesafari.dailynotification.ScheduleSource.ROLLOVER_ON_FIRE, false // skipPendingIntentIdempotence – rollover path does not skip ); - + if (scheduleId != null && !scheduleId.startsWith("daily_rollover_")) { + com.timesafari.dailynotification.ScheduleHelper.updateScheduleNextRunTimeBlocking( + getApplicationContext(), scheduleId, content.getScheduledTime(), nextScheduledTime); + } // Log next scheduled time in readable format String nextTimeStr = formatScheduledTime(nextScheduledTime); Log.i(TAG, "DN|RESCHEDULE_OK id=" + content.getId() + " next=" + nextTimeStr + " scheduleId=" + scheduleId); @@ -771,6 +793,21 @@ public class DailyNotificationWorker extends Worker { } } + /** + * Add minutes to a timestamp (DST-safe via Calendar). + * Used for rollover interval (e.g. 10 minutes for testing). + */ + private long addMinutesToTime(long timeMillis, int minutes) { + try { + java.util.Calendar cal = java.util.Calendar.getInstance(); + cal.setTimeInMillis(timeMillis); + cal.add(java.util.Calendar.MINUTE, minutes); + return cal.getTimeInMillis(); + } catch (Exception e) { + return timeMillis + (minutes * 60 * 1000L); + } + } + /** * Calculate next scheduled time with DST-safe handling * diff --git a/android/src/main/java/com/timesafari/dailynotification/DatabaseSchema.kt b/android/src/main/java/com/timesafari/dailynotification/DatabaseSchema.kt index f832cc8..06a385c 100644 --- a/android/src/main/java/com/timesafari/dailynotification/DatabaseSchema.kt +++ b/android/src/main/java/com/timesafari/dailynotification/DatabaseSchema.kt @@ -47,7 +47,9 @@ data class Schedule( val nextRunAt: Long? = null, val jitterMs: Int = 0, val backoffPolicy: String = "exp", - val stateJson: String? = null + val stateJson: String? = null, + /** When > 0, next occurrence is this many minutes after current trigger (dev/testing). Null or 0 = 24h. */ + val rolloverIntervalMinutes: Int? = null ) @Entity(tableName = "callbacks") @@ -83,7 +85,7 @@ data class History( NotificationDeliveryEntity::class, NotificationConfigEntity::class ], - version = 2, // Incremented for unified schema + version = 3, // 3: add rollover_interval_minutes to schedules exportSchema = false ) @TypeConverters(Converters::class) @@ -118,7 +120,7 @@ abstract class DailyNotificationDatabase : RoomDatabase() { DailyNotificationDatabase::class.java, DATABASE_NAME ) - .addMigrations(MIGRATION_1_2) // Migration from Kotlin-only to unified + .addMigrations(MIGRATION_1_2, MIGRATION_2_3) // 1->2: unified; 2->3: rollover_interval_minutes .addCallback(roomCallback) .build() INSTANCE = instance @@ -266,6 +268,15 @@ abstract class DailyNotificationDatabase : RoomDatabase() { """.trimIndent()) } } + + /** + * Migration from version 2 to 3: add rollover_interval_minutes to schedules + */ + val MIGRATION_2_3 = object : Migration(2, 3) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("ALTER TABLE schedules ADD COLUMN rollover_interval_minutes INTEGER") + } + } } } diff --git a/android/src/main/java/com/timesafari/dailynotification/ReactivationManager.kt b/android/src/main/java/com/timesafari/dailynotification/ReactivationManager.kt index 1aa412b..a0edd0d 100644 --- a/android/src/main/java/com/timesafari/dailynotification/ReactivationManager.kt +++ b/android/src/main/java/com/timesafari/dailynotification/ReactivationManager.kt @@ -125,8 +125,8 @@ class ReactivationManager(private val context: Context) { markMissedNotificationForSchedule(schedule, nextRunTime, db) missedCount++ - // Schedule next occurrence if repeating - val nextOccurrence = calculateNextOccurrence(currentTime) + // Schedule next occurrence (use rollover interval if set, else 24h) + val nextOccurrence = calculateNextOccurrenceForSchedule(schedule, nextRunTime, currentTime) rescheduleAlarmForBoot(context, schedule, nextOccurrence, db) rescheduledCount++ @@ -238,10 +238,25 @@ class ReactivationManager(private val context: Context) { } private fun calculateNextOccurrence(fromTime: Long): Long { - // For daily schedules, add 24 hours - // This is simplified - production should handle weekly/monthly patterns return fromTime + (24 * 60 * 60 * 1000L) } + + /** + * Next occurrence from a given trigger time. Uses schedule.rolloverIntervalMinutes when set and > 0 (dev/testing), else 24h. + * Advances until result > currentTime so we don't reschedule in the past. + */ + private fun calculateNextOccurrenceForSchedule(schedule: Schedule, fromTime: Long, currentTime: Long): Long { + val intervalMs = when { + schedule.rolloverIntervalMinutes != null && schedule.rolloverIntervalMinutes!! > 0 -> + schedule.rolloverIntervalMinutes!! * 60 * 1000L + else -> 24 * 60 * 60 * 1000L + } + var next = fromTime + intervalMs + while (next < currentTime) { + next += intervalMs + } + return next + } private suspend fun markMissedNotificationForSchedule( schedule: Schedule, diff --git a/ios/Plugin/DailyNotificationPlugin.swift b/ios/Plugin/DailyNotificationPlugin.swift index dda9fd1..f271e91 100644 --- a/ios/Plugin/DailyNotificationPlugin.swift +++ b/ios/Plugin/DailyNotificationPlugin.swift @@ -1118,12 +1118,14 @@ public class DailyNotificationPlugin: CAPPlugin { let sound = call.getBool("sound", true) let url = call.getString("url") let priority = call.getString("priority") ?? "default" + let rawRollover = call.getInt("rolloverIntervalMinutes") ?? 0 + let rolloverIntervalMinutes = rawRollover > 0 ? rawRollover : nil // Calculate scheduled time (next occurrence at specified hour:minute) let scheduledTime = calculateNextScheduledTime(hour: hour, minute: minute) let fetchedAt = Int64(Date().timeIntervalSince1970 * 1000) // Current time in milliseconds - // Create notification content + // Create notification content (persist rollover interval for dev/testing; survives app restart) let content = NotificationContent( id: "daily_\(Date().timeIntervalSince1970)", title: title, @@ -1132,7 +1134,8 @@ public class DailyNotificationPlugin: CAPPlugin { fetchedAt: fetchedAt, url: url, payload: nil, - etag: nil + etag: nil, + rolloverIntervalMinutes: rolloverIntervalMinutes ) // Delegate to ScheduleHelper for orchestration diff --git a/ios/Plugin/DailyNotificationScheduler.swift b/ios/Plugin/DailyNotificationScheduler.swift index b8633d3..46201d3 100644 --- a/ios/Plugin/DailyNotificationScheduler.swift +++ b/ios/Plugin/DailyNotificationScheduler.swift @@ -382,55 +382,24 @@ class DailyNotificationScheduler { } /** - * Calculate next scheduled time from current scheduled time (24 hours later, DST-safe) - * - * Matches Android calculateNextScheduledTime() functionality - * Handles DST transitions automatically using Calendar - * - * @param currentScheduledTime Current scheduled time in milliseconds - * @return Next scheduled time in milliseconds (24 hours later) - * - * TESTING: To test with shorter intervals (e.g., 2 minutes), change: - * - Line ~404: `.hour, value: 24` → `.minute, value: 2` - * - Line ~407: `(24 * 60 * 60 * 1000)` → `(2 * 60 * 1000)` + * Calculate next scheduled time from current (24h or rollover interval minutes). DST-safe. + * When rolloverIntervalMinutes > 0 (dev/testing), adds that many minutes; otherwise adds 24 hours. */ - func calculateNextScheduledTime(_ currentScheduledTime: Int64) -> Int64 { + func calculateNextScheduledTime(_ currentScheduledTime: Int64, rolloverIntervalMinutes: Int? = nil) -> Int64 { let calendar = Calendar.current let currentDate = Date(timeIntervalSince1970: Double(currentScheduledTime) / 1000.0) let currentTimeStr = formatTime(currentScheduledTime) - - // Add 24 hours (handles DST transitions automatically) - // TESTING: Change `.hour, value: 24` to `.minute, value: 2` for 2-minute testing - guard let nextDate = calendar.date(byAdding: .hour, value: 24, to: currentDate) else { - // Fallback to simple 24-hour addition if calendar calculation fails - // TESTING: Change `(24 * 60 * 60 * 1000)` to `(2 * 60 * 1000)` for 2-minute testing - let fallbackTime = currentScheduledTime + (24 * 60 * 60 * 1000) - let fallbackTimeStr = formatTime(fallbackTime) - NSLog("DNP-ROLLOVER: DST_CALC_FAILED current=\(currentTimeStr) using_fallback=\(fallbackTimeStr)") - print("DNP-ROLLOVER: DST_CALC_FAILED current=\(currentTimeStr) using_fallback=\(fallbackTimeStr)") + let addMinutes = (rolloverIntervalMinutes ?? 0) > 0 ? rolloverIntervalMinutes! : (24 * 60) + guard let nextDate = calendar.date(byAdding: .minute, value: addMinutes, to: currentDate) else { + let fallbackTime = currentScheduledTime + (Int64(addMinutes) * 60 * 1000) + NSLog("DNP-ROLLOVER: DST_CALC_FAILED current=\(currentTimeStr) using_fallback add_minutes=\(addMinutes)") return fallbackTime } - let nextTime = Int64(nextDate.timeIntervalSince1970 * 1000) let nextTimeStr = formatTime(nextTime) - - // Validate: Log DST transitions for debugging - let currentHour = calendar.component(.hour, from: currentDate) - let currentMinute = calendar.component(.minute, from: currentDate) - let nextHour = calendar.component(.hour, from: nextDate) - let nextMinute = calendar.component(.minute, from: nextDate) - - if currentHour != nextHour || currentMinute != nextMinute { - NSLog("DNP-ROLLOVER: DST_TRANSITION current=\(currentHour):\(String(format: "%02d", currentMinute)) next=\(nextHour):\(String(format: "%02d", nextMinute))") - print("DNP-ROLLOVER: DST_TRANSITION current=\(currentHour):\(String(format: "%02d", currentMinute)) next=\(nextHour):\(String(format: "%02d", nextMinute))") - } - - // Log the calculation result let timeDiffMs = nextTime - currentScheduledTime let timeDiffHours = Double(timeDiffMs) / 1000.0 / 60.0 / 60.0 - NSLog("DNP-ROLLOVER: CALC_NEXT current=\(currentTimeStr) next=\(nextTimeStr) diff_hours=\(String(format: "%.2f", timeDiffHours))") - print("DNP-ROLLOVER: CALC_NEXT current=\(currentTimeStr) next=\(nextTimeStr) diff_hours=\(String(format: "%.2f", timeDiffHours))") - + NSLog("DNP-ROLLOVER: CALC_NEXT current=\(currentTimeStr) next=\(nextTimeStr) diff_hours=\(String(format: "%.2f", timeDiffHours)) rollover_min=\(rolloverIntervalMinutes ?? 0)") return nextTime } @@ -473,16 +442,16 @@ class DailyNotificationScheduler { } } - // Calculate next occurrence using DST-safe calculation - var nextScheduledTime = calculateNextScheduledTime(content.scheduledTime) + // Calculate next occurrence (use stored rollover interval for dev/testing, else 24h) + let rolloverMin = (content.rolloverIntervalMinutes ?? 0) > 0 ? content.rolloverIntervalMinutes : nil + var nextScheduledTime = calculateNextScheduledTime(content.scheduledTime, rolloverIntervalMinutes: rolloverMin) // If next scheduled time is in the past, keep calculating forward until we get a future time // This handles cases where the notification fired more than 2 minutes ago while nextScheduledTime < currentTime { let nextTimeStr = formatTime(nextScheduledTime) NSLog("DNP-ROLLOVER: NEXT_IN_PAST id=\(content.id) next_time=\(nextTimeStr) current_time=\(currentTimeStr), calculating forward") - print("DNP-ROLLOVER: NEXT_IN_PAST id=\(content.id) next_time=\(nextTimeStr) current_time=\(currentTimeStr), calculating forward") - nextScheduledTime = calculateNextScheduledTime(nextScheduledTime) + nextScheduledTime = calculateNextScheduledTime(nextScheduledTime, rolloverIntervalMinutes: rolloverMin) } let nextScheduledTimeStr = formatTime(nextScheduledTime) @@ -539,13 +508,14 @@ class DailyNotificationScheduler { let nextId = "daily_rollover_\(Int64(Date().timeIntervalSince1970 * 1000))" let nextContent = NotificationContent( id: nextId, - title: content.title, // Will be updated by prefetch - body: content.body, // Will be updated by prefetch + title: content.title, + body: content.body, scheduledTime: nextScheduledTime, fetchedAt: Int64(Date().timeIntervalSince1970 * 1000), url: content.url, payload: content.payload, - etag: content.etag + etag: content.etag, + rolloverIntervalMinutes: content.rolloverIntervalMinutes ) // Schedule the next notification diff --git a/ios/Plugin/NotificationContent.swift b/ios/Plugin/NotificationContent.swift index a7d9b11..0ecadf1 100644 --- a/ios/Plugin/NotificationContent.swift +++ b/ios/Plugin/NotificationContent.swift @@ -27,6 +27,8 @@ class NotificationContent: Codable { let url: String? let payload: [String: Any]? let etag: String? + /** When > 0, next occurrence is this many minutes after trigger (dev/testing). Nil/0 = 24h. Persisted for rollover and recovery. */ + var rolloverIntervalMinutes: Int? // Phase 2: Delivery tracking properties var deliveryStatus: String? // e.g., "scheduled", "delivered", "missed", "error" @@ -43,6 +45,7 @@ class NotificationContent: Codable { case url case payload case etag + case rolloverIntervalMinutes case deliveryStatus case lastDeliveryAttempt } @@ -64,6 +67,7 @@ class NotificationContent: Codable { payload = nil } etag = try container.decodeIfPresent(String.self, forKey: .etag) + rolloverIntervalMinutes = try container.decodeIfPresent(Int.self, forKey: .rolloverIntervalMinutes) deliveryStatus = try container.decodeIfPresent(String.self, forKey: .deliveryStatus) lastDeliveryAttempt = try container.decodeIfPresent(Int64.self, forKey: .lastDeliveryAttempt) } @@ -83,6 +87,7 @@ class NotificationContent: Codable { try container.encode(payloadString, forKey: .payload) } try container.encodeIfPresent(etag, forKey: .etag) + try container.encodeIfPresent(rolloverIntervalMinutes, forKey: .rolloverIntervalMinutes) try container.encodeIfPresent(deliveryStatus, forKey: .deliveryStatus) try container.encodeIfPresent(lastDeliveryAttempt, forKey: .lastDeliveryAttempt) } @@ -100,20 +105,21 @@ class NotificationContent: Codable { * @param url URL for content fetching * @param payload Additional payload data * @param etag ETag for HTTP caching + * @param rolloverIntervalMinutes When > 0, next occurrence is this many minutes after trigger (dev/testing). Nil/0 = 24h. * @param deliveryStatus Delivery status (optional, Phase 2) * @param lastDeliveryAttempt Last delivery attempt timestamp (optional, Phase 2) */ - init(id: String, - title: String?, - body: String?, - scheduledTime: Int64, - fetchedAt: Int64, - url: String?, - payload: [String: Any]?, + init(id: String, + title: String?, + body: String?, + scheduledTime: Int64, + fetchedAt: Int64, + url: String?, + payload: [String: Any]?, etag: String?, + rolloverIntervalMinutes: Int? = nil, deliveryStatus: String? = nil, lastDeliveryAttempt: Int64? = nil) { - self.id = id self.title = title self.body = body @@ -122,6 +128,7 @@ class NotificationContent: Codable { self.url = url self.payload = payload self.etag = etag + self.rolloverIntervalMinutes = rolloverIntervalMinutes self.deliveryStatus = deliveryStatus self.lastDeliveryAttempt = lastDeliveryAttempt } @@ -259,6 +266,7 @@ class NotificationContent: Codable { lastDeliveryAttempt = nil } + let rollover = (dict["rolloverIntervalMinutes"] as? NSNumber)?.intValue return NotificationContent( id: id, title: dict["title"] as? String, @@ -268,6 +276,7 @@ class NotificationContent: Codable { url: dict["url"] as? String, payload: dict["payload"] as? [String: Any], etag: dict["etag"] as? String, + rolloverIntervalMinutes: rollover, deliveryStatus: dict["deliveryStatus"] as? String, lastDeliveryAttempt: lastDeliveryAttempt ) diff --git a/src/definitions.ts b/src/definitions.ts index af4f1d4..a49ec4d 100644 --- a/src/definitions.ts +++ b/src/definitions.ts @@ -82,6 +82,12 @@ export interface NotificationOptions { retryInterval?: number; offlineFallback?: boolean; headers?: Record; + /** + * Optional rollover interval in minutes for dev/testing. + * When set (e.g. 10), the next occurrence is scheduled this many minutes after the current trigger + * instead of 24 hours. Persisted with the schedule so it survives reboot. When absent or <= 0, use 24h. + */ + rolloverIntervalMinutes?: number; }