krz/domain-dig
an ios app for DNS & SSL analysis
clone: git clone https://gitbay.org/krz/domain-dig.git
v4.5.0: DomainDig/LocalNotificationService.swift · raw
1import Foundation
2import UserNotifications
3
4@MainActor
5final class LocalNotificationService {
6 static let shared = LocalNotificationService()
7
8 private init() {}
9
10 func configureForegroundPresentation() {
11 UNUserNotificationCenter.current().delegate = NotificationCenterDelegate.shared
12 }
13
14 func requestAuthorizationIfNeeded() async -> Bool {
15 let center = UNUserNotificationCenter.current()
16 let settings = await center.notificationSettings()
17
18 switch settings.authorizationStatus {
19 case .authorized, .provisional, .ephemeral:
20 return true
21 case .notDetermined:
22 return (try? await center.requestAuthorization(options: [.alert, .badge, .sound])) ?? false
23 case .denied:
24 return false
25 @unknown default:
26 return false
27 }
28 }
29
30 func isAuthorizedForAlerts() async -> Bool {
31 let settings = await UNUserNotificationCenter.current().notificationSettings()
32 switch settings.authorizationStatus {
33 case .authorized, .provisional, .ephemeral:
34 return true
35 case .denied, .notDetermined:
36 return false
37 @unknown default:
38 return false
39 }
40 }
41
42 func notifyDomainEvent(domain: String, message: String, severity: ChangeSeverity) async {
43 await schedule(
44 identifier: "domain-change-\(domain)",
45 title: domain,
46 body: message,
47 interruptionLevel: severity == .high ? .timeSensitive : .active
48 )
49 }
50
51 func notifyCertificateWarning(domain: String, daysRemaining: Int) async {
52 await schedule(
53 identifier: "cert-warning-\(domain)",
54 title: domain,
55 body: "Certificate expires in \(daysRemaining) days",
56 interruptionLevel: .timeSensitive
57 )
58 }
59
60 func notifyMonitoringAlert(
61 domain: String,
62 message: String,
63 severity: MonitoringAlertSeverity
64 ) async {
65 let interruptionLevel: UNNotificationInterruptionLevel
66 switch severity {
67 case .critical:
68 interruptionLevel = .timeSensitive
69 case .warning, .info:
70 interruptionLevel = .active
71 }
72
73 await schedule(
74 identifier: "monitoring-\(domain)-\(UUID().uuidString)",
75 title: domain,
76 body: message,
77 interruptionLevel: interruptionLevel
78 )
79 }
80
81 func notifyMonitoringSummary(
82 domain: String,
83 alerts: [MonitoringPendingAlert]
84 ) async {
85 let summary = alerts
86 .sorted { $0.detectedAt < $1.detectedAt }
87 .prefix(2)
88 .map(\.message)
89 .joined(separator: " • ")
90 let body: String
91 if alerts.count <= 1 {
92 body = alerts.first?.message ?? "Monitoring change detected"
93 } else if summary.isEmpty {
94 body = "\(alerts.count) monitoring changes detected"
95 } else {
96 body = "\(alerts.count) monitoring changes: \(summary)"
97 }
98
99 let severity = alerts.map(\.severity).max() ?? .info
100 let interruptionLevel: UNNotificationInterruptionLevel = severity == .critical ? .timeSensitive : .active
101
102 await schedule(
103 identifier: "monitoring-summary-\(domain)-\(UUID().uuidString)",
104 title: domain,
105 body: body,
106 interruptionLevel: interruptionLevel
107 )
108 }
109
110 func notifySweepComplete(summary: BatchSweepSummary) async {
111 let body = "\(summary.changedDomains) changed, \(summary.warningDomains) warnings, \(summary.unchangedDomains) unchanged"
112 await schedule(
113 identifier: "sweep-complete",
114 title: summary.source == .watchlistRefresh ? "Check All Complete" : "Batch Complete",
115 body: body,
116 interruptionLevel: .active
117 )
118 }
119
120 func clearAllNotifications() async {
121 let center = UNUserNotificationCenter.current()
122 center.removeAllPendingNotificationRequests()
123 center.removeAllDeliveredNotifications()
124 }
125
126 private func schedule(
127 identifier: String,
128 title: String,
129 body: String,
130 interruptionLevel: UNNotificationInterruptionLevel
131 ) async {
132 let content = UNMutableNotificationContent()
133 content.title = title
134 content.body = body
135 content.sound = .default
136 content.interruptionLevel = interruptionLevel
137
138 let request = UNNotificationRequest(
139 identifier: identifier,
140 content: content,
141 trigger: UNTimeIntervalNotificationTrigger(timeInterval: 0.1, repeats: false)
142 )
143
144 try? await UNUserNotificationCenter.current().add(request)
145 }
146}
147
148private final class NotificationCenterDelegate: NSObject, UNUserNotificationCenterDelegate {
149 static let shared = NotificationCenterDelegate()
150
151 func userNotificationCenter(
152 _ center: UNUserNotificationCenter,
153 willPresent notification: UNNotification
154 ) async -> UNNotificationPresentationOptions {
155 [.banner, .list, .sound]
156 }
157}