// // DailyNotificationBackgroundTasks.swift // DailyNotificationPlugin // // Created by Matthew Raymer on 2025-09-22 // Copyright © 2025 TimeSafari. All rights reserved. // import Foundation import BackgroundTasks import UserNotifications import CoreData /** * Background task handlers for iOS Daily Notification Plugin * Implements BGTaskScheduler handlers for content fetch and notification delivery * * @author Matthew Raymer * @version 1.1.0 * @created 2025-09-22 09:22:32 UTC */ extension DailyNotificationPlugin { private func handleBackgroundFetch(task: BGAppRefreshTask) { print("DNP-FETCH-START: Background fetch task started") task.expirationHandler = { print("DNP-FETCH-TIMEOUT: Background fetch task expired") task.setTaskCompleted(success: false) } Task { do { let startTime = Date() let content = try await performContentFetch() // Store content in Core Data try await storeContent(content) let duration = Date().timeIntervalSince(startTime) print("DNP-FETCH-SUCCESS: Content fetch completed in \(duration)s") // Fire callbacks try await fireCallbacks(eventType: "onFetchSuccess", payload: content) task.setTaskCompleted(success: true) } catch { print("DNP-FETCH-FAILURE: Content fetch failed: \(error)") task.setTaskCompleted(success: false) } } } private func handleBackgroundNotify(task: BGProcessingTask) { print("DNP-NOTIFY-START: Background notify task started") task.expirationHandler = { print("DNP-NOTIFY-TIMEOUT: Background notify task expired") task.setTaskCompleted(success: false) } Task { do { let startTime = Date() // Get latest cached content guard let latestContent = try await getLatestContent() else { print("DNP-NOTIFY-SKIP: No cached content available") task.setTaskCompleted(success: true) return } // Check TTL if isContentExpired(content: latestContent) { print("DNP-NOTIFY-SKIP-TTL: Content TTL expired, skipping notification") try await recordHistory(kind: "notify", outcome: "skipped_ttl") task.setTaskCompleted(success: true) return } // Show notification try await showNotification(content: latestContent) let duration = Date().timeIntervalSince(startTime) print("DNP-NOTIFY-SUCCESS: Notification displayed in \(duration)s") // Fire callbacks try await fireCallbacks(eventType: "onNotifyDelivered", payload: latestContent) task.setTaskCompleted(success: true) } catch { print("DNP-NOTIFY-FAILURE: Notification failed: \(error)") task.setTaskCompleted(success: false) } } } private func performContentFetch() async throws -> [String: Any] { // Mock content fetch implementation // In production, this would make actual HTTP requests let mockContent = [ "id": "fetch_\(Date().timeIntervalSince1970)", "timestamp": Date().timeIntervalSince1970, "content": "Daily notification content from iOS", "source": "ios_platform" ] as [String: Any] return mockContent } private func storeContent(_ content: [String: Any]) async throws { // Phase 1: Use DailyNotificationStorage instead of CoreData // Convert dictionary to NotificationContent and store via stateActor guard let id = content["id"] as? String else { throw NSError(domain: "DailyNotification", code: -1, userInfo: [NSLocalizedDescriptionKey: "Missing content ID"]) } let currentTime = Int64(Date().timeIntervalSince1970 * 1000) let notificationContent = NotificationContent( id: id, title: content["title"] as? String, body: content["content"] as? String ?? content["body"] as? String, scheduledTime: currentTime, // Will be updated by scheduler fetchedAt: currentTime, url: content["url"] as? String, payload: content, etag: content["etag"] as? String ) // Store via stateActor if available if #available(iOS 13.0, *), let stateActor = stateActor { await stateActor.saveNotificationContent(notificationContent) } else if let storage = storage { storage.saveNotificationContent(notificationContent) } print("DNP-CACHE-STORE: Content stored via DailyNotificationStorage") } private func getLatestContent() async throws -> [String: Any]? { // Phase 1: Get from DailyNotificationStorage if #available(iOS 13.0, *), let stateActor = stateActor { // Get latest notification from storage // For now, return nil - this will be implemented when needed return nil } else if let storage = storage { // Access storage directly if stateActor not available // For now, return nil - this will be implemented when needed return nil } return nil } private func isContentExpired(content: [String: Any]) -> Bool { guard let timestamp = content["timestamp"] as? TimeInterval else { return true } let fetchedAt = Date(timeIntervalSince1970: timestamp) let ttlExpiry = fetchedAt.addingTimeInterval(3600) // 1 hour TTL return Date() > ttlExpiry } private func showNotification(content: [String: Any]) async throws { let notificationContent = UNMutableNotificationContent() notificationContent.title = "Daily Notification" notificationContent.body = content["content"] as? String ?? "Your daily update is ready" notificationContent.sound = .default let request = UNNotificationRequest( identifier: "daily-notification-\(Date().timeIntervalSince1970)", content: notificationContent, trigger: nil // Immediate delivery ) try await notificationCenter.add(request) print("DNP-NOTIFY-DISPLAY: Notification displayed") } private func recordHistory(kind: String, outcome: String) async throws { // Phase 1: History recording is not yet implemented // TODO: Phase 2 - Implement history with CoreData print("DNP-HISTORY: \(kind) - \(outcome) (Phase 2 - not implemented)") } }