summaryrefslogtreecommitdiff
path: root/DomainDig
diff options
context:
space:
mode:
Diffstat (limited to 'DomainDig')
-rw-r--r--DomainDig/DomainDig.entitlements4
-rw-r--r--DomainDig/DomainDigApp.swift2
-rw-r--r--DomainDig/DomainDigIntents.swift78
-rw-r--r--DomainDig/DomainViewModel+Widget.swift60
-rw-r--r--DomainDig/DomainViewModel.swift1
-rw-r--r--DomainDig/RootTabView.swift19
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()
}
}
}