新增 APNs 推送注册与 Payload 路由
引入 AppDelegate、显式 Info.plist 与 entitlements,并将 PushNotificationManager 接入登录生命周期,用于 device token 上报与通知处理。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
150
suixinkan/Core/Push/PushNotificationManager.swift
Normal file
150
suixinkan/Core/Push/PushNotificationManager.swift
Normal 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))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user