From b4b94d54cbed9334a28cdcd70d3a8a0fd06e04ef Mon Sep 17 00:00:00 2001 From: Christian Cleberg Date: Fri, 17 Jul 2026 08:36:05 -0500 Subject: Implement v4.6.0: sweep Live Activity, share extension, iPad split view, actionable notifications - Sweep Live Activity: SweepActivityAttributes (Shared/), lock-screen + Dynamic Island UI in the widget extension, driven by SweepActivityController wired into the batch pipeline (begin/update/end); NSSupportsLiveActivities in Info.plist. - Share extension (DomainDigShareExtension): accepts a web URL from the share sheet, extracts the host, and hands it to the app via the App Group inbox (DomainDigShareInbox); the app consumes it on activation and inspects it. - iPad layout: RootTabView uses NavigationSplitView in the regular size class and the tab bar in compact. - Actionable notifications: per-domain threadIdentifier grouping, a Re-inspect action, and tap routing into the domain detail via the intent router. - Bump version to 4.6.0 (build 38) across app, widget, and share targets. --- DomainDig/LocalNotificationService.swift | 65 +++++++++++++++++++++++++++++--- 1 file changed, 59 insertions(+), 6 deletions(-) (limited to 'DomainDig/LocalNotificationService.swift') diff --git a/DomainDig/LocalNotificationService.swift b/DomainDig/LocalNotificationService.swift index eef0567..f417619 100644 --- a/DomainDig/LocalNotificationService.swift +++ b/DomainDig/LocalNotificationService.swift @@ -7,8 +7,27 @@ final class LocalNotificationService { private init() {} + static let domainUserInfoKey = "domain" + static let domainCategoryIdentifier = "domain-event" + static let reinspectActionIdentifier = "reinspect" + func configureForegroundPresentation() { - UNUserNotificationCenter.current().delegate = NotificationCenterDelegate.shared + let center = UNUserNotificationCenter.current() + center.delegate = NotificationCenterDelegate.shared + + let reinspect = UNNotificationAction( + identifier: Self.reinspectActionIdentifier, + title: "Re-inspect", + options: [] + ) + center.setNotificationCategories([ + UNNotificationCategory( + identifier: Self.domainCategoryIdentifier, + actions: [reinspect], + intentIdentifiers: [], + options: [] + ) + ]) } func requestAuthorizationIfNeeded() async -> Bool { @@ -44,7 +63,8 @@ final class LocalNotificationService { identifier: "domain-change-\(domain)", title: domain, body: message, - interruptionLevel: severity == .high ? .timeSensitive : .active + interruptionLevel: severity == .high ? .timeSensitive : .active, + domain: domain ) } @@ -53,7 +73,8 @@ final class LocalNotificationService { identifier: "cert-warning-\(domain)", title: domain, body: "Certificate expires in \(daysRemaining) days", - interruptionLevel: .timeSensitive + interruptionLevel: .timeSensitive, + domain: domain ) } @@ -74,7 +95,8 @@ final class LocalNotificationService { identifier: "monitoring-\(domain)-\(UUID().uuidString)", title: domain, body: message, - interruptionLevel: interruptionLevel + interruptionLevel: interruptionLevel, + domain: domain ) } @@ -103,7 +125,8 @@ final class LocalNotificationService { identifier: "monitoring-summary-\(domain)-\(UUID().uuidString)", title: domain, body: body, - interruptionLevel: interruptionLevel + interruptionLevel: interruptionLevel, + domain: domain ) } @@ -127,13 +150,20 @@ final class LocalNotificationService { identifier: String, title: String, body: String, - interruptionLevel: UNNotificationInterruptionLevel + interruptionLevel: UNNotificationInterruptionLevel, + domain: String? = nil ) async { let content = UNMutableNotificationContent() content.title = title content.body = body content.sound = .default content.interruptionLevel = interruptionLevel + if let domain { + // Group alerts per domain and let taps/actions route back into it. + content.threadIdentifier = domain + content.userInfo = [Self.domainUserInfoKey: domain] + content.categoryIdentifier = Self.domainCategoryIdentifier + } let request = UNNotificationRequest( identifier: identifier, @@ -154,4 +184,27 @@ private final class NotificationCenterDelegate: NSObject, UNUserNotificationCent ) async -> UNNotificationPresentationOptions { [.banner, .list, .sound] } + + func userNotificationCenter( + _ center: UNUserNotificationCenter, + didReceive response: UNNotificationResponse + ) async { + let userInfo = response.notification.request.content.userInfo + guard let domain = userInfo[LocalNotificationService.domainUserInfoKey] as? String, + !domain.isEmpty + else { return } + + let action: DomainDigDeepLink.Action + switch response.actionIdentifier { + case LocalNotificationService.reinspectActionIdentifier: + action = .inspect(domain) + default: + // Default tap: open the tracked domain's detail. + action = .detail(domain) + } + + await MainActor.run { + DomainDigIntentRouter.shared.pendingAction = action + } + } } -- cgit v1.2.3