pi-remote-ios/Sources/Core/Push/NotificationDelegate.swift

90 lines
3.0 KiB
Swift

// 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
nonisolated(unsafe) let ch = completionHandler
Task { @MainActor in
if let sessionId, sessionId == self.visibleSessionId {
ch([])
} else {
ch([.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
nonisolated(unsafe) let ch = completionHandler
Task { @MainActor in
NotificationCenter.default.post(
name: Notification.Name("piRemote.openSession"),
object: sessionId
)
ch()
}
}
}