// // 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 { let context = persistenceController.container.viewContext let contentEntity = ContentCache(context: context) contentEntity.id = content["id"] as? String contentEntity.fetchedAt = Date(timeIntervalSince1970: content["timestamp"] as? TimeInterval ?? 0) contentEntity.ttlSeconds = 3600 // 1 hour default TTL contentEntity.payload = try JSONSerialization.data(withJSONObject: content) contentEntity.meta = "fetched_by_ios_bg_task" try context.save() print("DNP-CACHE-STORE: Content stored in Core Data") } private func getLatestContent() async throws -> [String: Any]? { let context = persistenceController.container.viewContext let request: NSFetchRequest = ContentCache.fetchRequest() request.sortDescriptors = [NSSortDescriptor(keyPath: \ContentCache.fetchedAt, ascending: false)] request.fetchLimit = 1 let results = try context.fetch(request) guard let latest = results.first else { return nil } return try JSONSerialization.jsonObject(with: latest.payload!) as? [String: Any] } 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 { let context = persistenceController.container.viewContext let history = History(context: context) history.id = "\(kind)_\(Date().timeIntervalSince1970)" history.kind = kind history.occurredAt = Date() history.outcome = outcome try context.save() } }