Add smart device token caching for push notification registration

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 <noreply@anthropic.com>
This commit is contained in:
Trey t
2025-12-04 20:18:08 -06:00
parent 22bf109cf7
commit 70d46da14a
3 changed files with 62 additions and 1 deletions

View File

@@ -9,6 +9,14 @@ class PushNotificationManager: NSObject, ObservableObject {
@Published var deviceToken: String? @Published var deviceToken: String?
@Published var notificationPermissionGranted = false @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() { override init() {
super.init() super.init()
} }
@@ -58,12 +66,48 @@ class PushNotificationManager: NSObject, ObservableObject {
// MARK: - Backend Registration // 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 { guard TokenStorage.shared.getToken() != nil else {
print("⚠️ No auth token available, will register device after login") print("⚠️ No auth token available, will register device after login")
return return
} }
// Skip if token hasn't changed (unless forced)
if !force && token == lastRegisteredToken {
print("📱 Device token unchanged, skipping registration")
return
}
// Get unique device identifier // Get unique device identifier
let deviceId = await MainActor.run { let deviceId = await MainActor.run {
UIDevice.current.identifierForVendor?.uuidString ?? UUID().uuidString UIDevice.current.identifierForVendor?.uuidString ?? UUID().uuidString
@@ -84,6 +128,10 @@ class PushNotificationManager: NSObject, ObservableObject {
if let success = result as? ApiResultSuccess<DeviceRegistrationResponse> { if let success = result as? ApiResultSuccess<DeviceRegistrationResponse> {
print("✅ Device registered successfully: \(success.data)") 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 { } else if let error = result as? ApiResultError {
print("❌ Failed to register device: \(error.message)") print("❌ Failed to register device: \(error.message)")
} else { } else {

View File

@@ -40,6 +40,9 @@ class AuthenticationManager: ObservableObject {
if let success = result as? ApiResultSuccess<User> { if let success = result as? ApiResultSuccess<User> {
self.isVerified = success.data?.verified ?? false 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 // Verify subscription entitlements with backend for verified users
if self.isVerified { if self.isVerified {
await StoreKitManager.shared.verifyEntitlementsOnLaunch() await StoreKitManager.shared.verifyEntitlementsOnLaunch()
@@ -65,6 +68,9 @@ class AuthenticationManager: ObservableObject {
func login(verified: Bool) { func login(verified: Bool) {
isAuthenticated = true isAuthenticated = true
isVerified = verified isVerified = verified
// Register device for push notifications now that user is authenticated
PushNotificationManager.shared.registerDeviceAfterLogin()
} }
func markVerified() { func markVerified() {

View File

@@ -5,6 +5,7 @@ import ComposeApp
struct iOSApp: App { struct iOSApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
@StateObject private var themeManager = ThemeManager.shared @StateObject private var themeManager = ThemeManager.shared
@Environment(\.scenePhase) private var scenePhase
@State private var deepLinkResetToken: String? @State private var deepLinkResetToken: String?
init() { init() {
@@ -35,6 +36,12 @@ struct iOSApp: App {
.onOpenURL { url in .onOpenURL { url in
handleDeepLink(url: url) handleDeepLink(url: url)
} }
.onChange(of: scenePhase) { newPhase in
if newPhase == .active {
// Check and register device token when app becomes active
PushNotificationManager.shared.checkAndRegisterDeviceIfNeeded()
}
}
} }
} }