Add APNs push notification registration and payload routing.

Introduce AppDelegate, explicit Info.plist and entitlements, and wire PushNotificationManager into login lifecycle for device token upload and notification handling.

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

View File

@ -0,0 +1,41 @@
//
// AppDelegate.swift
// suixinkan
//
// Created by Codex on 2026/6/26.
//
import UIKit
/// AppDelegate APNs SwiftUI App adaptor
final class AppDelegate: NSObject, UIApplicationDelegate {
func application(
_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
) {
Task { @MainActor in
PushNotificationManager.shared.handleDeviceToken(deviceToken)
}
}
func application(
_ application: UIApplication,
didFailToRegisterForRemoteNotificationsWithError error: Error
) {
Task { @MainActor in
PushNotificationManager.shared.handleRegistrationError(error)
}
}
func application(
_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable: Any],
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void
) {
let payload = PushPayload(userInfo: userInfo)
Task { @MainActor in
PushNotificationManager.shared.handleRemoteNotification(payload)
completionHandler(.newData)
}
}
}

View File

@ -28,6 +28,7 @@ struct RootView: View {
@State private var statisticsAPI: StatisticsAPI
@State private var paymentAPI: PaymentAPI
@State private var walletAPI: WalletAPI
@State private var pushAPI: PushAPI
@State private var scenicPermissionAPI: ScenicPermissionAPI
@State private var scenicSettlementAPI: ScenicSettlementAPI
@State private var messageCenterAPI: MessageCenterAPI
@ -63,6 +64,7 @@ struct RootView: View {
_statisticsAPI = State(initialValue: StatisticsAPI(client: apiClient))
_paymentAPI = State(initialValue: PaymentAPI(client: apiClient))
_walletAPI = State(initialValue: WalletAPI(client: apiClient))
_pushAPI = State(initialValue: PushAPI(client: apiClient))
_scenicPermissionAPI = State(initialValue: ScenicPermissionAPI(client: apiClient))
_scenicSettlementAPI = State(initialValue: ScenicSettlementAPI(client: apiClient))
_messageCenterAPI = State(initialValue: MessageCenterAPI(client: apiClient))
@ -114,6 +116,7 @@ struct RootView: View {
.environment(statisticsAPI)
.environment(paymentAPI)
.environment(walletAPI)
.environment(pushAPI)
.environment(scenicPermissionAPI)
.environment(scenicSettlementAPI)
.environment(messageCenterAPI)
@ -132,6 +135,7 @@ struct RootView: View {
.environment(authSessionCoordinator)
.task {
apiClient.bindAuthTokenProvider { appSession.token }
PushNotificationManager.shared.configure(api: pushAPI, session: appSession, router: appRouter)
await globalLoading.withLoading {
await sessionBootstrapper.restore(
appSession: appSession,
@ -142,6 +146,9 @@ struct RootView: View {
toastCenter: toastCenter
)
}
if appSession.isLoggedIn {
PushNotificationManager.shared.requestAuthorizationAndRegister()
}
}
.task(id: scenicSpotTaskID) {
guard appSession.isLoggedIn else {
@ -176,12 +183,18 @@ struct RootView: View {
}
}
.onChange(of: appSession.phase) { _, phase in
guard phase == .loggedOut else { return }
accountContext.reset()
permissionContext.reset()
scenicSpotContext.reset()
appRouter.reset()
toastCenter.dismiss()
switch phase {
case .loggedIn:
PushNotificationManager.shared.requestAuthorizationAndRegister()
case .loggedOut:
accountContext.reset()
permissionContext.reset()
scenicSpotContext.reset()
appRouter.reset()
toastCenter.dismiss()
case .restoring:
break
}
}
}

View File

@ -10,6 +10,8 @@ import SwiftUI
/// SwiftUI
@main
struct suixinkanApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
var body: some Scene {
WindowGroup {
RootView()