// // PushNotificationManager.swift // suixinkan // // Created by Codex on 2026/6/26. // import UIKit import UserNotifications @MainActor /// 推送生命周期管理器,负责权限、APNs 注册、token 上报和通知点击路由。 final class PushNotificationManager: NSObject, UNUserNotificationCenterDelegate { static let shared = PushNotificationManager() private var api: PushAPI? private weak var session: AppSession? private weak var router: AppRouter? private var latestDeviceToken: String? private var latestAuthorizationStatus: UNAuthorizationStatus = .notDetermined private let tokenDefaultsKey = "apns_device_token" private let uploadedTokenDefaultsKey = "apns_uploaded_token" override init() { super.init() } /// 绑定推送所需的网络、登录和导航依赖。 func configure(api: PushAPI, session: AppSession, router: AppRouter) { self.api = api self.session = session self.router = router UNUserNotificationCenter.current().delegate = self latestDeviceToken = UserDefaults.standard.string(forKey: tokenDefaultsKey) } /// 请求通知权限并注册 APNs;已授权时会直接注册并尝试补传缓存 token。 func requestAuthorizationAndRegister() { Task { @MainActor in let settings = await UNUserNotificationCenter.current().notificationSettings() latestAuthorizationStatus = settings.authorizationStatus switch settings.authorizationStatus { case .notDetermined: let granted = (try? await UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound])) ?? false guard granted else { return } UIApplication.shared.registerForRemoteNotifications() case .authorized, .provisional, .ephemeral: UIApplication.shared.registerForRemoteNotifications() uploadPendingTokenIfPossible() case .denied: break @unknown default: break } } } /// 处理系统返回的 APNs token。 func handleDeviceToken(_ deviceToken: Data) { let token = APNsDeviceToken.hexString(from: deviceToken) latestDeviceToken = token UserDefaults.standard.set(token, forKey: tokenDefaultsKey) uploadPendingTokenIfPossible() } /// 处理 APNs 注册失败。失败不阻断登录和主流程。 func handleRegistrationError(_ error: Error) { #if DEBUG print("APNs registration failed: \(error.localizedDescription)") #endif } /// 登录后补传尚未成功上报的 token。 func uploadPendingTokenIfPossible() { guard let api, session?.isLoggedIn == true else { return } guard let token = latestDeviceToken, !token.isEmpty else { return } let uploaded = UserDefaults.standard.string(forKey: uploadedTokenDefaultsKey) guard uploaded != token else { return } Task { do { try await api.registerJPushId(token) UserDefaults.standard.set(token, forKey: uploadedTokenDefaultsKey) } catch { #if DEBUG print("Push token upload failed: \(error.localizedDescription)") #endif } } } /// 处理静默或远程通知 userInfo。 func handleRemoteNotification(_ userInfo: [AnyHashable: Any]) { route(from: PushPayload(userInfo: userInfo)) } /// 处理已解析的推送 payload。供系统回调和单测复用。 func handleRemoteNotification(_ payload: PushPayload) { route(from: payload) } nonisolated func userNotificationCenter( _ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void ) { let payload = PushPayload(userInfo: notification.request.content.userInfo) Task { @MainActor in handleRemoteNotification(payload) } completionHandler([.banner, .list, .sound, .badge]) } nonisolated func userNotificationCenter( _ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void ) { let payload = PushPayload(userInfo: response.notification.request.content.userInfo) Task { @MainActor in handleRemoteNotification(payload) completionHandler() } } private func route(from payload: PushPayload) { if let type = payload.paymentEventType, type == 1 || type == 6 { PaymentPushEventCenter.shared.post(type: type, dataJSON: payload.paymentEventData) if type == 1 { handlePaymentVoiceIfNeeded(payload: payload) } } guard let router else { return } switch payload.route { case .payment: navigateHomeRoute(.paymentCollection) case .order: router.selectOrders(entry: .storeOrders) case .verificationOrder: router.openWriteOffList() case .task: navigateHomeRoute(.taskManagement) case .queue: navigateHomeRoute(.queueManagement) case .messageCenter: navigateHomeRoute(.messageCenter) } } private func navigateHomeRoute(_ route: HomeRoute) { guard let router else { return } router.select(.home) router.router(for: .home).navigate(to: .home(route)) } /// 付款成功推送且语音开关开启时播报到账金额。 private func handlePaymentVoiceIfNeeded(payload: PushPayload) { let preferences = AppPreferencesStore() guard preferences.loadPaymentReceiveVoiceEnabled(), let amount = payload.paymentSuccessAmount, !amount.isEmpty else { return } PaymentVoiceSpeaker.shared.speak("收款到账\(amount)元") } }