- Eliminate NumberFormatters shared singleton data race; use local formatters - Add reduceMotion checks to empty-state animations in 3 list views - Wrap 68+ print() statements in #if DEBUG across push notification code - Remove redundant .receive(on: DispatchQueue.main) in SubscriptionCache - Remove redundant initializeLookups() call from iOSApp.init() - Clean up StoreKitManager Task capture in listenForTransactions() - Add memory warning observer to AuthenticatedImage cache - Cache parseContent result in UpgradePromptView init - Add DiskSpace and FileTimestamp API declarations to Privacy Manifest - Add FIXME for analytics debug/production API key separation - Use static formatter in PropertyHeaderCard Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
735 lines
26 KiB
Swift
735 lines
26 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import UserNotifications
|
|
import ComposeApp
|
|
|
|
@MainActor
|
|
class PushNotificationManager: NSObject, ObservableObject {
|
|
static let shared = PushNotificationManager()
|
|
|
|
@Published var deviceToken: String?
|
|
@Published var notificationPermissionGranted = false
|
|
|
|
/// Pending task ID to navigate to (set when notification is tapped before app is ready)
|
|
@Published var pendingNavigationTaskId: Int?
|
|
@Published var pendingNavigationResidenceId: Int?
|
|
@Published var pendingNavigationDocumentId: Int?
|
|
|
|
private let registeredTokenKey = "com.casera.registeredDeviceToken"
|
|
private let registeredUserIdKey = "com.casera.registeredUserId"
|
|
|
|
/// 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) }
|
|
}
|
|
|
|
/// The user ID associated with the last successful registration
|
|
private var lastRegisteredUserId: String? {
|
|
get { UserDefaults.standard.string(forKey: registeredUserIdKey) }
|
|
set { UserDefaults.standard.set(newValue, forKey: registeredUserIdKey) }
|
|
}
|
|
|
|
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 {
|
|
#if DEBUG
|
|
print("✅ Notification permission granted")
|
|
#endif
|
|
// Register for remote notifications on main thread
|
|
await MainActor.run {
|
|
UIApplication.shared.registerForRemoteNotifications()
|
|
}
|
|
} else {
|
|
#if DEBUG
|
|
print("❌ Notification permission denied")
|
|
#endif
|
|
}
|
|
|
|
return granted
|
|
} catch {
|
|
#if DEBUG
|
|
print("❌ Error requesting notification permission: \(error)")
|
|
#endif
|
|
return false
|
|
}
|
|
}
|
|
|
|
// MARK: - Token Management
|
|
|
|
func didRegisterForRemoteNotifications(withDeviceToken deviceToken: Data) {
|
|
let tokenString = deviceToken.map { String(format: "%02.2hhx", $0) }.joined()
|
|
self.deviceToken = tokenString
|
|
let redactedToken = "\(tokenString.prefix(8))...\(tokenString.suffix(8))"
|
|
#if DEBUG
|
|
print("📱 APNs device token: \(redactedToken)")
|
|
#endif
|
|
|
|
// Register with backend
|
|
Task { [weak self] in
|
|
guard let self else { return }
|
|
await self.registerDeviceWithBackend(token: tokenString)
|
|
}
|
|
}
|
|
|
|
func didFailToRegisterForRemoteNotifications(withError error: Error) {
|
|
#if DEBUG
|
|
print("❌ Failed to register for remote notifications: \(error)")
|
|
#endif
|
|
}
|
|
|
|
// MARK: - Backend Registration
|
|
|
|
/// Call this after login to register any pending device token
|
|
func registerDeviceAfterLogin() {
|
|
guard let token = deviceToken else {
|
|
#if DEBUG
|
|
print("⚠️ No device token available for registration")
|
|
#endif
|
|
return
|
|
}
|
|
|
|
Task { [weak self] in
|
|
guard let self else { return }
|
|
await self.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 {
|
|
#if DEBUG
|
|
print("⚠️ No device token available for registration check")
|
|
#endif
|
|
return
|
|
}
|
|
|
|
Task { [weak self] in
|
|
guard let self else { return }
|
|
await self.registerDeviceWithBackend(token: token, force: false)
|
|
}
|
|
}
|
|
|
|
private func registerDeviceWithBackend(token: String, force: Bool = false) async {
|
|
guard TokenStorage.shared.getToken() != nil else {
|
|
#if DEBUG
|
|
print("⚠️ No auth token available, will register device after login")
|
|
#endif
|
|
return
|
|
}
|
|
|
|
let currentUserId = await MainActor.run {
|
|
DataManagerObservable.shared.currentUser.map { String($0.id) }
|
|
}
|
|
|
|
// Skip only if both token and user identity match.
|
|
if !force, token == lastRegisteredToken {
|
|
if let currentUserId, currentUserId == lastRegisteredUserId {
|
|
#if DEBUG
|
|
print("📱 Device token already registered for current user, skipping")
|
|
#endif
|
|
return
|
|
}
|
|
|
|
if currentUserId == nil, lastRegisteredUserId == nil {
|
|
#if DEBUG
|
|
print("📱 Device token already registered, skipping")
|
|
#endif
|
|
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> {
|
|
#if DEBUG
|
|
if success.data != nil {
|
|
print("✅ Device registered successfully")
|
|
} else {
|
|
print("✅ Device registration acknowledged")
|
|
}
|
|
#endif
|
|
// Cache the token on successful registration
|
|
await MainActor.run {
|
|
self.lastRegisteredToken = token
|
|
self.lastRegisteredUserId = currentUserId
|
|
}
|
|
} else if let error = ApiResultBridge.error(from: result) {
|
|
#if DEBUG
|
|
print("❌ Failed to register device: \(error.message)")
|
|
#endif
|
|
} else {
|
|
#if DEBUG
|
|
print("⚠️ Unexpected result type from device registration")
|
|
#endif
|
|
}
|
|
} catch {
|
|
#if DEBUG
|
|
print("❌ Error registering device: \(error.localizedDescription)")
|
|
#endif
|
|
}
|
|
}
|
|
|
|
// MARK: - Handle Notifications
|
|
|
|
func handleNotification(userInfo: [AnyHashable: Any]) {
|
|
#if DEBUG
|
|
print("📬 Received notification: \(redactedPayloadSummary(userInfo: userInfo))")
|
|
#endif
|
|
|
|
// Extract notification data
|
|
if let notificationId = stringValue(for: "notification_id", in: userInfo) {
|
|
#if DEBUG
|
|
print("Notification ID: \(notificationId)")
|
|
#endif
|
|
|
|
// Mark as read when user taps notification
|
|
Task { [weak self] in
|
|
guard let self else { return }
|
|
await self.markNotificationAsRead(notificationId: notificationId)
|
|
}
|
|
}
|
|
|
|
if let type = userInfo["type"] as? String {
|
|
#if DEBUG
|
|
print("Notification type: \(type)")
|
|
#endif
|
|
handleNotificationType(type: type, userInfo: userInfo)
|
|
}
|
|
}
|
|
|
|
/// Called when user taps the notification body (not an action button)
|
|
func handleNotificationTap(userInfo: [AnyHashable: Any]) {
|
|
#if DEBUG
|
|
print("📬 Handling notification tap")
|
|
#endif
|
|
|
|
// Mark as read
|
|
if let notificationId = stringValue(for: "notification_id", in: userInfo) {
|
|
Task { [weak self] in
|
|
guard let self else { return }
|
|
await self.markNotificationAsRead(notificationId: notificationId)
|
|
}
|
|
}
|
|
|
|
// Check subscription status to determine navigation
|
|
// Allow navigation if: Pro user, OR limitations are disabled, OR subscription hasn't loaded yet
|
|
let subscription = SubscriptionCacheWrapper.shared.currentSubscription
|
|
let isPremium = SubscriptionCacheWrapper.shared.currentTier == "pro"
|
|
let limitationsEnabled = subscription?.limitationsEnabled ?? false // Default to false (allow) if not loaded
|
|
let canNavigateToTask = isPremium || !limitationsEnabled
|
|
|
|
#if DEBUG
|
|
print("📬 Push nav check: isPremium=\(isPremium), limitationsEnabled=\(limitationsEnabled), canNavigate=\(canNavigateToTask), subscription=\(subscription != nil ? "loaded" : "nil")")
|
|
#endif
|
|
|
|
if canNavigateToTask {
|
|
// Navigate to task detail
|
|
if let taskId = intValue(for: "task_id", in: userInfo) {
|
|
navigateToTask(taskId: taskId)
|
|
} else if let type = userInfo["type"] as? String {
|
|
// Fall back to type-specific routing for non-task notifications.
|
|
handleNotificationType(type: type, userInfo: userInfo)
|
|
} else {
|
|
navigateToHome()
|
|
}
|
|
} else {
|
|
// Free user with limitations enabled - navigate to home
|
|
navigateToHome()
|
|
}
|
|
}
|
|
|
|
/// Called when user selects an action button on the notification
|
|
func handleNotificationAction(actionIdentifier: String, userInfo: [AnyHashable: Any]) {
|
|
#if DEBUG
|
|
print("🔘 Handling notification action: \(actionIdentifier)")
|
|
#endif
|
|
|
|
// Mark as read
|
|
if let notificationId = stringValue(for: "notification_id", in: userInfo) {
|
|
Task { [weak self] in
|
|
guard let self else { return }
|
|
await self.markNotificationAsRead(notificationId: notificationId)
|
|
}
|
|
}
|
|
|
|
// Check subscription status
|
|
// Allow actions if: Pro user, OR limitations are disabled, OR subscription hasn't loaded yet
|
|
let subscription = SubscriptionCacheWrapper.shared.currentSubscription
|
|
let isPremium = SubscriptionCacheWrapper.shared.currentTier == "pro"
|
|
let limitationsEnabled = subscription?.limitationsEnabled ?? false // Default to false (allow) if not loaded
|
|
let canPerformAction = isPremium || !limitationsEnabled
|
|
|
|
guard canPerformAction else {
|
|
// Free user with limitations enabled shouldn't see actions, but if they somehow do, go to home
|
|
navigateToHome()
|
|
return
|
|
}
|
|
|
|
// Extract task ID
|
|
guard let taskId = intValue(for: "task_id", in: userInfo) else {
|
|
#if DEBUG
|
|
print("❌ No task_id found in notification")
|
|
#endif
|
|
return
|
|
}
|
|
|
|
// Handle action
|
|
switch actionIdentifier {
|
|
case NotificationActionID.viewTask.rawValue:
|
|
navigateToTask(taskId: taskId)
|
|
|
|
case NotificationActionID.completeTask.rawValue:
|
|
performCompleteTask(taskId: taskId)
|
|
|
|
case NotificationActionID.markInProgress.rawValue:
|
|
performMarkInProgress(taskId: taskId)
|
|
|
|
case NotificationActionID.cancelTask.rawValue:
|
|
performCancelTask(taskId: taskId)
|
|
|
|
case NotificationActionID.uncancelTask.rawValue:
|
|
performUncancelTask(taskId: taskId)
|
|
|
|
case NotificationActionID.editTask.rawValue:
|
|
navigateToEditTask(taskId: taskId)
|
|
|
|
default:
|
|
#if DEBUG
|
|
print("⚠️ Unknown action: \(actionIdentifier)")
|
|
#endif
|
|
navigateToTask(taskId: taskId)
|
|
}
|
|
}
|
|
|
|
private func handleNotificationType(type: String, userInfo: [AnyHashable: Any]) {
|
|
switch type {
|
|
case "task_due_soon", "task_overdue", "task_completed", "task_assigned":
|
|
if let taskId = intValue(for: "task_id", in: userInfo) {
|
|
#if DEBUG
|
|
print("Task notification for task ID: \(taskId)")
|
|
#endif
|
|
navigateToTask(taskId: taskId)
|
|
}
|
|
|
|
case "residence_shared":
|
|
if let residenceId = intValue(for: "residence_id", in: userInfo) {
|
|
#if DEBUG
|
|
print("Residence shared notification for residence ID: \(residenceId)")
|
|
#endif
|
|
navigateToResidence(residenceId: residenceId)
|
|
} else {
|
|
#if DEBUG
|
|
print("Residence shared notification without residence ID")
|
|
#endif
|
|
navigateToResidencesTab()
|
|
}
|
|
|
|
case "warranty_expiring":
|
|
if let documentId = intValue(for: "document_id", in: userInfo) {
|
|
#if DEBUG
|
|
print("Warranty expiring notification for document ID: \(documentId)")
|
|
#endif
|
|
navigateToDocument(documentId: documentId)
|
|
} else {
|
|
#if DEBUG
|
|
print("Warranty expiring notification without document ID")
|
|
#endif
|
|
navigateToDocumentsTab()
|
|
}
|
|
|
|
default:
|
|
#if DEBUG
|
|
print("Unknown notification type: \(type)")
|
|
#endif
|
|
}
|
|
}
|
|
|
|
// MARK: - Task Actions
|
|
|
|
private func performCompleteTask(taskId: Int) {
|
|
#if DEBUG
|
|
print("✅ Completing task \(taskId) from notification action")
|
|
#endif
|
|
Task { [weak self] in
|
|
guard let _ = self else { return }
|
|
do {
|
|
// Quick complete without photos/notes
|
|
let request = TaskCompletionCreateRequest(
|
|
taskId: Int32(taskId),
|
|
completedAt: nil,
|
|
notes: nil,
|
|
actualCost: nil,
|
|
rating: nil,
|
|
imageUrls: nil
|
|
)
|
|
let result = try await APILayer.shared.createTaskCompletion(request: request)
|
|
|
|
if result is ApiResultSuccess<TaskCompletionResponse> {
|
|
#if DEBUG
|
|
print("✅ Task \(taskId) completed successfully")
|
|
#endif
|
|
// Post notification for UI refresh
|
|
await MainActor.run {
|
|
NotificationCenter.default.post(name: .taskActionCompleted, object: nil)
|
|
}
|
|
} else if let error = ApiResultBridge.error(from: result) {
|
|
#if DEBUG
|
|
print("❌ Failed to complete task: \(error.message)")
|
|
#endif
|
|
} else {
|
|
#if DEBUG
|
|
print("⚠️ Unexpected result while completing task \(taskId)")
|
|
#endif
|
|
}
|
|
} catch {
|
|
#if DEBUG
|
|
print("❌ Error completing task: \(error.localizedDescription)")
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
|
|
private func performMarkInProgress(taskId: Int) {
|
|
#if DEBUG
|
|
print("🔄 Marking task \(taskId) as in progress from notification action")
|
|
#endif
|
|
Task { [weak self] in
|
|
guard let _ = self else { return }
|
|
do {
|
|
let result = try await APILayer.shared.markInProgress(taskId: Int32(taskId))
|
|
|
|
if result is ApiResultSuccess<TaskResponse> {
|
|
#if DEBUG
|
|
print("✅ Task \(taskId) marked as in progress")
|
|
#endif
|
|
await MainActor.run {
|
|
NotificationCenter.default.post(name: .taskActionCompleted, object: nil)
|
|
}
|
|
} else if let error = ApiResultBridge.error(from: result) {
|
|
#if DEBUG
|
|
print("❌ Failed to mark task in progress: \(error.message)")
|
|
#endif
|
|
} else {
|
|
#if DEBUG
|
|
print("⚠️ Unexpected result while marking task \(taskId) in progress")
|
|
#endif
|
|
}
|
|
} catch {
|
|
#if DEBUG
|
|
print("❌ Error marking task in progress: \(error.localizedDescription)")
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
|
|
private func performCancelTask(taskId: Int) {
|
|
#if DEBUG
|
|
print("🚫 Cancelling task \(taskId) from notification action")
|
|
#endif
|
|
Task { [weak self] in
|
|
guard let _ = self else { return }
|
|
do {
|
|
let result = try await APILayer.shared.cancelTask(taskId: Int32(taskId))
|
|
|
|
if result is ApiResultSuccess<TaskResponse> {
|
|
#if DEBUG
|
|
print("✅ Task \(taskId) cancelled")
|
|
#endif
|
|
await MainActor.run {
|
|
NotificationCenter.default.post(name: .taskActionCompleted, object: nil)
|
|
}
|
|
} else if let error = ApiResultBridge.error(from: result) {
|
|
#if DEBUG
|
|
print("❌ Failed to cancel task: \(error.message)")
|
|
#endif
|
|
} else {
|
|
#if DEBUG
|
|
print("⚠️ Unexpected result while cancelling task \(taskId)")
|
|
#endif
|
|
}
|
|
} catch {
|
|
#if DEBUG
|
|
print("❌ Error cancelling task: \(error.localizedDescription)")
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
|
|
private func performUncancelTask(taskId: Int) {
|
|
#if DEBUG
|
|
print("↩️ Uncancelling task \(taskId) from notification action")
|
|
#endif
|
|
Task { [weak self] in
|
|
guard let _ = self else { return }
|
|
do {
|
|
let result = try await APILayer.shared.uncancelTask(taskId: Int32(taskId))
|
|
|
|
if result is ApiResultSuccess<TaskResponse> {
|
|
#if DEBUG
|
|
print("✅ Task \(taskId) uncancelled")
|
|
#endif
|
|
await MainActor.run {
|
|
NotificationCenter.default.post(name: .taskActionCompleted, object: nil)
|
|
}
|
|
} else if let error = ApiResultBridge.error(from: result) {
|
|
#if DEBUG
|
|
print("❌ Failed to uncancel task: \(error.message)")
|
|
#endif
|
|
} else {
|
|
#if DEBUG
|
|
print("⚠️ Unexpected result while uncancelling task \(taskId)")
|
|
#endif
|
|
}
|
|
} catch {
|
|
#if DEBUG
|
|
print("❌ Error uncancelling task: \(error.localizedDescription)")
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Navigation
|
|
|
|
private func navigateToTask(taskId: Int) {
|
|
#if DEBUG
|
|
print("📱 Navigating to task \(taskId)")
|
|
#endif
|
|
// Store pending navigation in case MainTabView isn't ready yet
|
|
pendingNavigationTaskId = taskId
|
|
NotificationCenter.default.post(
|
|
name: .navigateToTask,
|
|
object: nil,
|
|
userInfo: ["taskId": taskId]
|
|
)
|
|
}
|
|
|
|
/// Clear pending navigation after it has been handled
|
|
func clearPendingNavigation() {
|
|
pendingNavigationTaskId = nil
|
|
pendingNavigationResidenceId = nil
|
|
pendingNavigationDocumentId = nil
|
|
}
|
|
|
|
/// Clear device registration cache (call on logout/account switch).
|
|
func clearRegistrationCache() {
|
|
lastRegisteredToken = nil
|
|
lastRegisteredUserId = nil
|
|
}
|
|
|
|
private func navigateToEditTask(taskId: Int) {
|
|
#if DEBUG
|
|
print("✏️ Navigating to edit task \(taskId)")
|
|
#endif
|
|
NotificationCenter.default.post(
|
|
name: .navigateToEditTask,
|
|
object: nil,
|
|
userInfo: ["taskId": taskId]
|
|
)
|
|
}
|
|
|
|
private func navigateToResidence(residenceId: Int) {
|
|
#if DEBUG
|
|
print("🏠 Navigating to residence \(residenceId)")
|
|
#endif
|
|
pendingNavigationResidenceId = residenceId
|
|
NotificationCenter.default.post(
|
|
name: .navigateToResidence,
|
|
object: nil,
|
|
userInfo: ["residenceId": residenceId]
|
|
)
|
|
}
|
|
|
|
private func navigateToResidencesTab() {
|
|
NotificationCenter.default.post(name: .navigateToResidence, object: nil)
|
|
}
|
|
|
|
private func navigateToDocument(documentId: Int) {
|
|
#if DEBUG
|
|
print("📄 Navigating to document \(documentId)")
|
|
#endif
|
|
pendingNavigationDocumentId = documentId
|
|
NotificationCenter.default.post(
|
|
name: .navigateToDocument,
|
|
object: nil,
|
|
userInfo: ["documentId": documentId]
|
|
)
|
|
}
|
|
|
|
private func navigateToDocumentsTab() {
|
|
NotificationCenter.default.post(name: .navigateToDocument, object: nil)
|
|
}
|
|
|
|
private func navigateToHome() {
|
|
#if DEBUG
|
|
print("🏠 Navigating to home")
|
|
#endif
|
|
pendingNavigationTaskId = nil
|
|
pendingNavigationResidenceId = nil
|
|
pendingNavigationDocumentId = nil
|
|
NotificationCenter.default.post(name: .navigateToHome, object: nil)
|
|
}
|
|
|
|
private func stringValue(for key: String, in userInfo: [AnyHashable: Any]) -> String? {
|
|
if let value = userInfo[key] as? String {
|
|
return value.trimmingCharacters(in: CharacterSet(charactersIn: "\""))
|
|
}
|
|
if let value = userInfo[key] as? NSNumber {
|
|
return value.stringValue
|
|
}
|
|
if let value = userInfo[key] as? Int {
|
|
return String(value)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
private func intValue(for key: String, in userInfo: [AnyHashable: Any]) -> Int? {
|
|
guard let raw = stringValue(for: key, in: userInfo) else {
|
|
return nil
|
|
}
|
|
return Int(raw)
|
|
}
|
|
|
|
private func redactedPayloadSummary(userInfo: [AnyHashable: Any]) -> String {
|
|
let type = stringValue(for: "type", in: userInfo) ?? "unknown"
|
|
let notificationId = stringValue(for: "notification_id", in: userInfo) ?? "missing"
|
|
let taskId = stringValue(for: "task_id", in: userInfo) ?? "none"
|
|
let residenceId = stringValue(for: "residence_id", in: userInfo) ?? "none"
|
|
let documentId = stringValue(for: "document_id", in: userInfo) ?? "none"
|
|
return "type=\(type), notification_id=\(notificationId), task_id=\(taskId), residence_id=\(residenceId), document_id=\(documentId)"
|
|
}
|
|
|
|
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<MessageResponse> {
|
|
#if DEBUG
|
|
print("✅ Notification marked as read")
|
|
#endif
|
|
} else if let error = ApiResultBridge.error(from: result) {
|
|
#if DEBUG
|
|
print("❌ Failed to mark notification as read: \(error.message)")
|
|
#endif
|
|
} else {
|
|
#if DEBUG
|
|
print("⚠️ Unexpected result while marking notification read")
|
|
#endif
|
|
}
|
|
} catch {
|
|
#if DEBUG
|
|
print("❌ Error marking notification as read: \(error.localizedDescription)")
|
|
#endif
|
|
}
|
|
}
|
|
|
|
// MARK: - Notification Preferences
|
|
|
|
func updateNotificationPreferences(_ preferences: UpdateNotificationPreferencesRequest) async -> Bool {
|
|
guard TokenStorage.shared.getToken() != nil else {
|
|
#if DEBUG
|
|
print("⚠️ No auth token available")
|
|
#endif
|
|
return false
|
|
}
|
|
|
|
do {
|
|
let result = try await APILayer.shared.updateNotificationPreferences(request: preferences)
|
|
|
|
if result is ApiResultSuccess<NotificationPreference> {
|
|
#if DEBUG
|
|
print("✅ Notification preferences updated")
|
|
#endif
|
|
return true
|
|
} else if let error = ApiResultBridge.error(from: result) {
|
|
#if DEBUG
|
|
print("❌ Failed to update preferences: \(error.message)")
|
|
#endif
|
|
return false
|
|
}
|
|
#if DEBUG
|
|
print("⚠️ Unexpected result while updating notification preferences")
|
|
#endif
|
|
return false
|
|
} catch {
|
|
#if DEBUG
|
|
print("❌ Error updating notification preferences: \(error.localizedDescription)")
|
|
#endif
|
|
return false
|
|
}
|
|
}
|
|
|
|
func getNotificationPreferences() async -> NotificationPreference? {
|
|
guard TokenStorage.shared.getToken() != nil else {
|
|
#if DEBUG
|
|
print("⚠️ No auth token available")
|
|
#endif
|
|
return nil
|
|
}
|
|
|
|
do {
|
|
let result = try await APILayer.shared.getNotificationPreferences()
|
|
|
|
if let success = result as? ApiResultSuccess<NotificationPreference> {
|
|
return success.data
|
|
} else if let error = ApiResultBridge.error(from: result) {
|
|
#if DEBUG
|
|
print("❌ Failed to get preferences: \(error.message)")
|
|
#endif
|
|
return nil
|
|
}
|
|
#if DEBUG
|
|
print("⚠️ Unexpected result while loading notification preferences")
|
|
#endif
|
|
return nil
|
|
} catch {
|
|
#if DEBUG
|
|
print("❌ Error getting notification preferences: \(error.localizedDescription)")
|
|
#endif
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// MARK: - Badge Management
|
|
|
|
func clearBadge() {
|
|
UNUserNotificationCenter.current().setBadgeCount(0)
|
|
}
|
|
|
|
func setBadge(count: Int) {
|
|
UNUserNotificationCenter.current().setBadgeCount(count)
|
|
}
|
|
}
|