Files
suixinkan_ios_new/suixinkan/Core/Push/PushNotificationManager.swift
汉秋 0314033a7f 新增 APNs 推送注册与 Payload 路由
引入 AppDelegate、显式 Info.plist 与 entitlements,并将 PushNotificationManager 接入登录生命周期,用于 device token 上报与通知处理。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-26 09:18:49 +08:00

151 lines
5.3 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// 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) {
guard let router else { return }
switch payload.route {
case .payment:
navigateHomeRoute(.paymentCollection)
case .order:
router.selectOrders(entry: .storeOrders)
case .verificationOrder:
router.selectOrders(entry: .verificationOrders)
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))
}
}