引入 AppDelegate、显式 Info.plist 与 entitlements,并将 PushNotificationManager 接入登录生命周期,用于 device token 上报与通知处理。 Co-authored-by: Cursor <cursoragent@cursor.com>
151 lines
5.3 KiB
Swift
151 lines
5.3 KiB
Swift
//
|
||
// 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))
|
||
}
|
||
}
|