You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
480 lines
17 KiB
480 lines
17 KiB
//
|
|
// 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..<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)")
|
|
}
|
|
}
|