From b35f1b432fc24fbab1fa05593c9a0bca7e4d95a6 Mon Sep 17 00:00:00 2001 From: Christian Cleberg Date: Thu, 23 Apr 2026 23:12:34 -0500 Subject: feat(v3.6.0): add iCloud sharing for workflows and tracked domains - implement CloudKit sharing (CKShare) for TrackedDomain and DomainWorkflow - support read-only and editable permissions - add Share actions and shared state indicators in UI - handle shared record ownership and participant roles - implement deterministic conflict resolution to prevent duplication - ensure compatibility with existing iCloud sync layer - maintain local-first behavior with graceful offline handling - remove debug logging for domain availability/rdap messages notes: - no custom backend or accounts introduced - sharing is Apple ecosystem only (iCloud-based) - large history data remains local-only --- DomainDig/ContentView.swift | 59 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) (limited to 'DomainDig/ContentView.swift') diff --git a/DomainDig/ContentView.swift b/DomainDig/ContentView.swift index e1d0a99..0386211 100644 --- a/DomainDig/ContentView.swift +++ b/DomainDig/ContentView.swift @@ -2421,6 +2421,7 @@ struct SettingsView: View { @Environment(\.appDensity) private var appDensity @Bindable var viewModel: DomainViewModel @State private var purchaseService = PurchaseService.shared + @State private var cloudSyncService = CloudSyncService.shared @AppStorage(DNSResolverOption.userDefaultsKey) private var storedResolverURL = DNSResolverOption.defaultURLString @AppStorage(AppDensity.userDefaultsKey) @@ -2480,6 +2481,43 @@ struct SettingsView: View { } } + Section("iCloud Sync") { + Toggle( + "Enable iCloud Sync", + isOn: Binding( + get: { cloudSyncService.isEnabled }, + set: { cloudSyncService.setSyncEnabled($0) } + ) + ) + + LabeledContent("Status", value: cloudSyncService.status.title) + LabeledContent( + "Last Sync", + value: cloudSyncService.lastSyncDate?.formatted(date: .abbreviated, time: .shortened) ?? "Not yet synced" + ) + + Button(cloudSyncService.status == .syncing ? "Syncing…" : "Sync Now") { + Task { + await cloudSyncService.syncNow(trigger: .manual) + } + } + .disabled(!cloudSyncService.isEnabled || cloudSyncService.status == .syncing) + + Text("iCloud Sync stores DomainDig data in your private iCloud account. DomainDig does not operate a sync server. Disabling sync keeps local data on this device.") + .font(appDensity.font(.caption, design: .default)) + .foregroundStyle(.secondary) + + Text(cloudSyncService.detailMessage) + .font(appDensity.font(.caption, design: .default)) + .foregroundStyle(.secondary) + + if let lastErrorMessage = cloudSyncService.lastErrorMessage { + Text(lastErrorMessage) + .font(appDensity.font(.caption, design: .default)) + .foregroundStyle(.red) + } + } + Section("Monitoring") { Toggle( "Enable Background Monitoring", @@ -2713,7 +2751,7 @@ struct SettingsView: View { Section("About") { LabeledContent("Version", value: appVersion) - LabeledContent("Storage", value: "Local-only") + LabeledContent("Storage", value: cloudSyncService.isEnabled ? "Local-first + iCloud" : "Local-only") LabeledContent("Backup Schema", value: "v\(DomainDigBackup.currentSchemaVersion)") } } @@ -2817,18 +2855,37 @@ struct SettingsView: View { Task { await viewModel.refreshUsageCredits() await viewModel.refreshMonitoringAuthorizationStatus() + await cloudSyncService.refreshAvailability() } } .onChange(of: resolverOption) { _, newValue in guard let presetURL = newValue.urlString else { storedResolverURL = customResolverURL.trimmingCharacters(in: .whitespacesAndNewlines) + viewModel.persistCurrentAppSettings( + resolverURLString: storedResolverURL, + appDensityRawValue: storedDensity + ) return } storedResolverURL = presetURL + viewModel.persistCurrentAppSettings( + resolverURLString: storedResolverURL, + appDensityRawValue: storedDensity + ) } .onChange(of: customResolverURL) { _, newValue in guard resolverOption == .custom else { return } storedResolverURL = newValue.trimmingCharacters(in: .whitespacesAndNewlines) + viewModel.persistCurrentAppSettings( + resolverURLString: storedResolverURL, + appDensityRawValue: storedDensity + ) + } + .onChange(of: storedDensity) { _, newValue in + viewModel.persistCurrentAppSettings( + resolverURLString: storedResolverURL, + appDensityRawValue: newValue + ) } } -- cgit v1.2.3