/** * DailyNotificationPlugin.swift * * Main iOS plugin class for handling daily notifications * * @author Matthew Raymer * @version 1.0.0 */ import Foundation import Capacitor import BackgroundTasks import UserNotifications @objc(DailyNotificationPlugin) public class DailyNotificationPlugin: CAPPlugin { private static let TAG = "DailyNotificationPlugin" private var database: DailyNotificationDatabase? private var ttlEnforcer: DailyNotificationTTLEnforcer? private var rollingWindow: DailyNotificationRollingWindow? private var backgroundTaskManager: DailyNotificationBackgroundTaskManager? private var useSharedStorage: Bool = false private var databasePath: String? private var ttlSeconds: TimeInterval = 3600 private var prefetchLeadMinutes: Int = 15 public override func load() { super.load() print("\(Self.TAG): DailyNotificationPlugin loading") initializeComponents() if #available(iOS 13.0, *) { backgroundTaskManager?.registerBackgroundTask() } print("\(Self.TAG): DailyNotificationPlugin loaded successfully") } private func initializeComponents() { if useSharedStorage, let databasePath = databasePath { database = DailyNotificationDatabase(path: databasePath) } ttlEnforcer = DailyNotificationTTLEnforcer(database: database, useSharedStorage: useSharedStorage) rollingWindow = DailyNotificationRollingWindow(ttlEnforcer: ttlEnforcer!, database: database, useSharedStorage: useSharedStorage) if #available(iOS 13.0, *) { backgroundTaskManager = DailyNotificationBackgroundTaskManager(database: database, ttlEnforcer: ttlEnforcer!, rollingWindow: rollingWindow!) } print("\(Self.TAG): All components initialized successfully") } @objc func configure(_ call: CAPPluginCall) { print("\(Self.TAG): Configuring plugin") if let dbPath = call.getString("dbPath") { databasePath = dbPath } if let storage = call.getString("storage") { useSharedStorage = (storage == "shared") } if let ttl = call.getDouble("ttlSeconds") { ttlSeconds = ttl } if let leadMinutes = call.getInt("prefetchLeadMinutes") { prefetchLeadMinutes = leadMinutes } storeConfiguration() initializeComponents() call.resolve() } private func storeConfiguration() { if useSharedStorage, let database = database { // Store in SQLite print("\(Self.TAG): Storing configuration in SQLite") } else { // Store in UserDefaults UserDefaults.standard.set(databasePath, forKey: "databasePath") UserDefaults.standard.set(useSharedStorage, forKey: "useSharedStorage") UserDefaults.standard.set(ttlSeconds, forKey: "ttlSeconds") UserDefaults.standard.set(prefetchLeadMinutes, forKey: "prefetchLeadMinutes") } } @objc func maintainRollingWindow(_ call: CAPPluginCall) { print("\(Self.TAG): Manual rolling window maintenance requested") if let rollingWindow = rollingWindow { rollingWindow.forceMaintenance() call.resolve() } else { call.reject("Rolling window not initialized") } } @objc func getRollingWindowStats(_ call: CAPPluginCall) { print("\(Self.TAG): Rolling window stats requested") if let rollingWindow = rollingWindow { let stats = rollingWindow.getRollingWindowStats() let result = [ "stats": stats, "maintenanceNeeded": rollingWindow.isMaintenanceNeeded(), "timeUntilNextMaintenance": rollingWindow.getTimeUntilNextMaintenance() ] as [String : Any] call.resolve(result) } else { call.reject("Rolling window not initialized") } } @objc func scheduleBackgroundTask(_ call: CAPPluginCall) { print("\(Self.TAG): Scheduling background task") guard let scheduledTimeString = call.getString("scheduledTime") else { call.reject("scheduledTime parameter is required") return } let formatter = DateFormatter() formatter.dateFormat = "HH:mm" guard let scheduledTime = formatter.date(from: scheduledTimeString) else { call.reject("Invalid scheduledTime format") return } if #available(iOS 13.0, *) { backgroundTaskManager?.scheduleBackgroundTask(scheduledTime: scheduledTime, prefetchLeadMinutes: prefetchLeadMinutes) call.resolve() } else { call.reject("Background tasks not available on this iOS version") } } @objc func getBackgroundTaskStatus(_ call: CAPPluginCall) { print("\(Self.TAG): Background task status requested") if #available(iOS 13.0, *) { let status = backgroundTaskManager?.getBackgroundTaskStatus() ?? [:] call.resolve(status) } else { call.resolve(["available": false, "reason": "iOS version not supported"]) } } @objc func cancelAllBackgroundTasks(_ call: CAPPluginCall) { print("\(Self.TAG): Cancelling all background tasks") if #available(iOS 13.0, *) { backgroundTaskManager?.cancelAllBackgroundTasks() call.resolve() } else { call.reject("Background tasks not available on this iOS version") } } @objc func getTTLViolationStats(_ call: CAPPluginCall) { print("\(Self.TAG): TTL violation stats requested") if let ttlEnforcer = ttlEnforcer { let stats = ttlEnforcer.getTTLViolationStats() call.resolve(["stats": stats]) } else { call.reject("TTL enforcer not initialized") } } @objc func scheduleDailyNotification(_ call: CAPPluginCall) { print("\(Self.TAG): Scheduling daily notification") guard let time = call.getString("time") else { call.reject("Time parameter is required") return } let formatter = DateFormatter() formatter.dateFormat = "HH:mm" guard let scheduledTime = formatter.date(from: time) else { call.reject("Invalid time format") return } let notification = NotificationContent( id: UUID().uuidString, title: call.getString("title") ?? "Daily Update", body: call.getString("body") ?? "Your daily notification is ready", scheduledTime: scheduledTime.timeIntervalSince1970 * 1000, fetchedAt: Date().timeIntervalSince1970 * 1000, url: call.getString("url"), payload: nil, etag: nil ) if let ttlEnforcer = ttlEnforcer, !ttlEnforcer.validateBeforeArming(notification) { call.reject("Notification content violates TTL") return } scheduleNotification(notification) if #available(iOS 13.0, *) { backgroundTaskManager?.scheduleBackgroundTask(scheduledTime: scheduledTime, prefetchLeadMinutes: prefetchLeadMinutes) } call.resolve() } private func scheduleNotification(_ notification: NotificationContent) { let content = UNMutableNotificationContent() content.title = notification.title ?? "Daily Notification" content.body = notification.body ?? "Your daily notification is ready" content.sound = UNNotificationSound.default let scheduledTime = Date(timeIntervalSince1970: notification.scheduledTime / 1000) let trigger = UNCalendarNotificationTrigger(dateMatching: Calendar.current.dateComponents([.year, .month, .day, .hour, .minute], from: scheduledTime), repeats: false) let request = UNNotificationRequest(identifier: notification.id, content: content, trigger: trigger) UNUserNotificationCenter.current().add(request) { error in if let error = error { print("\(Self.TAG): Failed to schedule notification \(notification.id): \(error)") } else { print("\(Self.TAG): Successfully scheduled notification: \(notification.id)") } } } @objc func getLastNotification(_ call: CAPPluginCall) { let result = [ "id": "placeholder", "title": "Last Notification", "body": "This is a placeholder", "timestamp": Date().timeIntervalSince1970 * 1000 ] as [String : Any] call.resolve(result) } @objc func cancelAllNotifications(_ call: CAPPluginCall) { UNUserNotificationCenter.current().removeAllPendingNotificationRequests() call.resolve() } }