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/WatchlistView.swift | 56 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 49 insertions(+), 7 deletions(-) (limited to 'DomainDig/WatchlistView.swift') diff --git a/DomainDig/WatchlistView.swift b/DomainDig/WatchlistView.swift index 968914f..1a38282 100644 --- a/DomainDig/WatchlistView.swift +++ b/DomainDig/WatchlistView.swift @@ -193,10 +193,12 @@ struct WatchlistView: View { } .tint(.cyan) - Button(role: .destructive) { - viewModel.deleteTrackedDomain(trackedDomain) - } label: { - Label("Delete", systemImage: "trash") + if viewModel.canDelete(trackedDomain) { + Button(role: .destructive) { + viewModel.deleteTrackedDomain(trackedDomain) + } label: { + Label("Delete", systemImage: "trash") + } } } .contextMenu { @@ -219,11 +221,21 @@ struct WatchlistView: View { } label: { Label(trackedDomain.isPinned ? "Unpin" : "Pin", systemImage: trackedDomain.isPinned ? "pin.slash" : "pin") } + .disabled(!viewModel.canEdit(trackedDomain)) - Button(role: .destructive) { - viewModel.deleteTrackedDomain(trackedDomain) + Button { + // The system sharing UI manages participants and permissions. } label: { - Label("Delete", systemImage: "trash") + Label(trackedDomain.collaboration?.isShared == true ? "Shared" : "Private", systemImage: "person.2") + } + .disabled(true) + + if viewModel.canDelete(trackedDomain) { + Button(role: .destructive) { + viewModel.deleteTrackedDomain(trackedDomain) + } label: { + Label("Delete", systemImage: "trash") + } } } .listRowBackground(Color(.systemGray6).opacity(0.5)) @@ -287,6 +299,12 @@ struct WatchlistRowView: View { .font(appDensity.font(.caption2)) .foregroundStyle(.secondary) + if let collaboration = trackedDomain.collaboration, collaboration.isShared { + Text("\(collaboration.ownership.title) • \(collaboration.permission.title)") + .font(appDensity.font(.caption2)) + .foregroundStyle(.secondary) + } + HStack(spacing: 8) { Text(trackedDomain.monitoringEnabled ? "Monitoring on" : "Monitoring off") if let lastMonitoredAt = trackedDomain.lastMonitoredAt { @@ -340,6 +358,17 @@ struct WatchlistRowView: View { @ViewBuilder private var indicatorRow: some View { HStack(spacing: 8) { + if trackedDomain.collaboration?.isShared == true { + AppStatusBadgeView( + model: .init( + title: "Shared", + systemImage: "person.2.fill", + foregroundColor: .cyan, + backgroundColor: .cyan.opacity(0.16) + ) + ) + } + AppStatusBadgeView(model: AppStatusFactory.change(trackedDomain.lastChangeSummary)) if trackedDomain.certificateWarningLevel != .none { @@ -369,6 +398,7 @@ struct TrackedDomainDetailView: View { @State private var noteDraft = "" @State private var isEditingNote = false @State private var showRerunOptions = false + @State private var shareEntity: ShareableEntity? private var liveTrackedDomain: TrackedDomain { viewModel.trackedDomains.first(where: { $0.id == trackedDomain.id }) ?? trackedDomain @@ -410,6 +440,7 @@ struct TrackedDomainDetailView: View { } label: { Label(liveTrackedDomain.isPinned ? "Unpin Domain" : "Pin Domain", systemImage: liveTrackedDomain.isPinned ? "pin.slash" : "pin") } + .disabled(!viewModel.canEdit(liveTrackedDomain)) Button { viewModel.toggleMonitoring(for: liveTrackedDomain) @@ -419,6 +450,7 @@ struct TrackedDomainDetailView: View { systemImage: liveTrackedDomain.monitoringEnabled ? "bell.slash" : "bell" ) } + .disabled(!viewModel.canEdit(liveTrackedDomain)) Button { noteDraft = liveTrackedDomain.note ?? "" @@ -426,6 +458,13 @@ struct TrackedDomainDetailView: View { } label: { Label(liveTrackedDomain.note == nil ? "Add Note" : "Edit Note", systemImage: "note.text") } + .disabled(!viewModel.canEdit(liveTrackedDomain)) + + Button { + shareEntity = .trackedDomain(liveTrackedDomain.domain) + } label: { + Label(liveTrackedDomain.collaboration?.isShared == true ? "Manage Share" : "Share Domain", systemImage: "person.2") + } } .listRowBackground(Color(.systemGray6).opacity(0.5)) @@ -518,5 +557,8 @@ struct TrackedDomainDetailView: View { } } } + .sheet(item: $shareEntity) { entity in + CloudSharingSheet(entity: entity, title: liveTrackedDomain.domain) + } } } -- cgit v1.2.3