import Foundation import UserNotifications import UIKit class NotificationService: ObservableObject { static let shared = NotificationService() @Published var isAuthorized = false @Published var pendingCount = 0 private let center = UNUserNotificationCenter.current() private init() { checkAuthorizationStatus() } // MARK: - Authorization func checkAuthorizationStatus() { center.getNotificationSettings { [weak self] settings in DispatchQueue.main.async { self?.isAuthorized = settings.authorizationStatus == .authorized } } } func requestAuthorization() async -> Bool { do { let granted = try await center.requestAuthorization(options: [.alert, .badge, .sound]) await MainActor.run { self.isAuthorized = granted } return granted } catch { print("Notification authorization error: \(error)") return false } } // MARK: - Schedule Notifications func scheduleReminder(for source: InvestmentSource) { guard let nextDate = source.nextReminderDate else { return } guard source.frequency != .never else { return } // Remove existing notification for this source cancelReminder(for: source) // Get notification time from settings let settings = AppSettings.getOrCreate(in: CoreDataStack.shared.viewContext) let notificationTime = settings.defaultNotificationTime ?? defaultNotificationTime() // Combine date and time let calendar = Calendar.current var components = calendar.dateComponents([.year, .month, .day], from: nextDate) let timeComponents = calendar.dateComponents([.hour, .minute], from: notificationTime) components.hour = timeComponents.hour components.minute = timeComponents.minute guard let triggerDate = calendar.date(from: components) else { return } // Create notification content let content = UNMutableNotificationContent() content.title = "Investment Update Reminder" content.body = "Time to update \(source.name). Tap to add a new snapshot." content.sound = .default content.badge = NSNumber(value: pendingCount + 1) content.userInfo = [ "sourceId": source.id.uuidString, "sourceName": source.name ] // Create trigger let triggerComponents = calendar.dateComponents( [.year, .month, .day, .hour, .minute], from: triggerDate ) let trigger = UNCalendarNotificationTrigger(dateMatching: triggerComponents, repeats: false) // Create request let request = UNNotificationRequest( identifier: notificationIdentifier(for: source), content: content, trigger: trigger ) // Schedule center.add(request) { error in if let error = error { print("Failed to schedule notification: \(error)") } else { print("Scheduled reminder for \(source.name) on \(triggerDate)") FirebaseService.shared.logNotificationScheduled(frequency: source.notificationFrequency) } } } func scheduleAllReminders(for sources: [InvestmentSource]) { for source in sources { scheduleReminder(for: source) } } // MARK: - Cancel Notifications func cancelReminder(for source: InvestmentSource) { center.removePendingNotificationRequests( withIdentifiers: [notificationIdentifier(for: source)] ) } func cancelAllReminders() { center.removeAllPendingNotificationRequests() } // MARK: - Badge Management func updateBadgeCount() { let repository = InvestmentSourceRepository() let needsUpdate = repository.fetchSourcesNeedingUpdate() pendingCount = needsUpdate.count DispatchQueue.main.async { UIApplication.shared.applicationIconBadgeNumber = self.pendingCount } } func clearBadge() { pendingCount = 0 DispatchQueue.main.async { UIApplication.shared.applicationIconBadgeNumber = 0 } } // MARK: - Pending Notifications func getPendingNotifications() async -> [UNNotificationRequest] { await center.pendingNotificationRequests() } // MARK: - Helpers private func notificationIdentifier(for source: InvestmentSource) -> String { "investment_reminder_\(source.id.uuidString)" } private func defaultNotificationTime() -> Date { var components = DateComponents() components.hour = 9 components.minute = 0 return Calendar.current.date(from: components) ?? Date() } } // MARK: - Deep Link Handler extension NotificationService { func handleNotificationResponse(_ response: UNNotificationResponse) { let userInfo = response.notification.request.content.userInfo guard let sourceIdString = userInfo["sourceId"] as? String, let sourceId = UUID(uuidString: sourceIdString) else { return } // Post notification for deep linking NotificationCenter.default.post( name: .openSourceDetail, object: nil, userInfo: ["sourceId": sourceId] ) } } // MARK: - Notification Names extension Notification.Name { static let openSourceDetail = Notification.Name("openSourceDetail") } // MARK: - Background Refresh extension NotificationService { func performBackgroundRefresh() { updateBadgeCount() // Reschedule any missed notifications let repository = InvestmentSourceRepository() let sources = repository.fetchActiveSources() scheduleAllReminders(for: sources) } }