From 70d46da14a7ebd50a29bf21a42f4cfb933432114 Mon Sep 17 00:00:00 2001 From: Trey t Date: Thu, 4 Dec 2025 20:18:08 -0600 Subject: [PATCH] Add smart device token caching for push notification registration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cache device token in UserDefaults and only register with backend when token changes. Also registers when app returns from background if token differs from cached value, reducing unnecessary API calls. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../PushNotificationManager.swift | 50 ++++++++++++++++++- iosApp/iosApp/RootView.swift | 6 +++ iosApp/iosApp/iOSApp.swift | 7 +++ 3 files changed, 62 insertions(+), 1 deletion(-) diff --git a/iosApp/iosApp/PushNotifications/PushNotificationManager.swift b/iosApp/iosApp/PushNotifications/PushNotificationManager.swift index f07bc78..1513af1 100644 --- a/iosApp/iosApp/PushNotifications/PushNotificationManager.swift +++ b/iosApp/iosApp/PushNotifications/PushNotificationManager.swift @@ -9,6 +9,14 @@ class PushNotificationManager: NSObject, ObservableObject { @Published var deviceToken: String? @Published var notificationPermissionGranted = false + private let registeredTokenKey = "com.casera.registeredDeviceToken" + + /// The last token that was successfully registered with the backend + private var lastRegisteredToken: String? { + get { UserDefaults.standard.string(forKey: registeredTokenKey) } + set { UserDefaults.standard.set(newValue, forKey: registeredTokenKey) } + } + override init() { super.init() } @@ -58,12 +66,48 @@ class PushNotificationManager: NSObject, ObservableObject { // MARK: - Backend Registration - private func registerDeviceWithBackend(token: String) async { + /// Call this after login to register any pending device token + func registerDeviceAfterLogin() { + guard let token = deviceToken else { + print("⚠️ No device token available for registration") + return + } + + Task { + await registerDeviceWithBackend(token: token, force: false) + } + } + + /// Call this when app returns from background to check and register if needed + func checkAndRegisterDeviceIfNeeded() { + guard let token = deviceToken else { + print("⚠️ No device token available for registration check") + return + } + + // Skip if token hasn't changed + if token == lastRegisteredToken { + print("📱 Device token unchanged, skipping registration") + return + } + + Task { + await registerDeviceWithBackend(token: token, force: false) + } + } + + private func registerDeviceWithBackend(token: String, force: Bool = false) async { guard TokenStorage.shared.getToken() != nil else { print("⚠️ No auth token available, will register device after login") return } + // Skip if token hasn't changed (unless forced) + if !force && token == lastRegisteredToken { + print("📱 Device token unchanged, skipping registration") + return + } + // Get unique device identifier let deviceId = await MainActor.run { UIDevice.current.identifierForVendor?.uuidString ?? UUID().uuidString @@ -84,6 +128,10 @@ class PushNotificationManager: NSObject, ObservableObject { if let success = result as? ApiResultSuccess { print("✅ Device registered successfully: \(success.data)") + // Cache the token on successful registration + await MainActor.run { + self.lastRegisteredToken = token + } } else if let error = result as? ApiResultError { print("❌ Failed to register device: \(error.message)") } else { diff --git a/iosApp/iosApp/RootView.swift b/iosApp/iosApp/RootView.swift index a0a67ba..c758acc 100644 --- a/iosApp/iosApp/RootView.swift +++ b/iosApp/iosApp/RootView.swift @@ -40,6 +40,9 @@ class AuthenticationManager: ObservableObject { if let success = result as? ApiResultSuccess { self.isVerified = success.data?.verified ?? false + // Register device for push notifications for authenticated users + PushNotificationManager.shared.registerDeviceAfterLogin() + // Verify subscription entitlements with backend for verified users if self.isVerified { await StoreKitManager.shared.verifyEntitlementsOnLaunch() @@ -65,6 +68,9 @@ class AuthenticationManager: ObservableObject { func login(verified: Bool) { isAuthenticated = true isVerified = verified + + // Register device for push notifications now that user is authenticated + PushNotificationManager.shared.registerDeviceAfterLogin() } func markVerified() { diff --git a/iosApp/iosApp/iOSApp.swift b/iosApp/iosApp/iOSApp.swift index 220dc9f..08b42aa 100644 --- a/iosApp/iosApp/iOSApp.swift +++ b/iosApp/iosApp/iOSApp.swift @@ -5,6 +5,7 @@ import ComposeApp struct iOSApp: App { @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate @StateObject private var themeManager = ThemeManager.shared + @Environment(\.scenePhase) private var scenePhase @State private var deepLinkResetToken: String? init() { @@ -35,6 +36,12 @@ struct iOSApp: App { .onOpenURL { url in handleDeepLink(url: url) } + .onChange(of: scenePhase) { newPhase in + if newPhase == .active { + // Check and register device token when app becomes active + PushNotificationManager.shared.checkAndRegisterDeviceIfNeeded() + } + } } }