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/DataResetService.swift | 76 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 DomainDig/DataResetService.swift (limited to 'DomainDig/DataResetService.swift') diff --git a/DomainDig/DataResetService.swift b/DomainDig/DataResetService.swift new file mode 100644 index 0000000..e993eba --- /dev/null +++ b/DomainDig/DataResetService.swift @@ -0,0 +1,76 @@ +import Foundation +import Security + +enum DataResetService { + enum ResetError: LocalizedError { + case missingBundleIdentifier + + var errorDescription: String? { + switch self { + case .missingBundleIdentifier: + "DomainDig could not determine its local storage identifier." + } + } + } + + static func wipeAllLocalData(viewModel: DomainViewModel) async throws { + let secretReferences = await MainActor.run { + IntegrationService.shared.localSecretReferences() + } + + try await Task.detached(priority: .userInitiated) { + try performPersistentWipe(secretReferences: secretReferences) + }.value + + await LookupRuntime.shared.clearCache() + await LocalNotificationService.shared.clearAllNotifications() + await UsageCreditService.shared.resetForCurrentVersion() + + await MainActor.run { + IntegrationService.shared.resetAfterLocalWipe() + CloudSyncService.shared.resetLocalStateAfterWipe() + PurchaseService.shared.resetCachedStateAfterLocalWipe() + _ = DomainMonitoringScheduler.shared.syncSchedule() + } + + await viewModel.applyLocalDataReset() + } + + private nonisolated static func performPersistentWipe(secretReferences: [String]) throws { + guard let bundleIdentifier = Bundle.main.bundleIdentifier else { + throw ResetError.missingBundleIdentifier + } + + for reference in secretReferences { + deleteIntegrationSecret(reference: reference) + } + + try removeTemporaryFiles() + + let defaults = UserDefaults.standard + defaults.removePersistentDomain(forName: bundleIdentifier) + defaults.synchronize() + } + + private nonisolated static func removeTemporaryFiles() throws { + let tempDirectory = FileManager.default.temporaryDirectory + let urls = try FileManager.default.contentsOfDirectory( + at: tempDirectory, + includingPropertiesForKeys: nil, + options: [.skipsHiddenFiles] + ) + + for url in urls { + try? FileManager.default.removeItem(at: url) + } + } + + private nonisolated static func deleteIntegrationSecret(reference: String) { + let query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrAccount as String: reference + ] + + SecItemDelete(query as CFDictionary) + } +} -- cgit v1.2.3