From bf489878ddb6307e830925487ebf1f5ba6f0be9e Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Mon, 8 Sep 2025 16:08:19 -0500 Subject: [PATCH] Signed-off-by: Matt Bruce --- TheNoiseClock/App/TheNoiseClockApp.swift | 6 + TheNoiseClock/Services/FocusModeService.swift | 2 +- .../Services/NotificationDelegate.swift | 179 ++++++++++++++++++ TheNoiseClock/ViewModels/AlarmViewModel.swift | 3 + 4 files changed, 189 insertions(+), 1 deletion(-) create mode 100644 TheNoiseClock/Services/NotificationDelegate.swift diff --git a/TheNoiseClock/App/TheNoiseClockApp.swift b/TheNoiseClock/App/TheNoiseClockApp.swift index 3713687..2c002b4 100644 --- a/TheNoiseClock/App/TheNoiseClockApp.swift +++ b/TheNoiseClock/App/TheNoiseClockApp.swift @@ -11,6 +11,12 @@ import SwiftUI @main struct TheNoiseClockApp: App { + // MARK: - Initialization + init() { + // Initialize notification delegate to handle snooze actions + _ = NotificationDelegate.shared + } + // MARK: - Body var body: some Scene { WindowGroup { diff --git a/TheNoiseClock/Services/FocusModeService.swift b/TheNoiseClock/Services/FocusModeService.swift index 3c7729b..2670e5c 100644 --- a/TheNoiseClock/Services/FocusModeService.swift +++ b/TheNoiseClock/Services/FocusModeService.swift @@ -70,7 +70,7 @@ class FocusModeService { UNNotificationAction( identifier: "SNOOZE_ACTION", title: "Snooze", - options: [.foreground] + options: [] ), UNNotificationAction( identifier: "STOP_ACTION", diff --git a/TheNoiseClock/Services/NotificationDelegate.swift b/TheNoiseClock/Services/NotificationDelegate.swift new file mode 100644 index 0000000..646c000 --- /dev/null +++ b/TheNoiseClock/Services/NotificationDelegate.swift @@ -0,0 +1,179 @@ +// +// NotificationDelegate.swift +// TheNoiseClock +// +// Created by Matt Bruce on 9/8/25. +// + +import UserNotifications +import Foundation + +/// Delegate to handle notification actions (snooze, stop, etc.) +class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate { + + // MARK: - Singleton + static let shared = NotificationDelegate() + + // MARK: - Properties + private var alarmService: AlarmService? + + // MARK: - Initialization + private override init() { + super.init() + setupNotificationCenter() + } + + // MARK: - Setup + private func setupNotificationCenter() { + UNUserNotificationCenter.current().delegate = self + print("🔔 Notification delegate configured") + } + + /// Set the alarm service instance (called from AlarmViewModel) + func setAlarmService(_ service: AlarmService) { + self.alarmService = service + } + + // MARK: - UNUserNotificationCenterDelegate + + /// Handle notification actions when app is in foreground + func userNotificationCenter( + _ center: UNUserNotificationCenter, + didReceive response: UNNotificationResponse, + withCompletionHandler completionHandler: @escaping () -> Void + ) { + let actionIdentifier = response.actionIdentifier + let notification = response.notification + let userInfo = notification.request.content.userInfo + + print("🔔 Notification action received: \(actionIdentifier)") + + switch actionIdentifier { + case "SNOOZE_ACTION": + handleSnoozeAction(userInfo: userInfo) + case "STOP_ACTION": + handleStopAction(userInfo: userInfo) + case UNNotificationDefaultActionIdentifier: + // User tapped the notification itself + handleNotificationTap(userInfo: userInfo) + default: + print("🔔 Unknown action: \(actionIdentifier)") + } + + completionHandler() + } + + /// Handle notifications when app is in foreground + func userNotificationCenter( + _ center: UNUserNotificationCenter, + willPresent notification: UNNotification, + withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void + ) { + // Show notification even when app is in foreground + completionHandler([.banner, .sound, .badge]) + } + + // MARK: - Action Handlers + + private func handleSnoozeAction(userInfo: [AnyHashable: Any]) { + guard let alarmIdString = userInfo["alarmId"] as? String, + let alarmId = UUID(uuidString: alarmIdString), + let alarmService = self.alarmService, + let alarm = alarmService.getAlarm(id: alarmId) else { + print("❌ Could not find alarm for snooze action") + return + } + + print("🔔 Snoozing alarm: \(alarm.label) for \(alarm.snoozeDuration) minutes") + + // Calculate snooze time (current time + snooze duration) + let snoozeTime = Date().addingTimeInterval(TimeInterval(alarm.snoozeDuration * 60)) + print("🔔 Snooze time: \(snoozeTime)") + print("🔔 Current time: \(Date())") + + // Create a temporary alarm for the snooze + let snoozeAlarm = Alarm( + id: UUID(), // New ID for snooze alarm + time: snoozeTime, + isEnabled: true, + soundName: alarm.soundName, + label: "\(alarm.label) (Snoozed)", + notificationMessage: "Snoozed: \(alarm.notificationMessage)", + snoozeDuration: alarm.snoozeDuration, + isVibrationEnabled: alarm.isVibrationEnabled, + isLightFlashEnabled: alarm.isLightFlashEnabled, + volume: alarm.volume + ) + + // Schedule the snooze notification + Task { + await scheduleSnoozeNotification(snoozeAlarm, userInfo: userInfo) + } + } + + private func handleStopAction(userInfo: [AnyHashable: Any]) { + guard let alarmIdString = userInfo["alarmId"] as? String, + let alarmId = UUID(uuidString: alarmIdString) else { + print("❌ Could not find alarm ID for stop action") + return + } + + print("🔔 Stopping alarm: \(alarmId)") + + // Cancel any pending notifications for this alarm + UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: [alarmIdString]) + + // If this was a snooze alarm, we don't want to disable the original alarm + // Just cancel the current notification + } + + private func handleNotificationTap(userInfo: [AnyHashable: Any]) { + guard let alarmIdString = userInfo["alarmId"] as? String, + let alarmId = UUID(uuidString: alarmIdString) else { + print("❌ Could not find alarm ID for notification tap") + return + } + + print("🔔 Notification tapped for alarm: \(alarmId)") + + // For now, just log the tap. In the future, this could open the alarm details + // or perform some other action when the user taps the notification + } + + // MARK: - Private Methods + + private func scheduleSnoozeNotification(_ snoozeAlarm: Alarm, userInfo: [AnyHashable: Any]) async { + let content = UNMutableNotificationContent() + content.title = snoozeAlarm.label + content.body = snoozeAlarm.notificationMessage + content.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: snoozeAlarm.soundName)) + content.categoryIdentifier = "ALARM_CATEGORY" + content.userInfo = [ + "alarmId": snoozeAlarm.id.uuidString, + "soundName": snoozeAlarm.soundName, + "isSnooze": true, + "originalAlarmId": userInfo["alarmId"] as? String ?? "" + ] + + // Create trigger for snooze time + let trigger = UNTimeIntervalNotificationTrigger( + timeInterval: snoozeAlarm.time.timeIntervalSinceNow, + repeats: false + ) + + // Create request + let request = UNNotificationRequest( + identifier: snoozeAlarm.id.uuidString, + content: content, + trigger: trigger + ) + + // Schedule notification + do { + try await UNUserNotificationCenter.current().add(request) + print("🔔 Snooze notification scheduled for \(snoozeAlarm.time)") + } catch { + print("❌ Error scheduling snooze notification: \(error)") + } + } +} diff --git a/TheNoiseClock/ViewModels/AlarmViewModel.swift b/TheNoiseClock/ViewModels/AlarmViewModel.swift index c0c6152..e372188 100644 --- a/TheNoiseClock/ViewModels/AlarmViewModel.swift +++ b/TheNoiseClock/ViewModels/AlarmViewModel.swift @@ -29,6 +29,9 @@ class AlarmViewModel { notificationService: NotificationService = NotificationService()) { self.alarmService = alarmService self.notificationService = notificationService + + // Register alarm service with notification delegate for snooze handling + NotificationDelegate.shared.setAlarmService(alarmService) } // MARK: - Public Interface