import Foundation import UserNotifications import UIKit enum NotificationService { @MainActor static func requestPermission() async { _ = try? await UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) UIApplication.shared.registerForRemoteNotifications() } @MainActor private static var deviceToken: String? @MainActor private static var registeredSession: String? @MainActor static func receivedDeviceToken(_ data: Data) async { deviceToken = data.map { String(format: "%02x", $0) }.joined() registeredSession = nil await syncPushRegistration() } @MainActor static func syncPushRegistration() async { let session = KeychainStore.load() guard !session.isEmpty, let deviceToken, registeredSession != session else { return } let environment = Bundle.main.object(forInfoDictionaryKey: "KASZTELANPushEnvironment") as? String ?? "production" do { try await APIClient().registerPush(deviceToken: deviceToken, environment: environment, token: session) // The account may have changed while the request was in flight. if KeychainStore.load() == session { registeredSession = session } } catch { // Retry when the app next refreshes; keep account access working offline. } } @MainActor static func unregisterPush(session: String) async { registeredSession = nil guard let deviceToken else { return } try? await APIClient().unregisterPush(deviceToken: deviceToken, token: session) } static func recordReceivedPush(_ content: UNNotificationContent) { guard let id = content.userInfo["message_id"] as? Int, id > 0 else { return } var received = UserDefaults.standard.array(forKey: "kasztelan.push.receivedIDs") as? [Int] ?? [] received.removeAll { $0 == id } received.append(id) UserDefaults.standard.set(Array(received.suffix(100)), forKey: "kasztelan.push.receivedIDs") } static func show(_ message: KasztelanMessage) async { let received = UserDefaults.standard.array(forKey: "kasztelan.push.receivedIDs") as? [Int] ?? [] guard !received.contains(message.id) else { return } let settings = await UNUserNotificationCenter.current().notificationSettings() guard settings.authorizationStatus == .authorized || settings.authorizationStatus == .provisional else { return } let delivered = await UNUserNotificationCenter.current().deliveredNotifications() guard !delivered.contains(where: { ($0.request.content.userInfo["message_id"] as? Int) == message.id }) else { return } let content = UNMutableNotificationContent() content.title = "KASZTELAN" content.subtitle = message.title content.body = message.text content.sound = .default content.threadIdentifier = "kasztelan-messages" content.categoryIdentifier = "KASZTELAN_MESSAGE" content.userInfo = ["message_id": message.id] let request = UNNotificationRequest(identifier: "kasztelan-\(message.id)", content: content, trigger: nil) try? await UNUserNotificationCenter.current().add(request) } }