// // DailyNotificationPlugin.swift // DailyNotificationPlugin // // Created by Matthew Raymer on 2025-09-22 // Copyright © 2025 TimeSafari. All rights reserved. // import Foundation import Capacitor import UserNotifications import BackgroundTasks import CoreData /** * iOS implementation of Daily Notification Plugin * Implements BGTaskScheduler + UNUserNotificationCenter for dual scheduling * * @author Matthew Raymer * @version 1.1.0 * @created 2025-09-22 09:22:32 UTC */ @objc(DailyNotificationPlugin) public class DailyNotificationPlugin: CAPPlugin { private let notificationCenter = UNUserNotificationCenter.current() private let backgroundTaskScheduler = BGTaskScheduler.shared private let persistenceController = PersistenceController.shared // Background task identifiers private let fetchTaskIdentifier = "com.timesafari.dailynotification.fetch" private let notifyTaskIdentifier = "com.timesafari.dailynotification.notify" override public func load() { super.load() setupBackgroundTasks() print("DNP-PLUGIN: Daily Notification Plugin loaded on iOS") } // MARK: - Configuration Methods @objc func configure(_ call: CAPPluginCall) { guard let options = call.getObject("options") else { call.reject("Configuration options required") return } print("DNP-PLUGIN: Configure called with options: \(options)") // Store configuration in UserDefaults UserDefaults.standard.set(options, forKey: "DailyNotificationConfig") call.resolve() } // MARK: - Dual Scheduling Methods @objc func scheduleContentFetch(_ call: CAPPluginCall) { guard let config = call.getObject("config") else { call.reject("Content fetch config required") return } print("DNP-PLUGIN: Scheduling content fetch") do { try scheduleBackgroundFetch(config: config) call.resolve() } catch { print("DNP-PLUGIN: Failed to schedule content fetch: \(error)") call.reject("Content fetch scheduling failed: \(error.localizedDescription)") } } @objc func scheduleUserNotification(_ call: CAPPluginCall) { guard let config = call.getObject("config") else { call.reject("User notification config required") return } print("DNP-PLUGIN: Scheduling user notification") do { try scheduleUserNotification(config: config) call.resolve() } catch { print("DNP-PLUGIN: Failed to schedule user notification: \(error)") call.reject("User notification scheduling failed: \(error.localizedDescription)") } } @objc func scheduleDualNotification(_ call: CAPPluginCall) { guard let config = call.getObject("config"), let contentFetchConfig = config["contentFetch"] as? [String: Any], let userNotificationConfig = config["userNotification"] as? [String: Any] else { call.reject("Dual notification config required") return } print("DNP-PLUGIN: Scheduling dual notification") do { try scheduleBackgroundFetch(config: contentFetchConfig) try scheduleUserNotification(config: userNotificationConfig) call.resolve() } catch { print("DNP-PLUGIN: Failed to schedule dual notification: \(error)") call.reject("Dual notification scheduling failed: \(error.localizedDescription)") } } @objc func getDualScheduleStatus(_ call: CAPPluginCall) { Task { do { let status = try await getHealthStatus() call.resolve(status) } catch { print("DNP-PLUGIN: Failed to get dual schedule status: \(error)") call.reject("Status retrieval failed: \(error.localizedDescription)") } } } // MARK: - Private Implementation Methods private func setupBackgroundTasks() { // Register background fetch task backgroundTaskScheduler.register(forTaskWithIdentifier: fetchTaskIdentifier, using: nil) { task in self.handleBackgroundFetch(task: task as! BGAppRefreshTask) } // Register background processing task backgroundTaskScheduler.register(forTaskWithIdentifier: notifyTaskIdentifier, using: nil) { task in self.handleBackgroundNotify(task: task as! BGProcessingTask) } } private func scheduleBackgroundFetch(config: [String: Any]) throws { let request = BGAppRefreshTaskRequest(identifier: fetchTaskIdentifier) // Calculate next run time (simplified - would use proper cron parsing in production) let nextRunTime = calculateNextRunTime(from: config["schedule"] as? String ?? "0 9 * * *") request.earliestBeginDate = Date(timeIntervalSinceNow: nextRunTime) try backgroundTaskScheduler.submit(request) print("DNP-FETCH-SCHEDULE: Background fetch scheduled for \(request.earliestBeginDate!)") } private func scheduleUserNotification(config: [String: Any]) throws { let content = UNMutableNotificationContent() content.title = config["title"] as? String ?? "Daily Notification" content.body = config["body"] as? String ?? "Your daily update is ready" content.sound = (config["sound"] as? Bool ?? true) ? .default : nil // Create trigger (simplified - would use proper cron parsing in production) let nextRunTime = calculateNextRunTime(from: config["schedule"] as? String ?? "0 9 * * *") let trigger = UNTimeIntervalNotificationTrigger(timeInterval: nextRunTime, repeats: false) let request = UNNotificationRequest( identifier: "daily-notification-\(Date().timeIntervalSince1970)", content: content, trigger: trigger ) notificationCenter.add(request) { error in if let error = error { print("DNP-NOTIFY-SCHEDULE: Failed to schedule notification: \(error)") } else { print("DNP-NOTIFY-SCHEDULE: Notification scheduled successfully") } } } private func calculateNextRunTime(from schedule: String) -> TimeInterval { // Simplified implementation - would use proper cron parsing in production // For now, return next day at 9 AM return 86400 // 24 hours } // MARK: - Static Daily Reminder Methods @objc func scheduleDailyReminder(_ call: CAPPluginCall) { guard let id = call.getString("id"), let title = call.getString("title"), let body = call.getString("body"), let time = call.getString("time") else { call.reject("Missing required parameters: id, title, body, time") return } let sound = call.getBool("sound", true) let vibration = call.getBool("vibration", true) let priority = call.getString("priority", "normal") let repeatDaily = call.getBool("repeatDaily", true) let timezone = call.getString("timezone") print("DNP-REMINDER: Scheduling daily reminder: \(id)") // Parse time (HH:mm format) let timeComponents = time.components(separatedBy: ":") guard timeComponents.count == 2, let hour = Int(timeComponents[0]), let minute = Int(timeComponents[1]), hour >= 0 && hour <= 23, minute >= 0 && minute <= 59 else { call.reject("Invalid time format. Use HH:mm (e.g., 09:00)") return } // Create notification content let content = UNMutableNotificationContent() content.title = title content.body = body content.sound = sound ? .default : nil content.categoryIdentifier = "DAILY_REMINDER" // Set priority switch priority { case "high": content.interruptionLevel = .critical case "low": content.interruptionLevel = .passive default: content.interruptionLevel = .active } // Create date components for daily trigger var dateComponents = DateComponents() dateComponents.hour = hour dateComponents.minute = minute let trigger = UNCalendarNotificationTrigger( dateMatching: dateComponents, repeats: repeatDaily ) let request = UNNotificationRequest( identifier: "reminder_\(id)", content: content, trigger: trigger ) // Store reminder in UserDefaults storeReminderInUserDefaults( id: id, title: title, body: body, time: time, sound: sound, vibration: vibration, priority: priority, repeatDaily: repeatDaily, timezone: timezone ) // Schedule the notification notificationCenter.add(request) { error in DispatchQueue.main.async { if let error = error { print("DNP-REMINDER: Failed to schedule reminder: \(error)") call.reject("Failed to schedule daily reminder: \(error.localizedDescription)") } else { print("DNP-REMINDER: Daily reminder scheduled successfully: \(id)") call.resolve() } } } } @objc func cancelDailyReminder(_ call: CAPPluginCall) { guard let reminderId = call.getString("reminderId") else { call.reject("Missing reminderId parameter") return } print("DNP-REMINDER: Cancelling daily reminder: \(reminderId)") // Cancel the notification notificationCenter.removePendingNotificationRequests(withIdentifiers: ["reminder_\(reminderId)"]) // Remove from UserDefaults removeReminderFromUserDefaults(id: reminderId) call.resolve() } @objc func getScheduledReminders(_ call: CAPPluginCall) { print("DNP-REMINDER: Getting scheduled reminders") // Get pending notifications notificationCenter.getPendingNotificationRequests { requests in let reminderRequests = requests.filter { $0.identifier.hasPrefix("reminder_") } // Get stored reminder data from UserDefaults let reminders = self.getRemindersFromUserDefaults() var result: [[String: Any]] = [] for reminder in reminders { let isScheduled = reminderRequests.contains { $0.identifier == "reminder_\(reminder["id"] as! String)" } var reminderInfo = reminder reminderInfo["isScheduled"] = isScheduled result.append(reminderInfo) } DispatchQueue.main.async { call.resolve(["reminders": result]) } } } @objc func updateDailyReminder(_ call: CAPPluginCall) { guard let reminderId = call.getString("reminderId") else { call.reject("Missing reminderId parameter") return } print("DNP-REMINDER: Updating daily reminder: \(reminderId)") // Cancel existing reminder notificationCenter.removePendingNotificationRequests(withIdentifiers: ["reminder_\(reminderId)"]) // Update in UserDefaults let title = call.getString("title") let body = call.getString("body") let time = call.getString("time") let sound = call.getBool("sound") let vibration = call.getBool("vibration") let priority = call.getString("priority") let repeatDaily = call.getBool("repeatDaily") let timezone = call.getString("timezone") updateReminderInUserDefaults( id: reminderId, title: title, body: body, time: time, sound: sound, vibration: vibration, priority: priority, repeatDaily: repeatDaily, timezone: timezone ) // Reschedule with new settings if all required fields are provided if let title = title, let body = body, let time = time { // Parse time let timeComponents = time.components(separatedBy: ":") guard timeComponents.count == 2, let hour = Int(timeComponents[0]), let minute = Int(timeComponents[1]) else { call.reject("Invalid time format") return } // Create new notification content let content = UNMutableNotificationContent() content.title = title content.body = body content.sound = (sound ?? true) ? .default : nil content.categoryIdentifier = "DAILY_REMINDER" // Set priority let finalPriority = priority ?? "normal" switch finalPriority { case "high": content.interruptionLevel = .critical case "low": content.interruptionLevel = .passive default: content.interruptionLevel = .active } // Create date components for daily trigger var dateComponents = DateComponents() dateComponents.hour = hour dateComponents.minute = minute let trigger = UNCalendarNotificationTrigger( dateMatching: dateComponents, repeats: repeatDaily ?? true ) let request = UNNotificationRequest( identifier: "reminder_\(reminderId)", content: content, trigger: trigger ) // Schedule the updated notification notificationCenter.add(request) { error in DispatchQueue.main.async { if let error = error { call.reject("Failed to reschedule updated reminder: \(error.localizedDescription)") } else { call.resolve() } } } } else { call.resolve() } } // MARK: - Helper Methods for Reminder Storage private func storeReminderInUserDefaults( id: String, title: String, body: String, time: String, sound: Bool, vibration: Bool, priority: String, repeatDaily: Bool, timezone: String? ) { let reminderData: [String: Any] = [ "id": id, "title": title, "body": body, "time": time, "sound": sound, "vibration": vibration, "priority": priority, "repeatDaily": repeatDaily, "timezone": timezone ?? "", "createdAt": Date().timeIntervalSince1970, "lastTriggered": 0 ] var reminders = UserDefaults.standard.array(forKey: "daily_reminders") as? [[String: Any]] ?? [] reminders.append(reminderData) UserDefaults.standard.set(reminders, forKey: "daily_reminders") print("DNP-REMINDER: Reminder stored: \(id)") } private func removeReminderFromUserDefaults(id: String) { var reminders = UserDefaults.standard.array(forKey: "daily_reminders") as? [[String: Any]] ?? [] reminders.removeAll { ($0["id"] as? String) == id } UserDefaults.standard.set(reminders, forKey: "daily_reminders") print("DNP-REMINDER: Reminder removed: \(id)") } private func getRemindersFromUserDefaults() -> [[String: Any]] { return UserDefaults.standard.array(forKey: "daily_reminders") as? [[String: Any]] ?? [] } private func updateReminderInUserDefaults( id: String, title: String?, body: String?, time: String?, sound: Bool?, vibration: Bool?, priority: String?, repeatDaily: Bool?, timezone: String? ) { var reminders = UserDefaults.standard.array(forKey: "daily_reminders") as? [[String: Any]] ?? [] for i in 0..