diff options
Diffstat (limited to 'DomainDig')
| -rw-r--r-- | DomainDig/DomainDig.entitlements | 4 | ||||
| -rw-r--r-- | DomainDig/DomainDigApp.swift | 2 | ||||
| -rw-r--r-- | DomainDig/DomainDigIntents.swift | 78 | ||||
| -rw-r--r-- | DomainDig/DomainViewModel+Widget.swift | 60 | ||||
| -rw-r--r-- | DomainDig/DomainViewModel.swift | 1 | ||||
| -rw-r--r-- | DomainDig/RootTabView.swift | 19 |
6 files changed, 113 insertions, 51 deletions
diff --git a/DomainDig/DomainDig.entitlements b/DomainDig/DomainDig.entitlements index d792729..223e66a 100644 --- a/DomainDig/DomainDig.entitlements +++ b/DomainDig/DomainDig.entitlements @@ -10,5 +10,9 @@ <array> <string>CloudKit</string> </array> + <key>com.apple.security.application-groups</key> + <array> + <string>group.net.cleberg.DomainDig</string> + </array> </dict> </plist> diff --git a/DomainDig/DomainDigApp.swift b/DomainDig/DomainDigApp.swift index ff40caf..8716806 100644 --- a/DomainDig/DomainDigApp.swift +++ b/DomainDig/DomainDigApp.swift @@ -39,6 +39,7 @@ struct DomainDigApp: App { cloudSyncService.scheduleSyncIfNeeded(trigger: .launch) viewModel.monitoringStatusMessage = DomainMonitoringScheduler.shared.syncSchedule() IntegrationService.shared.processQueueNow() + viewModel.refreshWidgetData() } .onReceive(NotificationCenter.default.publisher(for: .cloudSyncDidApplyChanges)) { _ in viewModel.refreshPersistedData() @@ -57,6 +58,7 @@ struct DomainDigApp: App { cloudSyncService.scheduleSyncIfNeeded(trigger: .launch) viewModel.monitoringStatusMessage = DomainMonitoringScheduler.shared.syncSchedule() IntegrationService.shared.processQueueNow() + viewModel.refreshWidgetData() } } } diff --git a/DomainDig/DomainDigIntents.swift b/DomainDig/DomainDigIntents.swift index 44a1dc4..c0c73cc 100644 --- a/DomainDig/DomainDigIntents.swift +++ b/DomainDig/DomainDigIntents.swift @@ -124,6 +124,24 @@ struct AddToWatchlistIntent: AppIntent { } } +/// App Intent that opens DomainDig and re-inspects every tracked domain. It runs +/// through the existing view-model batch path (`refreshAllTrackedDomains`), which +/// enforces the batch feature gate and surfaces the paywall when needed. +struct RunSweepIntent: AppIntent { + static var title: LocalizedStringResource = "Run Watchlist Sweep" + static var description = IntentDescription( + "Open DomainDig and re-inspect every domain on your watchlist." + ) + + static var openAppWhenRun = true + + @MainActor + func perform() async throws -> some IntentResult { + DomainDigIntentRouter.shared.pendingAction = .sweep + return .result() + } +} + /// In-process hand-off from an `openAppWhenRun` intent to the running SwiftUI /// layer. `RootTabView` observes `pendingAction` and performs it. @MainActor @@ -134,57 +152,6 @@ final class DomainDigIntentRouter { private init() {} } -/// Shared builder/parser for the `domaindig://` URL scheme, used by both the -/// intents (to open the app) and the app (to route incoming links). -enum DomainDigDeepLink { - static let scheme = "domaindig" - - enum Action: Equatable { - case inspect(String) - case watch(String) - - var host: String { - switch self { - case .inspect: return "inspect" - case .watch: return "watch" - } - } - - var domain: String { - switch self { - case let .inspect(domain), let .watch(domain): return domain - } - } - } - - static func url(for action: Action) -> URL { - var components = URLComponents() - components.scheme = scheme - components.host = action.host - components.queryItems = [URLQueryItem(name: "domain", value: action.domain)] - // The scheme and host are fixed and the domain is percent-encoded by - // URLComponents, so this is always a valid URL. - return components.url! - } - - static func action(from url: URL) -> Action? { - guard url.scheme == scheme else { return nil } - - let domain = URLComponents(url: url, resolvingAgainstBaseURL: false)? - .queryItems? - .first { $0.name == "domain" }? - .value? - .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" - guard !domain.isEmpty else { return nil } - - switch url.host() { - case "inspect": return .inspect(domain) - case "watch": return .watch(domain) - default: return nil - } - } -} - /// Exposes DomainDig intents to Spotlight and Siri with invocation phrases. struct DomainDigShortcuts: AppShortcutsProvider { static var appShortcuts: [AppShortcut] { @@ -206,5 +173,14 @@ struct DomainDigShortcuts: AppShortcutsProvider { shortTitle: "Add to Watchlist", systemImageName: "plus.circle" ) + AppShortcut( + intent: RunSweepIntent(), + phrases: [ + "Run a sweep with \(.applicationName)", + "Sweep my \(.applicationName) watchlist" + ], + shortTitle: "Run Sweep", + systemImageName: "arrow.trianglehead.2.clockwise" + ) } } diff --git a/DomainDig/DomainViewModel+Widget.swift b/DomainDig/DomainViewModel+Widget.swift new file mode 100644 index 0000000..59b28e0 --- /dev/null +++ b/DomainDig/DomainViewModel+Widget.swift @@ -0,0 +1,60 @@ +import Foundation +import WidgetKit + +extension DomainViewModel { + /// Publishes the current portfolio state to the App Group container so the + /// widget can render it, then asks WidgetKit to refresh its timelines. + func refreshWidgetData() { + let data = portfolioDashboardData + let snapshot = data.snapshot + + let ordered = data.domainStates.sorted { lhs, rhs in + if lhs.trackedDomain.isPinned != rhs.trackedDomain.isPinned { + return lhs.trackedDomain.isPinned + } + return lhs.health.widgetSeverityRank > rhs.health.widgetSeverityRank + } + + let domains = ordered.prefix(6).map { state in + DomainDigWidgetDomain( + domain: state.trackedDomain.domain, + isPinned: state.trackedDomain.isPinned, + status: state.health.widgetStatus, + certDaysRemaining: state.certificateDaysRemaining, + lastChange: state.lastChangeDate + ) + } + + DomainDigWidgetStore.write( + DomainDigWidgetData( + generatedAt: Date(), + totalDomains: snapshot.totalDomains, + healthyCount: snapshot.healthyCount, + warningCount: snapshot.warningCount, + criticalCount: snapshot.criticalCount, + expiringSoonCount: snapshot.expiringSoonCount, + unreachableCount: snapshot.unreachableCount, + domains: Array(domains) + ) + ) + WidgetCenter.shared.reloadAllTimelines() + } +} + +private extension DomainHealth { + var widgetStatus: DomainDigWidgetStatus { + switch self { + case .healthy: return .healthy + case .warning: return .warning + case .critical: return .critical + } + } + + var widgetSeverityRank: Int { + switch self { + case .healthy: return 0 + case .warning: return 1 + case .critical: return 2 + } + } +} diff --git a/DomainDig/DomainViewModel.swift b/DomainDig/DomainViewModel.swift index f0c4411..3f37cb9 100644 --- a/DomainDig/DomainViewModel.swift +++ b/DomainDig/DomainViewModel.swift @@ -2582,6 +2582,7 @@ final class DomainViewModel { DomainDataPortabilityService.saveTrackedDomains(trackedDomains) CloudSyncService.shared.scheduleSyncIfNeeded() refreshDataLifecycleSummary() + refreshWidgetData() } private func beginBulkPersistenceDeferral() { diff --git a/DomainDig/RootTabView.swift b/DomainDig/RootTabView.swift index 72038a1..9823d37 100644 --- a/DomainDig/RootTabView.swift +++ b/DomainDig/RootTabView.swift @@ -12,6 +12,7 @@ struct RootTabView: View { @Bindable var viewModel: DomainViewModel @State private var purchaseService = PurchaseService.shared @State private var intentRouter = DomainDigIntentRouter.shared + @State private var detailDomain: TrackedDomain? @State private var selectedTab: RootTab = FeatureAccessService.currentTier == .free ? .inspect : .dashboard var body: some View { @@ -83,6 +84,11 @@ struct RootTabView: View { selectedTab = .dashboard } } + .sheet(item: $detailDomain) { trackedDomain in + NavigationStack { + TrackedDomainDetailView(viewModel: viewModel, trackedDomain: trackedDomain) + } + } .onOpenURL { url in guard let action = DomainDigDeepLink.action(from: url) else { return } perform(action) @@ -113,6 +119,19 @@ struct RootTabView: View { if viewModel.trackDomain(domain: domain, availabilityStatus: nil) { selectedTab = .dashboard } + case let .detail(domain): + // Open the tracked domain's detail (e.g. from a widget tap). If it is + // no longer tracked, fall back to inspecting it. + if let tracked = viewModel.trackedDomains.first( + where: { $0.domain.caseInsensitiveCompare(domain) == .orderedSame } + ) { + detailDomain = tracked + } else { + perform(.inspect(domain)) + } + case .sweep: + selectedTab = .dashboard + viewModel.refreshAllTrackedDomains() } } } |
