learning_ai_clock/ios/ChronoMindWatch/WatchNotificationHandler.swift
saravanakumardb1 d179c4c624 feat(watch,mac): complete watchOS + macOS companion targets
- watchOS: add WatchSessionManager (WCSession bridge), WatchNotificationHandler
  (snooze/dismiss actions), recommendations() for AppIntentTimelineProvider
- watchOS: WatchTimerStore tick loop with cascade haptics, App Group sync
- macOS: CreateTimerSheet (countdown/alarm/pomodoro), launch-at-login toggle
- macOS: MenuBarState showCreateSheet, MacSettingsView data tab
- Xcode project updated with new file references
2026-03-27 11:28:13 -07:00

132 lines
4.4 KiB
Swift

// 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])
}
}