Fixed critical compilation errors preventing iOS plugin build: - Updated logger API calls from logger.debug(TAG, msg) to logger.log(.debug, msg) across all iOS plugin files to match DailyNotificationLogger interface - Fixed async/await concurrency in makeConditionalRequest using semaphore pattern - Fixed NotificationContent immutability by creating new instances instead of mutation - Changed private access control to internal for extension-accessible methods - Added iOS 15.0+ availability checks for interruptionLevel property - Fixed static member references using Self.MEMBER_NAME syntax - Added missing .scheduling case to exhaustive switch statement - Fixed variable initialization in retry state closures Added DailyNotificationStorage.swift implementation matching Android pattern. Updated build scripts with improved error reporting and full log visibility. iOS plugin now compiles successfully. All build errors resolved.
484 lines
17 KiB
Swift
484 lines
17 KiB
Swift
//
|
|
// 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 {
|
|
|
|
let notificationCenter = UNUserNotificationCenter.current()
|
|
let backgroundTaskScheduler = BGTaskScheduler.shared
|
|
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
|
|
if #available(iOS 15.0, *) {
|
|
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"
|
|
if #available(iOS 15.0, *) {
|
|
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..<reminders.count {
|
|
if reminders[i]["id"] as? String == id {
|
|
if let title = title { reminders[i]["title"] = title }
|
|
if let body = body { reminders[i]["body"] = body }
|
|
if let time = time { reminders[i]["time"] = time }
|
|
if let sound = sound { reminders[i]["sound"] = sound }
|
|
if let vibration = vibration { reminders[i]["vibration"] = vibration }
|
|
if let priority = priority { reminders[i]["priority"] = priority }
|
|
if let repeatDaily = repeatDaily { reminders[i]["repeatDaily"] = repeatDaily }
|
|
if let timezone = timezone { reminders[i]["timezone"] = timezone }
|
|
break
|
|
}
|
|
}
|
|
|
|
UserDefaults.standard.set(reminders, forKey: "daily_reminders")
|
|
print("DNP-REMINDER: Reminder updated: \(id)")
|
|
}
|
|
} |