Extract iOS orchestration logic from plugin to dedicated helper, matching Android's ScheduleHelper.kt pattern. This completes the P2.1 native plugin refactoring for both platforms. Changes: - Created DailyNotificationScheduleHelper.swift (192 lines) - scheduleDailyNotification(): Full orchestration (cancel, clear, save, schedule, prefetch) - scheduleDualNotification(): Dual scheduling coordination - clearRolloverState(): Rollover state cleanup helper - getHealthStatus(): Status combination from multiple sources - Refactored DailyNotificationPlugin.swift to delegate to helper - Reduced plugin by 236 lines (1854 → 1807 LOC) - Total iOS reduction: 11.7% (2047 → 1807 LOC) - Updated documentation - docs/progress/00-STATUS.md: Marked verification complete, added helper extraction - docs/progress/01-CHANGELOG-WORK.md: Added iOS helper extraction entry - docs/progress/P2.1-REFACTORING-COMPLETE.md: Updated with helper extraction - docs/00-INDEX.md: Added reference to refactoring summary Verification: - TypeScript typecheck: PASS - Build: PASS - Tests: PASS (115 tests, 8 test suites) - External API behavior unchanged Files changed: - ios/Plugin/DailyNotificationScheduleHelper.swift (new, 192 lines) - ios/Plugin/DailyNotificationPlugin.swift (198 insertions, 434 deletions) - docs/progress/00-STATUS.md (verification status updated) - docs/progress/01-CHANGELOG-WORK.md (changelog entry added) - docs/00-INDEX.md (index reference added) Related: - Completes P2.1 iOS refactoring (27 methods across 3 batches) - Matches Android ScheduleHelper.kt pattern - Total P2.1: 55 methods refactored (28 Android + 27 iOS)
193 lines
7.1 KiB
Swift
193 lines
7.1 KiB
Swift
//
|
|
// DailyNotificationScheduleHelper.swift
|
|
// DailyNotificationPlugin
|
|
//
|
|
// Created by Matthew Raymer on 2025-12-23
|
|
// Copyright © 2025 TimeSafari. All rights reserved.
|
|
//
|
|
|
|
import Foundation
|
|
import UserNotifications
|
|
|
|
/**
|
|
* DailyNotificationScheduleHelper.swift
|
|
*
|
|
* Orchestration helper for daily notification scheduling
|
|
*
|
|
* This helper encapsulates complex scheduling orchestration logic that combines
|
|
* multiple services (scheduler, storage, stateActor, background tasks).
|
|
* Similar to Android's ScheduleHelper.kt pattern.
|
|
*
|
|
* Responsibilities:
|
|
* - Schedule daily notifications with full orchestration (cancel, clear, save, schedule, prefetch)
|
|
* - Schedule dual notifications (background fetch + user notification)
|
|
* - Clear rollover state
|
|
* - Combine status from multiple sources
|
|
*
|
|
* @author Matthew Raymer
|
|
* @version 1.0.0
|
|
* @created 2025-12-23
|
|
*/
|
|
enum DailyNotificationScheduleHelper {
|
|
|
|
/**
|
|
* Schedule daily notification with full orchestration
|
|
*
|
|
* Orchestrates:
|
|
* 1. Cancel all existing notifications
|
|
* 2. Clear all stored notification content
|
|
* 3. Clear rollover state
|
|
* 4. Save notification content (via stateActor if available)
|
|
* 5. Schedule notification
|
|
* 6. Schedule background fetch (5 minutes before notification)
|
|
*
|
|
* @param content Notification content to schedule
|
|
* @param scheduledTime Scheduled time in milliseconds
|
|
* @param scheduler DailyNotificationScheduler instance
|
|
* @param storage DailyNotificationStorage instance
|
|
* @param stateActor DailyNotificationStateActor instance (optional, iOS 13+)
|
|
* @param scheduleBackgroundFetch Closure to schedule background fetch
|
|
* @return true if scheduling succeeded, false otherwise
|
|
*/
|
|
static func scheduleDailyNotification(
|
|
content: NotificationContent,
|
|
scheduledTime: Int64,
|
|
scheduler: DailyNotificationScheduler,
|
|
storage: DailyNotificationStorage?,
|
|
stateActor: DailyNotificationStateActor?,
|
|
scheduleBackgroundFetch: (Int64) -> Void
|
|
) async -> Bool {
|
|
// Step 1: Cancel all existing notifications
|
|
await scheduler.cancelAllNotifications()
|
|
|
|
// Step 2: Clear all stored notification content
|
|
storage?.clearAllNotifications()
|
|
|
|
// Step 3: Clear rollover state
|
|
clearRolloverState(storage: storage)
|
|
|
|
// Step 4: Save notification content (via stateActor if available, otherwise storage)
|
|
if #available(iOS 13.0, *), let stateActor = stateActor {
|
|
await stateActor.saveNotificationContent(content)
|
|
} else {
|
|
storage?.saveNotificationContent(content)
|
|
}
|
|
|
|
// Step 5: Schedule notification
|
|
let scheduled = await scheduler.scheduleNotification(content)
|
|
|
|
// Step 6: Schedule background fetch if notification was scheduled
|
|
if scheduled {
|
|
scheduleBackgroundFetch(scheduledTime)
|
|
}
|
|
|
|
return scheduled
|
|
}
|
|
|
|
/**
|
|
* Schedule dual notification (background fetch + user notification)
|
|
*
|
|
* Orchestrates both background fetch and user notification scheduling.
|
|
*
|
|
* @param contentFetchConfig Background fetch configuration
|
|
* @param userNotificationConfig User notification configuration
|
|
* @param scheduleBackgroundFetch Closure to schedule background fetch
|
|
* @param scheduleUserNotification Closure to schedule user notification
|
|
* @throws Error if scheduling fails
|
|
*/
|
|
static func scheduleDualNotification(
|
|
contentFetchConfig: [String: Any],
|
|
userNotificationConfig: [String: Any],
|
|
scheduleBackgroundFetch: ([String: Any]) throws -> Void,
|
|
scheduleUserNotification: ([String: Any]) throws -> Void
|
|
) throws {
|
|
// Schedule both background fetch and user notification
|
|
try scheduleBackgroundFetch(contentFetchConfig)
|
|
try scheduleUserNotification(userNotificationConfig)
|
|
}
|
|
|
|
/**
|
|
* Clear rollover state from storage and UserDefaults
|
|
*
|
|
* Clears:
|
|
* - Global rollover time in storage
|
|
* - All rollover_* keys from UserDefaults
|
|
*
|
|
* @param storage DailyNotificationStorage instance (optional)
|
|
*/
|
|
static func clearRolloverState(storage: DailyNotificationStorage?) {
|
|
// Clear global rollover time
|
|
storage?.saveLastRolloverTime(0)
|
|
|
|
// Clear per-notification rollover times from UserDefaults
|
|
let userDefaults = UserDefaults.standard
|
|
let allKeys = userDefaults.dictionaryRepresentation().keys
|
|
for key in allKeys {
|
|
if key.hasPrefix("rollover_") {
|
|
userDefaults.removeObject(forKey: key)
|
|
}
|
|
}
|
|
userDefaults.synchronize()
|
|
}
|
|
|
|
/**
|
|
* Get health status combining multiple sources
|
|
*
|
|
* Combines:
|
|
* - Scheduler status (pending count, permission status)
|
|
* - Storage/StateActor status (last notification)
|
|
*
|
|
* @param scheduler DailyNotificationScheduler instance
|
|
* @param storage DailyNotificationStorage instance (optional)
|
|
* @param stateActor DailyNotificationStateActor instance (optional, iOS 13+)
|
|
* @return Health status dictionary
|
|
* @throws Error if scheduler not initialized
|
|
*/
|
|
static func getHealthStatus(
|
|
scheduler: DailyNotificationScheduler,
|
|
storage: DailyNotificationStorage?,
|
|
stateActor: DailyNotificationStateActor?
|
|
) async throws -> [String: Any] {
|
|
// Delegate to scheduler for pending count and permission status
|
|
let pendingCount = await scheduler.getPendingNotificationCount()
|
|
let isEnabled = await scheduler.checkPermissionStatus() == .authorized
|
|
|
|
// Delegate to stateActor if available (thread-safe), otherwise use storage directly
|
|
let lastNotification: NotificationContent?
|
|
if #available(iOS 13.0, *), let stateActor = stateActor {
|
|
lastNotification = await stateActor.getLastNotification()
|
|
} else {
|
|
lastNotification = storage?.getLastNotification()
|
|
}
|
|
|
|
return [
|
|
"contentFetch": [
|
|
"isEnabled": true,
|
|
"isScheduled": pendingCount > 0,
|
|
"lastFetchTime": lastNotification?.fetchedAt ?? 0,
|
|
"nextFetchTime": 0,
|
|
"pendingFetches": pendingCount
|
|
],
|
|
"userNotification": [
|
|
"isEnabled": isEnabled,
|
|
"isScheduled": pendingCount > 0,
|
|
"lastNotificationTime": lastNotification?.scheduledTime ?? 0,
|
|
"nextNotificationTime": 0,
|
|
"pendingNotifications": pendingCount
|
|
],
|
|
"relationship": [
|
|
"isLinked": true,
|
|
"contentAvailable": lastNotification != nil,
|
|
"lastLinkTime": lastNotification?.fetchedAt ?? 0
|
|
],
|
|
"overall": [
|
|
"isActive": isEnabled && pendingCount > 0,
|
|
"lastActivity": lastNotification?.scheduledTime ?? 0,
|
|
"errorCount": 0,
|
|
"successRate": 1.0
|
|
]
|
|
]
|
|
}
|
|
}
|
|
|