initial version
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user