From 535bb0ff0f64d57be1074e33ceb42f598f80205b Mon Sep 17 00:00:00 2001 From: Christian Cleberg Date: Sat, 25 Apr 2026 00:41:34 -0500 Subject: DomainDig v4.1.0: Add a full local data reset flow in Data Management - add a destructive Delete All Data action with confirmation, progress, success, and failure handling - centralize wipe behavior in DataResetService instead of scattering delete logic in views - clear local persistence, temp/export files, integration secrets, notifications, caches, and in-memory app state - reset sync, purchase, and integration services after wipe so the app returns to a clean first-launch state Polish batch and empty-state UX - present the batch sweep summary after manual bulk searches complete, not only from Watchlist - align the Workflows empty state styling with other empty states by removing the extra background card treatment Bump the project version from 4.0.0 to 4.1.0 --- DomainDig/ContentView.swift | 92 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) (limited to 'DomainDig/ContentView.swift') diff --git a/DomainDig/ContentView.swift b/DomainDig/ContentView.swift index c9f40d6..c5c8e50 100644 --- a/DomainDig/ContentView.swift +++ b/DomainDig/ContentView.swift @@ -261,6 +261,9 @@ struct ContentView: View { availableDomains: viewModel.batchResults.map(\.domain) ) } + .sheet(item: manualBatchSummaryBinding) { summary in + BatchSweepSummaryView(viewModel: viewModel, summary: summary) + } .sheet(isPresented: $showingTimeline) { NavigationStack { TimelineView(viewModel: viewModel, domain: viewModel.searchedDomain) @@ -268,6 +271,19 @@ struct ContentView: View { } } + private var manualBatchSummaryBinding: Binding { + Binding( + get: { + guard let summary = viewModel.latestBatchSweepSummary, + summary.source == .manual else { + return nil + } + return summary + }, + set: { viewModel.latestBatchSweepSummary = $0 } + ) + } + private var inputSection: some View { VStack(spacing: appDensity.metrics.cardSpacing + 2) { Picker("Mode", selection: $inputMode) { @@ -3257,6 +3273,10 @@ private struct DataManagementSettingsView: View { @State private var showClearCacheConfirmation = false @State private var showClearWorkflowsConfirmation = false @State private var showClearTrackedDomainsConfirmation = false + @State private var showDeleteAllConfirmation = false + @State private var deleteAllErrorMessage: String? + @State private var deleteAllSuccessMessage: String? + @State private var isDeletingAllData = false var body: some View { Form { @@ -3277,7 +3297,28 @@ private struct DataManagementSettingsView: View { showClearTrackedDomainsConfirmation = true } } + + Section { + Button(role: .destructive) { + showDeleteAllConfirmation = true + } label: { + HStack { + Text("Delete All Data") + Spacer() + if isDeletingAllData { + ProgressView() + .controlSize(.small) + } + } + } + .disabled(isDeletingAllData) + } header: { + Text("Danger Zone") + } footer: { + Text("Permanently removes all local DomainDig data from this device.") + } } + .disabled(isDeletingAllData) .navigationTitle("Data Management") .alert("Clear history?", isPresented: $showClearHistoryConfirmation) { Button("Clear", role: .destructive) { @@ -3311,6 +3352,57 @@ private struct DataManagementSettingsView: View { } message: { Text("This removes the watchlist and clears monitoring run history. History and workflows stay intact.") } + .alert("Delete All Data?", isPresented: $showDeleteAllConfirmation) { + Button("Cancel", role: .cancel) {} + Button("Delete All Data", role: .destructive) { + deleteAllData() + } + } message: { + Text("This will permanently remove all saved DomainDig data from this device. This includes tracked domains, monitoring history, snapshots, exports, cached reports, and local settings. This action cannot be undone.") + } + .alert("Delete Failed", isPresented: Binding( + get: { deleteAllErrorMessage != nil }, + set: { if !$0 { deleteAllErrorMessage = nil } } + )) { + Button("OK", role: .cancel) {} + } message: { + Text(deleteAllErrorMessage ?? "The local data reset could not be completed.") + } + .safeAreaInset(edge: .bottom) { + if let deleteAllSuccessMessage { + Text(deleteAllSuccessMessage) + .font(.footnote.weight(.medium)) + .foregroundStyle(.secondary) + .padding(.horizontal, 14) + .padding(.vertical, 10) + .background(.thinMaterial, in: Capsule()) + .padding(.bottom, 8) + .transition(.move(edge: .bottom).combined(with: .opacity)) + } + } + } + + private func deleteAllData() { + guard !isDeletingAllData else { return } + + isDeletingAllData = true + deleteAllErrorMessage = nil + deleteAllSuccessMessage = nil + + Task { + do { + try await DataResetService.wipeAllLocalData(viewModel: viewModel) + deleteAllSuccessMessage = "All local data removed." + try? await Task.sleep(for: .seconds(2)) + if deleteAllSuccessMessage == "All local data removed." { + deleteAllSuccessMessage = nil + } + } catch { + deleteAllErrorMessage = error.localizedDescription + } + + isDeletingAllData = false + } } } -- cgit v1.2.3