Files
daily-notification-plugin/ios/Plugin/DailyNotificationPlugin.swift
Matthew Raymer 8ded555a21 fix(ios): resolve compilation errors and enable successful build
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.
2025-11-04 22:22:02 -08:00

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)")
}
}