aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-07-20 12:11:48 -0500
committerChristian Cleberg <[email protected]>2026-07-20 13:56:41 -0500
commitce71e6d93f5200972b9baff0fd17f55d93dbd2b9 (patch)
tree36f18c2af80318d2810445d3b711a451a292ea08
parent6f8a441b622987ffe102cf7153e1bcdd1b730cfc (diff)
downloaddomain-dig-ce71e6d93f5200972b9baff0fd17f55d93dbd2b9.tar.gz
domain-dig-ce71e6d93f5200972b9baff0fd17f55d93dbd2b9.tar.bz2
domain-dig-ce71e6d93f5200972b9baff0fd17f55d93dbd2b9.zip
misc. cleanup
-rw-r--r--DomainDig-4.1.0.patch413
-rw-r--r--DomainDig.xcodeproj/xcuserdata/cmc.xcuserdatad/xcschemes/xcschememanagement.plist4
-rw-r--r--README.md6
3 files changed, 2 insertions, 421 deletions
diff --git a/DomainDig-4.1.0.patch b/DomainDig-4.1.0.patch
deleted file mode 100644
index 3c90b2c..0000000
--- a/DomainDig-4.1.0.patch
+++ /dev/null
@@ -1,413 +0,0 @@
-diff --git a/DomainDig.xcodeproj/project.pbxproj b/DomainDig.xcodeproj/project.pbxproj
-index c641a56..2de9125 100644
---- a/DomainDig.xcodeproj/project.pbxproj
-+++ b/DomainDig.xcodeproj/project.pbxproj
-@@ -366,7 +366,7 @@
- ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
- CODE_SIGN_ENTITLEMENTS = DomainDig/DomainDig.entitlements;
- CODE_SIGN_STYLE = Automatic;
-- CURRENT_PROJECT_VERSION = 32;
-+ CURRENT_PROJECT_VERSION = 33;
- DEVELOPMENT_TEAM = ZCNAX3VL9D;
- ENABLE_PREVIEWS = YES;
- GENERATE_INFOPLIST_FILE = YES;
-@@ -383,7 +383,7 @@
- "$(inherited)",
- "@executable_path/Frameworks",
- );
-- MARKETING_VERSION = 4.0.0;
-+ MARKETING_VERSION = 4.1.0;
- PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.DomainDig;
- PRODUCT_NAME = "$(TARGET_NAME)";
- STRING_CATALOG_GENERATE_SYMBOLS = YES;
-@@ -403,7 +403,7 @@
- ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
- CODE_SIGN_ENTITLEMENTS = DomainDig/DomainDig.entitlements;
- CODE_SIGN_STYLE = Automatic;
-- CURRENT_PROJECT_VERSION = 32;
-+ CURRENT_PROJECT_VERSION = 33;
- DEVELOPMENT_TEAM = ZCNAX3VL9D;
- ENABLE_PREVIEWS = YES;
- GENERATE_INFOPLIST_FILE = YES;
-@@ -420,7 +420,7 @@
- "$(inherited)",
- "@executable_path/Frameworks",
- );
-- MARKETING_VERSION = 4.0.0;
-+ MARKETING_VERSION = 4.1.0;
- PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.DomainDig;
- PRODUCT_NAME = "$(TARGET_NAME)";
- STRING_CATALOG_GENERATE_SYMBOLS = YES;
-diff --git a/DomainDig/CloudSyncService.swift b/DomainDig/CloudSyncService.swift
-index 1cf41d9..2cb4cfe 100644
---- a/DomainDig/CloudSyncService.swift
-+++ b/DomainDig/CloudSyncService.swift
-@@ -321,6 +321,20 @@ final class CloudSyncService {
- }
- }
-
-+ func resetLocalStateAfterWipe() {
-+ scheduledSyncTask?.cancel()
-+ scheduledSyncTask = nil
-+ syncTask?.cancel()
-+ syncTask = nil
-+
-+ let syncEnabled = defaults.bool(forKey: StorageKey.isEnabled)
-+ isEnabled = syncEnabled
-+ status = CloudSyncStatus(rawValue: defaults.string(forKey: StorageKey.status) ?? "") ?? (syncEnabled ? .synced : .disabled)
-+ lastSyncDate = defaults.object(forKey: StorageKey.lastSyncDate) as? Date
-+ lastErrorMessage = defaults.string(forKey: StorageKey.lastErrorMessage)
-+ detailMessage = defaults.string(forKey: StorageKey.detailMessage) ?? "DomainDig stores synced data in your private iCloud account."
-+ }
-+
- func acceptShare(metadata: CKShare.Metadata) async throws {
- guard let container = cloudKitContainer() else {
- throw CloudSyncRuntimeError.missingEntitlement
-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<BatchSweepSummary?> {
-+ 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
-+ }
- }
- }
-
-diff --git a/DomainDig/DomainViewModel.swift b/DomainDig/DomainViewModel.swift
-index b65c6df..1d75221 100644
---- a/DomainDig/DomainViewModel.swift
-+++ b/DomainDig/DomainViewModel.swift
-@@ -953,6 +953,46 @@ final class DomainViewModel {
- refreshDataLifecycleSummary()
- }
-
-+ func applyLocalDataReset() async {
-+ domain = ""
-+ bulkInput = ""
-+ reset()
-+
-+ recentSearches = []
-+ savedDomains = []
-+ trackedDomains = []
-+ history = []
-+ workflows = []
-+ historySearchText = ""
-+ historyDateFilter = .all
-+ historyChangeFilter = .all
-+ historySortOption = .newest
-+ timelineGrouping = .relativeDay
-+ timelineDomainFilter = ""
-+ watchlistSearchText = ""
-+ watchlistFilter = .all
-+ watchlistSortOption = .pinned
-+ dashboardSearchText = ""
-+ dashboardFilter = .all
-+ monitoringSettings = MonitoringSettings()
-+ monitoringLogs = []
-+ notificationsAuthorized = false
-+ monitoringRunInProgress = false
-+ monitoringStatusMessage = nil
-+ portabilityStatusMessage = "All local data removed."
-+ upgradePrompt = nil
-+ isPaywallPresented = false
-+ selectedSnapshotIDs.removeAll()
-+ activeDomainDiff = nil
-+ activeDiffChangeIndex = 0
-+ latestBatchSweepSummary = nil
-+ latestWorkflowRunSummary = nil
-+ historyAutoPruneOption = Self.loadHistoryAutoPruneOption()
-+ refreshDataLifecycleSummary()
-+ await refreshUsageCredits()
-+ await refreshMonitoringAuthorizationStatus()
-+ }
-+
- func refreshMonitoringAuthorizationStatus() async {
- let settings = await UNUserNotificationCenter.current().notificationSettings()
- monitoringNotificationStatus = settings.authorizationStatus
-diff --git a/DomainDig/IntegrationService.swift b/DomainDig/IntegrationService.swift
-index 838cfc2..dba8034 100644
---- a/DomainDig/IntegrationService.swift
-+++ b/DomainDig/IntegrationService.swift
-@@ -177,6 +177,28 @@ final class IntegrationService {
- scheduleProcessing(force: true)
- }
-
-+ func localSecretReferences() -> [String] {
-+ targets.compactMap { target in
-+ switch target.configuration {
-+ case .webhook(let configuration):
-+ configuration.credentialReference
-+ case .slack(let configuration):
-+ configuration.credentialReference
-+ case .email(let configuration):
-+ configuration.credentialReference
-+ }
-+ }
-+ }
-+
-+ func resetAfterLocalWipe() {
-+ processingTask?.cancel()
-+ processingTask = nil
-+ targets = []
-+ deliveryRecords = []
-+ queue = []
-+ statusMessage = nil
-+ }
-+
- private func scheduleProcessing(force: Bool = false) {
- if force {
- processingTask?.cancel()
-diff --git a/DomainDig/LocalNotificationService.swift b/DomainDig/LocalNotificationService.swift
-index 93beca5..eef0567 100644
---- a/DomainDig/LocalNotificationService.swift
-+++ b/DomainDig/LocalNotificationService.swift
-@@ -117,6 +117,12 @@ final class LocalNotificationService {
- )
- }
-
-+ func clearAllNotifications() async {
-+ let center = UNUserNotificationCenter.current()
-+ center.removeAllPendingNotificationRequests()
-+ center.removeAllDeliveredNotifications()
-+ }
-+
- private func schedule(
- identifier: String,
- title: String,
-diff --git a/DomainDig/PurchaseService.swift b/DomainDig/PurchaseService.swift
-index af34a33..dbc31ba 100644
---- a/DomainDig/PurchaseService.swift
-+++ b/DomainDig/PurchaseService.swift
-@@ -196,6 +196,14 @@ final class PurchaseService {
- errorMessage = nil
- }
-
-+ func resetCachedStateAfterLocalWipe() {
-+ currentTier = Self.cachedTier
-+ activeProductID = Self.cachedEntitlement?.activeProductID
-+ statusMessage = nil
-+ errorMessage = nil
-+ applyDebugOverrideIfNeeded()
-+ }
-+
- private func apply(transaction: Transaction) {
- guard Self.productIDs.contains(transaction.productID), transaction.revocationDate == nil else {
- return
-diff --git a/DomainDig/WorkflowsView.swift b/DomainDig/WorkflowsView.swift
-index 06ae96b..11273c7 100644
---- a/DomainDig/WorkflowsView.swift
-+++ b/DomainDig/WorkflowsView.swift
-@@ -100,7 +100,8 @@ struct WorkflowsView: View {
- title: "No Workflows Yet",
- message: "Workflows save a reusable set of domains so repeat inspections take one tap instead of rebuilding the same batch each time.",
- suggestion: "Create a workflow for a weekly audit set, customer domains, or a monitoring group.",
-- systemImage: "square.stack.3d.down.right"
-+ systemImage: "square.stack.3d.down.right",
-+ showsCardBackground: false
- )
- }
- .listRowBackground(Color(.systemGray6).opacity(0.5))
-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)
-+ }
-+}
diff --git a/DomainDig.xcodeproj/xcuserdata/cmc.xcuserdatad/xcschemes/xcschememanagement.plist b/DomainDig.xcodeproj/xcuserdata/cmc.xcuserdatad/xcschemes/xcschememanagement.plist
index 5f600f0..cb3b8b8 100644
--- a/DomainDig.xcodeproj/xcuserdata/cmc.xcuserdatad/xcschemes/xcschememanagement.plist
+++ b/DomainDig.xcodeproj/xcuserdata/cmc.xcuserdatad/xcschemes/xcschememanagement.plist
@@ -12,12 +12,12 @@
<key>DomainDigShareExtension.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
- <integer>1</integer>
+ <integer>2</integer>
</dict>
<key>DomainDigWidgetExtension.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
- <integer>2</integer>
+ <integer>1</integer>
</dict>
</dict>
<key>SuppressBuildableAutocreation</key>
diff --git a/README.md b/README.md
index 71ac70d..b8562b0 100644
--- a/README.md
+++ b/README.md
@@ -7,12 +7,6 @@
DomainDig is a local-first iOS domain inspection toolkit for DNS, web, ownership, monitoring, reporting, and audit workflows. The app gathers a point-in-time domain snapshot, normalizes it into a canonical `DomainReport`, and keeps user data on device unless the user exports, shares, or syncs it.
-## Current Release Target
-
-Current release: `v4.6.0`.
-
-This minor release makes alerts and results first-class across contexts: a sweep Live Activity (Lock Screen + Dynamic Island), a share-sheet extension for digging a domain from Safari, an iPad `NavigationSplitView` layout, and actionable, grouped notifications.
-
## Features
- Domain inspection for DNS records, email security, TLS certificates, HTTP headers, redirects, IP geolocation, reachability, open ports, RDAP, ownership, subdomain discovery, and availability.