From a7dd559c4a1129ff025b99ad691f7886194f8abf Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Tue, 11 Nov 2025 01:50:26 -0800 Subject: [PATCH] feat(ios): implement scheduleDailyNotification method Implemented main scheduling method for iOS plugin, matching Android functionality: Core Features: - Permission checking and requesting (iOS notification authorization) - Time parsing (HH:mm format) with validation - Next run time calculation (handles same-day and next-day scheduling) - UNUserNotificationCenter scheduling with daily repeat - Priority/interruption level support (iOS 15.0+) - Prefetch scheduling 5 minutes before notification (BGTaskScheduler) - Schedule storage in UserDefaults Implementation Details: - Checks notification authorization status before scheduling - Requests permission if not granted (equivalent to Android exact alarm permission) - Parses time string and calculates next occurrence - Creates UNCalendarNotificationTrigger for daily repeat - Schedules BGAppRefreshTask for prefetch 5 minutes before - Stores schedule metadata in UserDefaults for persistence Matches Android API: - Same parameter structure (time, title, body, sound, priority, url) - Same behavior (daily repeat, prefetch scheduling) - iOS-specific adaptations (UNUserNotificationCenter vs AlarmManager) This is the first critical method implementation (10/52 methods now complete). --- ios/Plugin/DailyNotificationPlugin.swift | 210 +++++++++++++++++++++++ 1 file changed, 210 insertions(+) diff --git a/ios/Plugin/DailyNotificationPlugin.swift b/ios/Plugin/DailyNotificationPlugin.swift index 37db6b2..8455aa8 100644 --- a/ios/Plugin/DailyNotificationPlugin.swift +++ b/ios/Plugin/DailyNotificationPlugin.swift @@ -121,6 +121,216 @@ public class DailyNotificationPlugin: CAPPlugin { } } + // MARK: - Main Scheduling Method + + /** + * Schedule a daily notification + * + * This is the main scheduling method, equivalent to Android's scheduleDailyNotification. + * Schedules both the notification and a prefetch 5 minutes before. + * + * @param call Plugin call with options: + * - time: String (required) - Time in HH:mm format (e.g., "09:00") + * - title: String (optional) - Notification title (default: "Daily Notification") + * - body: String (optional) - Notification body (default: "") + * - sound: Bool (optional) - Enable sound (default: true) + * - priority: String (optional) - Priority: "high", "default", "low" (default: "default") + * - url: String (optional) - URL for prefetch (optional, native fetcher used if registered) + */ + @objc func scheduleDailyNotification(_ call: CAPPluginCall) { + // Check notification permissions first + notificationCenter.getNotificationSettings { settings in + if settings.authorizationStatus != .authorized { + // Request permission if not granted + self.notificationCenter.requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in + DispatchQueue.main.async { + if let error = error { + print("DNP-PLUGIN: Permission request failed: \(error)") + call.reject("Notification permission request failed: \(error.localizedDescription)") + return + } + + if !granted { + print("DNP-PLUGIN: Notification permission denied") + call.reject("Notification permission denied. Please enable notifications in Settings.", "PERMISSION_DENIED") + return + } + + // Permission granted, proceed with scheduling + self.performScheduleDailyNotification(call: call) + } + } + } else { + // Permission already granted, proceed + self.performScheduleDailyNotification(call: call) + } + } + } + + /** + * Perform the actual scheduling after permission check + */ + private func performScheduleDailyNotification(call: CAPPluginCall) { + guard let options = call.options else { + call.reject("Options are required") + return + } + + guard let timeString = options["time"] as? String else { + call.reject("Time is required (format: HH:mm)") + return + } + + let title = options["title"] as? String ?? "Daily Notification" + let body = options["body"] as? String ?? "" + let sound = options["sound"] as? Bool ?? true + let priority = options["priority"] as? String ?? "default" + let url = options["url"] as? String // Optional URL for prefetch + + print("DNP-PLUGIN: Scheduling daily notification: time=\(timeString), title=\(title)") + + // Parse time (HH:mm format) + let timeComponents = timeString.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 + } + + // Calculate next run time + let calendar = Calendar.current + let now = Date() + var dateComponents = calendar.dateComponents([.year, .month, .day], from: now) + dateComponents.hour = hour + dateComponents.minute = minute + dateComponents.second = 0 + + guard var nextRunDate = calendar.date(from: dateComponents) else { + call.reject("Failed to calculate next run time") + return + } + + // If the time has already passed today, schedule for tomorrow + if nextRunDate <= now { + nextRunDate = calendar.date(byAdding: .day, value: 1, to: nextRunDate) ?? nextRunDate + } + + let nextRunTime = nextRunDate.timeIntervalSince1970 * 1000 // Convert to milliseconds + let nextRunTimeInterval = nextRunDate.timeIntervalSinceNow + + // Create notification content + let content = UNMutableNotificationContent() + content.title = title + content.body = body + content.sound = sound ? .default : nil + content.categoryIdentifier = "DAILY_NOTIFICATION" + + // Set priority/interruption level + if #available(iOS 15.0, *) { + switch priority.lowercased() { + case "high", "max": + content.interruptionLevel = .critical + case "low", "min": + content.interruptionLevel = .passive + default: + content.interruptionLevel = .active + } + } + + // Create date components for daily trigger + var triggerComponents = DateComponents() + triggerComponents.hour = hour + triggerComponents.minute = minute + + let trigger = UNCalendarNotificationTrigger( + dateMatching: triggerComponents, + repeats: true // Daily repeat + ) + + // Create unique identifier + let scheduleId = "daily_\(Int(Date().timeIntervalSince1970 * 1000))" + + let request = UNNotificationRequest( + identifier: scheduleId, + content: content, + trigger: trigger + ) + + // Schedule the notification + notificationCenter.add(request) { error in + if let error = error { + print("DNP-PLUGIN: Failed to schedule notification: \(error)") + call.reject("Failed to schedule notification: \(error.localizedDescription)") + return + } + + print("DNP-PLUGIN: Notification scheduled successfully: \(scheduleId)") + + // Schedule prefetch 5 minutes before notification + let fetchTime = nextRunTime - (5 * 60 * 1000) // 5 minutes before in milliseconds + let fetchTimeInterval = (fetchTime / 1000) - Date().timeIntervalSince1970 + + if fetchTimeInterval > 0 { + // Schedule background fetch task + do { + let fetchRequest = BGAppRefreshTaskRequest(identifier: self.fetchTaskIdentifier) + fetchRequest.earliestBeginDate = Date(timeIntervalSinceNow: fetchTimeInterval) + + try self.backgroundTaskScheduler.submit(fetchRequest) + print("DNP-PLUGIN: Prefetch scheduled: fetchTime=\(fetchTime), notificationTime=\(nextRunTime)") + } catch { + print("DNP-PLUGIN: Failed to schedule prefetch: \(error)") + // Don't fail the whole operation if prefetch scheduling fails + } + } else { + // Fetch time is in the past, trigger immediate fetch if possible + print("DNP-PLUGIN: Fetch time is in the past, skipping prefetch scheduling") + } + + // Store schedule in UserDefaults (similar to Android database storage) + self.storeScheduleInUserDefaults( + id: scheduleId, + time: timeString, + title: title, + body: body, + nextRunTime: nextRunTime + ) + + call.resolve() + } + } + + /** + * Store schedule in UserDefaults + */ + private func storeScheduleInUserDefaults( + id: String, + time: String, + title: String, + body: String, + nextRunTime: TimeInterval + ) { + let schedule: [String: Any] = [ + "id": id, + "kind": "notify", + "time": time, + "title": title, + "body": body, + "nextRunTime": nextRunTime, + "enabled": true, + "createdAt": Date().timeIntervalSince1970 * 1000 + ] + + var schedules = UserDefaults.standard.array(forKey: "DailyNotificationSchedules") as? [[String: Any]] ?? [] + schedules.append(schedule) + UserDefaults.standard.set(schedules, forKey: "DailyNotificationSchedules") + + print("DNP-PLUGIN: Schedule stored: \(id)") + } + // MARK: - Private Implementation Methods private func setupBackgroundTasks() {