Files
daily-notification-plugin/ios/Plugin/DailyNotificationScheduleHelper.swift
Matthew Raymer 1dca99ad17 feat(ios): Extract orchestration helpers to ScheduleHelper
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)
2025-12-24 06:35:03 +00:00

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
]
]
}
}