InvestmentTrackerApp/PortfolioJournal/Services/NotificationService.swift

190 lines
5.7 KiB
Swift

import Foundation
import Combine
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
center.setBadgeCount(pendingCount) { _ in }
}
func clearBadge() {
pendingCount = 0
center.setBadgeCount(0) { _ in }
}
// 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)
}
}