新增 APNs 推送注册与 Payload 路由

引入 AppDelegate、显式 Info.plist 与 entitlements,并将 PushNotificationManager 接入登录生命周期,用于 device token 上报与通知处理。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-26 09:18:49 +08:00
parent a04168cf30
commit 0314033a7f
10 changed files with 561 additions and 26 deletions

View File

@ -0,0 +1,150 @@
//
// 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))
}
}