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>
268 lines
9.0 KiB
Swift
268 lines
9.0 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import UserNotifications
|
|
import ComposeApp
|
|
|
|
class PushNotificationManager: NSObject, ObservableObject {
|
|
@MainActor static let shared = PushNotificationManager()
|
|
|
|
@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()
|
|
}
|
|
|
|
// MARK: - Permission Request
|
|
|
|
func requestNotificationPermission() async -> Bool {
|
|
let center = UNUserNotificationCenter.current()
|
|
|
|
do {
|
|
let granted = try await center.requestAuthorization(options: [.alert, .sound, .badge])
|
|
notificationPermissionGranted = granted
|
|
|
|
if granted {
|
|
print("✅ Notification permission granted")
|
|
// Register for remote notifications on main thread
|
|
await MainActor.run {
|
|
UIApplication.shared.registerForRemoteNotifications()
|
|
}
|
|
} else {
|
|
print("❌ Notification permission denied")
|
|
}
|
|
|
|
return granted
|
|
} catch {
|
|
print("❌ Error requesting notification permission: \(error)")
|
|
return false
|
|
}
|
|
}
|
|
|
|
// MARK: - Token Management
|
|
|
|
func didRegisterForRemoteNotifications(withDeviceToken deviceToken: Data) {
|
|
let tokenString = deviceToken.map { String(format: "%02.2hhx", $0) }.joined()
|
|
self.deviceToken = tokenString
|
|
print("📱 APNs device token: \(tokenString)")
|
|
|
|
// Register with backend
|
|
Task {
|
|
await registerDeviceWithBackend(token: tokenString)
|
|
}
|
|
}
|
|
|
|
func didFailToRegisterForRemoteNotifications(withError error: Error) {
|
|
print("❌ Failed to register for remote notifications: \(error)")
|
|
}
|
|
|
|
// MARK: - Backend Registration
|
|
|
|
/// 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
|
|
}
|
|
let deviceName = await MainActor.run {
|
|
UIDevice.current.name
|
|
}
|
|
|
|
let request = DeviceRegistrationRequest(
|
|
deviceId: deviceId,
|
|
registrationId: token,
|
|
platform: "ios",
|
|
name: deviceName
|
|
)
|
|
|
|
do {
|
|
let result = try await APILayer.shared.registerDevice(request: request)
|
|
|
|
if let success = result as? ApiResultSuccess<DeviceRegistrationResponse> {
|
|
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 {
|
|
print("⚠️ Unexpected result type from device registration")
|
|
}
|
|
} catch {
|
|
print("❌ Error registering device: \(error.localizedDescription)")
|
|
}
|
|
}
|
|
|
|
// MARK: - Handle Notifications
|
|
|
|
func handleNotification(userInfo: [AnyHashable: Any]) {
|
|
print("📬 Received notification: \(userInfo)")
|
|
|
|
// Extract notification data
|
|
if let notificationId = userInfo["notification_id"] as? String {
|
|
print("Notification ID: \(notificationId)")
|
|
|
|
// Mark as read when user taps notification
|
|
Task {
|
|
await markNotificationAsRead(notificationId: notificationId)
|
|
}
|
|
}
|
|
|
|
if let type = userInfo["type"] as? String {
|
|
print("Notification type: \(type)")
|
|
handleNotificationType(type: type, userInfo: userInfo)
|
|
}
|
|
}
|
|
|
|
private func handleNotificationType(type: String, userInfo: [AnyHashable: Any]) {
|
|
switch type {
|
|
case "task_due_soon", "task_overdue", "task_completed", "task_assigned":
|
|
if let taskId = userInfo["task_id"] as? String {
|
|
print("Task notification for task ID: \(taskId)")
|
|
// TODO: Navigate to task detail
|
|
}
|
|
|
|
case "residence_shared":
|
|
if let residenceId = userInfo["residence_id"] as? String {
|
|
print("Residence shared notification for residence ID: \(residenceId)")
|
|
// TODO: Navigate to residence detail
|
|
}
|
|
|
|
case "warranty_expiring":
|
|
if let documentId = userInfo["document_id"] as? String {
|
|
print("Warranty expiring notification for document ID: \(documentId)")
|
|
// TODO: Navigate to document detail
|
|
}
|
|
|
|
default:
|
|
print("Unknown notification type: \(type)")
|
|
}
|
|
}
|
|
|
|
private func markNotificationAsRead(notificationId: String) async {
|
|
guard TokenStorage.shared.getToken() != nil,
|
|
let notificationIdInt = Int32(notificationId) else {
|
|
return
|
|
}
|
|
|
|
do {
|
|
let result = try await APILayer.shared.markNotificationAsRead(notificationId: notificationIdInt)
|
|
|
|
if result is ApiResultSuccess<ComposeApp.Notification> {
|
|
print("✅ Notification marked as read")
|
|
} else if let error = result as? ApiResultError {
|
|
print("❌ Failed to mark notification as read: \(error.message)")
|
|
}
|
|
} catch {
|
|
print("❌ Error marking notification as read: \(error.localizedDescription)")
|
|
}
|
|
}
|
|
|
|
// MARK: - Notification Preferences
|
|
|
|
func updateNotificationPreferences(_ preferences: UpdateNotificationPreferencesRequest) async -> Bool {
|
|
guard TokenStorage.shared.getToken() != nil else {
|
|
print("⚠️ No auth token available")
|
|
return false
|
|
}
|
|
|
|
do {
|
|
let result = try await APILayer.shared.updateNotificationPreferences(request: preferences)
|
|
|
|
if result is ApiResultSuccess<NotificationPreference> {
|
|
print("✅ Notification preferences updated")
|
|
return true
|
|
} else if let error = result as? ApiResultError {
|
|
print("❌ Failed to update preferences: \(error.message)")
|
|
return false
|
|
}
|
|
return false
|
|
} catch {
|
|
print("❌ Error updating notification preferences: \(error.localizedDescription)")
|
|
return false
|
|
}
|
|
}
|
|
|
|
func getNotificationPreferences() async -> NotificationPreference? {
|
|
guard TokenStorage.shared.getToken() != nil else {
|
|
print("⚠️ No auth token available")
|
|
return nil
|
|
}
|
|
|
|
do {
|
|
let result = try await APILayer.shared.getNotificationPreferences()
|
|
|
|
if let success = result as? ApiResultSuccess<NotificationPreference> {
|
|
return success.data
|
|
} else if let error = result as? ApiResultError {
|
|
print("❌ Failed to get preferences: \(error.message)")
|
|
return nil
|
|
}
|
|
return nil
|
|
} catch {
|
|
print("❌ Error getting notification preferences: \(error.localizedDescription)")
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// MARK: - Badge Management
|
|
|
|
func clearBadge() {
|
|
UNUserNotificationCenter.current().setBadgeCount(0)
|
|
}
|
|
|
|
func setBadge(count: Int) {
|
|
UNUserNotificationCenter.current().setBadgeCount(count)
|
|
}
|
|
}
|
|
|