// Local notification handler for watchOS timer alerts import Foundation import UserNotifications import os @MainActor final class WatchNotificationHandler: NSObject, ObservableObject { static let shared = WatchNotificationHandler() private let logger = Logger(subsystem: "com.chronomind.watch", category: "Notifications") private let center = UNUserNotificationCenter.current() static let timerFiredCategory = "CHRONOMIND_TIMER_FIRED" static let snoozeAction = "SNOOZE_ACTION" static let dismissAction = "DISMISS_ACTION" private override init() { super.init() } // MARK: - Setup func requestAuthorization() { center.requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in Task { @MainActor in if let error = error { self.logger.error("Notification auth error: \(error.localizedDescription)") return } self.logger.info("Notification auth granted: \(granted)") self.registerCategories() } } } private func registerCategories() { let snooze = UNNotificationAction( identifier: Self.snoozeAction, title: "Snooze 5m", options: [] ) let dismiss = UNNotificationAction( identifier: Self.dismissAction, title: "Dismiss", options: [.destructive] ) let category = UNNotificationCategory( identifier: Self.timerFiredCategory, actions: [snooze, dismiss], intentIdentifiers: [], options: [] ) center.setNotificationCategories([category]) center.delegate = self logger.info("Notification categories registered") } // MARK: - Schedule func scheduleTimerNotification(id: String, label: String, targetTime: Date, urgency: UrgencyLevel) { let interval = targetTime.timeIntervalSinceNow guard interval > 0 else { return } let content = UNMutableNotificationContent() content.title = "Timer Fired" content.body = label content.sound = urgency == .critical ? .defaultCritical : .default content.categoryIdentifier = Self.timerFiredCategory content.userInfo = ["timerId": id, "urgency": urgency.rawValue] let trigger = UNTimeIntervalNotificationTrigger(timeInterval: interval, repeats: false) let request = UNNotificationRequest(identifier: "timer-\(id)", content: content, trigger: trigger) center.add(request) { error in Task { @MainActor in if let error = error { self.logger.error("Failed to schedule notification: \(error.localizedDescription)") } else { self.logger.info("Scheduled notification for timer \(id) in \(Int(interval))s") } } } } func cancelNotification(for timerId: String) { center.removePendingNotificationRequests(withIdentifiers: ["timer-\(timerId)"]) } } // MARK: - UNUserNotificationCenterDelegate extension WatchNotificationHandler: UNUserNotificationCenterDelegate { nonisolated func userNotificationCenter( _ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void ) { let userInfo = response.notification.request.content.userInfo guard let timerId = userInfo["timerId"] as? String else { completionHandler() return } Task { @MainActor in let sessionManager = WatchSessionManager.shared switch response.actionIdentifier { case Self.snoozeAction: sessionManager.sendCommand(.snooze(id: timerId, minutes: 5)) logger.info("Snooze action for timer \(timerId)") case Self.dismissAction: sessionManager.sendCommand(.dismiss(id: timerId)) logger.info("Dismiss action for timer \(timerId)") default: break } } completionHandler() } nonisolated func userNotificationCenter( _ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void ) { completionHandler([.banner, .sound]) } }