From 1f58092dcb67cded75a850db364f3b46d86181f3 Mon Sep 17 00:00:00 2001 From: Christian Cleberg Date: Tue, 21 Apr 2026 21:53:16 -0500 Subject: feat(v2.2.0): add foreground monitoring, notifications, and smarter diffs - implement “Check All” sweep for tracked domains - add local notifications for changes and certificate expiration - introduce change severity filtering (low/medium/high) - enhance change summaries and diff readability - add certificate expiration tracking and alerts - improve watchlist indicators for changes - add sweep summary screen - optimize performance for larger watchlists MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DomainDig/LocalNotificationService.swift | 89 ++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 DomainDig/LocalNotificationService.swift (limited to 'DomainDig/LocalNotificationService.swift') diff --git a/DomainDig/LocalNotificationService.swift b/DomainDig/LocalNotificationService.swift new file mode 100644 index 0000000..450d844 --- /dev/null +++ b/DomainDig/LocalNotificationService.swift @@ -0,0 +1,89 @@ +import Foundation +import UserNotifications + +@MainActor +final class LocalNotificationService { + static let shared = LocalNotificationService() + + private init() {} + + func configureForegroundPresentation() { + UNUserNotificationCenter.current().delegate = NotificationCenterDelegate.shared + } + + func requestAuthorizationIfNeeded() async -> Bool { + let center = UNUserNotificationCenter.current() + let settings = await center.notificationSettings() + + switch settings.authorizationStatus { + case .authorized, .provisional, .ephemeral: + return true + case .notDetermined: + return (try? await center.requestAuthorization(options: [.alert, .badge, .sound])) ?? false + case .denied: + return false + @unknown default: + return false + } + } + + func notifyDomainEvent(domain: String, message: String, severity: ChangeSeverity) async { + await schedule( + identifier: "domain-change-\(domain)", + title: domain, + body: message, + interruptionLevel: severity == .high ? .timeSensitive : .active + ) + } + + func notifyCertificateWarning(domain: String, daysRemaining: Int) async { + await schedule( + identifier: "cert-warning-\(domain)", + title: domain, + body: "Certificate expires in \(daysRemaining) days", + interruptionLevel: .timeSensitive + ) + } + + func notifySweepComplete(summary: BatchSweepSummary) async { + let body = "\(summary.changedDomains) changed, \(summary.warningDomains) warnings, \(summary.unchangedDomains) unchanged" + await schedule( + identifier: "sweep-complete", + title: summary.source == .watchlistRefresh ? "Check All Complete" : "Batch Complete", + body: body, + interruptionLevel: .active + ) + } + + private func schedule( + identifier: String, + title: String, + body: String, + interruptionLevel: UNNotificationInterruptionLevel + ) async { + let content = UNMutableNotificationContent() + content.title = title + content.body = body + content.sound = .default + content.interruptionLevel = interruptionLevel + + let request = UNNotificationRequest( + identifier: identifier, + content: content, + trigger: UNTimeIntervalNotificationTrigger(timeInterval: 0.1, repeats: false) + ) + + try? await UNUserNotificationCenter.current().add(request) + } +} + +private final class NotificationCenterDelegate: NSObject, UNUserNotificationCenterDelegate { + static let shared = NotificationCenterDelegate() + + func userNotificationCenter( + _ center: UNUserNotificationCenter, + willPresent notification: UNNotification + ) async -> UNNotificationPresentationOptions { + [.banner, .list, .sound] + } +} -- cgit v1.2.3