feat(plugin): add optional rolloverIntervalMinutes for dev/testing
Add optional rolloverIntervalMinutes to scheduleDailyNotification so the next occurrence can be scheduled N minutes after the current trigger (e.g. 10 minutes) instead of 24 hours. Value is persisted and used on rollover and after reboot. - TypeScript: NotificationOptions.rolloverIntervalMinutes?: number - Android: Schedule.rolloverIntervalMinutes in Room (migration 2→3); Plugin and ScheduleHelper persist it; Worker uses it in rollover and updates nextRunAt; ReactivationManager uses it in boot recovery - iOS: NotificationContent.rolloverIntervalMinutes (Codable); Plugin passes it into content; Scheduler uses it in calculateNextScheduledTime and copies to nextContent on rollover When absent or ≤0, behavior unchanged (24h). App can clear by calling scheduleDailyNotification without the parameter.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user