summaryrefslogtreecommitdiff
path: root/Shared
diff options
context:
space:
mode:
Diffstat (limited to 'Shared')
-rw-r--r--Shared/DomainDigDeepLink.swift71
-rw-r--r--Shared/DomainDigWidgetData.swift82
2 files changed, 153 insertions, 0 deletions
diff --git a/Shared/DomainDigDeepLink.swift b/Shared/DomainDigDeepLink.swift
new file mode 100644
index 0000000..ba1d64b
--- /dev/null
+++ b/Shared/DomainDigDeepLink.swift
@@ -0,0 +1,71 @@
+import Foundation
+
+/// Shared builder/parser for the `domaindig://` URL scheme, used by the intents
+/// and app (to open/route) and by the widget (to deep-link into a domain).
+///
+/// Lives in `Shared/` so it compiles into both the app and the widget target.
+/// It relies only on Foundation and no actor isolation, so it is safe in the
+/// widget extension (`APPLICATION_EXTENSION_API_ONLY`).
+enum DomainDigDeepLink {
+ static let scheme = "domaindig"
+
+ enum Action: Equatable {
+ case inspect(String)
+ case watch(String)
+ case detail(String)
+ case sweep
+
+ var host: String {
+ switch self {
+ case .inspect: return "inspect"
+ case .watch: return "watch"
+ case .detail: return "domain"
+ case .sweep: return "sweep"
+ }
+ }
+
+ /// The domain the action targets, if any. `.sweep` has no domain.
+ var domain: String? {
+ switch self {
+ case let .inspect(domain), let .watch(domain), let .detail(domain):
+ return domain
+ case .sweep:
+ return nil
+ }
+ }
+ }
+
+ static func url(for action: Action) -> URL {
+ var components = URLComponents()
+ components.scheme = scheme
+ components.host = action.host
+ if let domain = action.domain {
+ components.queryItems = [URLQueryItem(name: "domain", value: domain)]
+ }
+ // The scheme and host are fixed and any 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 }
+
+ if url.host() == "sweep" {
+ return .sweep
+ }
+
+ 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)
+ case "domain": return .detail(domain)
+ default: return nil
+ }
+ }
+}
diff --git a/Shared/DomainDigWidgetData.swift b/Shared/DomainDigWidgetData.swift
new file mode 100644
index 0000000..5ceae48
--- /dev/null
+++ b/Shared/DomainDigWidgetData.swift
@@ -0,0 +1,82 @@
+import Foundation
+
+/// App Group + snapshot shared between the app (writer) and the widget (reader).
+///
+/// Deliberately self-contained: it references no app-target types (TrackedDomain,
+/// PortfolioSnapshot, DomainHealth, …) so it compiles unchanged into the widget
+/// extension. The app maps its richer types into these DTOs.
+enum DomainDigWidgetStore {
+ static let appGroupID = "group.net.cleberg.DomainDig"
+ private static let key = "widgetData"
+
+ private static var defaults: UserDefaults? {
+ UserDefaults(suiteName: appGroupID)
+ }
+
+ static func write(_ data: DomainDigWidgetData) {
+ guard let defaults, let encoded = try? JSONEncoder().encode(data) else { return }
+ defaults.set(encoded, forKey: key)
+ }
+
+ static func read() -> DomainDigWidgetData? {
+ guard let defaults,
+ let encoded = defaults.data(forKey: key),
+ let data = try? JSONDecoder().decode(DomainDigWidgetData.self, from: encoded)
+ else { return nil }
+ return data
+ }
+}
+
+struct DomainDigWidgetData: Codable, Sendable {
+ var generatedAt: Date
+ var totalDomains: Int
+ var healthyCount: Int
+ var warningCount: Int
+ var criticalCount: Int
+ var expiringSoonCount: Int
+ var unreachableCount: Int
+ var domains: [DomainDigWidgetDomain]
+
+ static let placeholder = DomainDigWidgetData(
+ generatedAt: .distantPast,
+ totalDomains: 3,
+ healthyCount: 2,
+ warningCount: 1,
+ criticalCount: 0,
+ expiringSoonCount: 1,
+ unreachableCount: 0,
+ domains: [
+ DomainDigWidgetDomain(domain: "example.com", isPinned: true, status: .healthy, certDaysRemaining: 240, lastChange: nil),
+ DomainDigWidgetDomain(domain: "cleberg.net", isPinned: false, status: .warning, certDaysRemaining: 21, lastChange: nil),
+ DomainDigWidgetDomain(domain: "example.org", isPinned: false, status: .healthy, certDaysRemaining: 88, lastChange: nil)
+ ]
+ )
+
+ /// Empty state used before the app has written any data.
+ static let empty = DomainDigWidgetData(
+ generatedAt: .distantPast,
+ totalDomains: 0,
+ healthyCount: 0,
+ warningCount: 0,
+ criticalCount: 0,
+ expiringSoonCount: 0,
+ unreachableCount: 0,
+ domains: []
+ )
+}
+
+struct DomainDigWidgetDomain: Codable, Sendable, Identifiable {
+ var domain: String
+ var isPinned: Bool
+ var status: DomainDigWidgetStatus
+ var certDaysRemaining: Int?
+ var lastChange: Date?
+
+ var id: String { domain }
+}
+
+enum DomainDigWidgetStatus: String, Codable, Sendable {
+ case healthy
+ case warning
+ case critical
+}