// Sources/Core/Push/NotificationDelegate.swift // T-2.9: APNs — foreground presentation control + tap handling import UserNotifications import UIKit /// Centralised `UNUserNotificationCenterDelegate`. /// /// Responsibilities: /// - Suppresses the banner when the user is already viewing the relevant /// terminal session (`visibleSessionId`). /// - Posts `"piRemote.openSession"` when the user taps a notification, /// carrying the session-id as the notification object, so any subscriber /// (e.g. the root navigation stack) can navigate to the right session. @MainActor final class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate, ObservableObject { // MARK: - Singleton static let shared = NotificationDelegate() private override init() {} // MARK: - State /// Set by the visible terminal view so foreground banners can be suppressed. var visibleSessionId: String? = nil // MARK: - Setup /// Wire this delegate into `UNUserNotificationCenter`. /// Call once from `piRemoteApp.onAppear`. func setup() { UNUserNotificationCenter.current().delegate = self } // MARK: - Permission /// Request alert + sound + badge authorisation. /// - Returns: `true` if the user granted permission. func requestPermission() async -> Bool { do { return try await UNUserNotificationCenter.current() .requestAuthorization(options: [.alert, .sound, .badge]) } catch { return false } } // MARK: - UNUserNotificationCenterDelegate /// Foreground presentation: suppress the banner when the relevant session /// is already on-screen. nonisolated func userNotificationCenter( _ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void ) { let sessionId = notification.request.content.userInfo["sessionId"] as? String Task { @MainActor in if let sessionId, sessionId == self.visibleSessionId { // Session is already visible — suppress the banner entirely. completionHandler([]) } else { // Show banner + sound + badge update. completionHandler([.banner, .sound, .badge]) } } } /// Tap handling: post `"piRemote.openSession"` so any subscriber can /// navigate to the correct session without a tight coupling. nonisolated func userNotificationCenter( _ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void ) { let sessionId = response.notification.request.content.userInfo["sessionId"] as? String Task { @MainActor in NotificationCenter.default.post( name: Notification.Name("piRemote.openSession"), object: sessionId ) completionHandler() } } }