krz/domain-dig
an ios app for DNS & SSL analysis
clone: git clone https://gitbay.org/krz/domain-dig.git
main: DomainDig/AppInfoView.swift · raw
1import SwiftUI
2
3/// Settings → App Info. Shows app/build metadata and links out to docs, source,
4/// privacy, support, and the App Store. The actionable rows are driven by a
5/// single declarative `AppInfoRow` model so titles, icons, and destinations live
6/// in one place; only the Share row is special-cased (a `ShareLink` view).
7struct AppInfoView: View {
8 @Environment(\.openURL) private var openURL
9 @State private var cloudSyncService = CloudSyncService.shared
10 @State private var activeSheet: AppInfoSheet?
11
12 var body: some View {
13 Form {
14 Section("About") {
15 LabeledContent("Version", value: AppInfo.versionDisplay)
16 LabeledContent("Storage", value: cloudSyncService.isEnabled ? "Local-first + iCloud" : "Local-only")
17 LabeledContent("Backup Schema", value: "v\(DomainDigBackup.currentSchemaVersion)")
18 LabeledContent("Minimum iOS", value: AppInfo.minimumOS)
19 }
20
21 Section("Resources") {
22 ForEach(resourceRows) { row($0) }
23 }
24
25 Section("Support") {
26 ForEach(supportRows) { row($0) }
27 }
28
29 Section {
30 Button {
31 openURL(AppLinks.writeReview)
32 } label: {
33 Label("Rate DomainDig", systemImage: "star")
34 }
35 .accessibilityHint("Opens the App Store")
36
37 ShareLink(item: AppLinks.appStoreListing) {
38 Label("Share DomainDig", systemImage: "square.and.arrow.up")
39 }
40 .accessibilityHint("Opens the share sheet")
41 } header: {
42 Text("Support the App")
43 } footer: {
44 Text(AppLinks.copyright)
45 .frame(maxWidth: .infinity, alignment: .center)
46 }
47 }
48 .navigationTitle("App Info")
49 .task {
50 await cloudSyncService.refreshAvailability()
51 }
52 .sheet(item: $activeSheet) { sheet in
53 NavigationStack {
54 switch sheet {
55 case .whatsNew:
56 WhatsNewView()
57 case .acknowledgements:
58 AcknowledgementsView()
59 case .reportIssue:
60 ReportIssueView()
61 }
62 }
63 }
64 }
65
66 private var resourceRows: [AppInfoRow] {
67 [
68 AppInfoRow(title: "What's New", systemImage: "sparkles", action: .sheet(.whatsNew)),
69 AppInfoRow(title: "Documentation & FAQ", systemImage: "book", action: .openURL(AppLinks.documentation)),
70 AppInfoRow(title: "Source Code", systemImage: "chevron.left.forwardslash.chevron.right", action: .openURL(AppLinks.sourceCode)),
71 AppInfoRow(title: "Privacy Policy", systemImage: "hand.raised", action: .openURL(AppLinks.privacyPolicy)),
72 AppInfoRow(title: "Acknowledgements", systemImage: "checkmark.seal", action: .sheet(.acknowledgements))
73 ]
74 }
75
76 private var supportRows: [AppInfoRow] {
77 [
78 AppInfoRow(title: "Report an Issue", systemImage: "ladybug", action: .sheet(.reportIssue)),
79 AppInfoRow(
80 title: "Contact",
81 systemImage: "envelope",
82 action: .mail(address: AppLinks.supportEmail, subject: "DomainDig", body: "")
83 )
84 ]
85 }
86
87 @ViewBuilder
88 private func row(_ item: AppInfoRow) -> some View {
89 switch item.action {
90 case .openURL(let url):
91 Link(destination: url) {
92 Label(item.title, systemImage: item.systemImage)
93 }
94 .accessibilityHint("Opens outside the app")
95 case .mail(let address, let subject, let body):
96 Button {
97 if let url = Self.mailURL(to: address, subject: subject, body: body) {
98 openURL(url)
99 }
100 } label: {
101 Label(item.title, systemImage: item.systemImage)
102 }
103 .accessibilityHint("Opens your mail app")
104 case .sheet(let sheet):
105 Button {
106 activeSheet = sheet
107 } label: {
108 Label(item.title, systemImage: item.systemImage)
109 }
110 }
111 }
112
113 static func mailURL(to address: String, subject: String, body: String) -> URL? {
114 var components = URLComponents()
115 components.scheme = "mailto"
116 components.path = address
117 var query: [URLQueryItem] = []
118 if !subject.isEmpty { query.append(URLQueryItem(name: "subject", value: subject)) }
119 if !body.isEmpty { query.append(URLQueryItem(name: "body", value: body)) }
120 components.queryItems = query.isEmpty ? nil : query
121 return components.url
122 }
123}
124
125private struct AppInfoRow: Identifiable {
126 enum Action {
127 case openURL(URL)
128 case mail(address: String, subject: String, body: String)
129 case sheet(AppInfoSheet)
130 }
131
132 let id = UUID()
133 let title: String
134 let systemImage: String
135 let action: Action
136}
137
138private enum AppInfoSheet: String, Identifiable {
139 case whatsNew
140 case acknowledgements
141 case reportIssue
142
143 var id: String { rawValue }
144}
145
146// MARK: - Sheets
147
148private struct WhatsNewView: View {
149 @Environment(\.dismiss) private var dismiss
150 private let notes = ReleaseNotes.bundled()
151
152 var body: some View {
153 Form {
154 if let notes {
155 Section {
156 ForEach(Array(notes.highlights.enumerated()), id: \.offset) { _, highlight in
157 Label(highlight, systemImage: "sparkle")
158 .labelStyle(.titleAndIcon)
159 }
160 } header: {
161 Text("Version \(notes.version)")
162 }
163 } else {
164 Section {
165 Text("Release notes are unavailable.")
166 .foregroundStyle(Color(.appTextSecondary))
167 }
168 }
169 }
170 .navigationTitle("What's New")
171 .navigationBarTitleDisplayMode(.inline)
172 .toolbar {
173 ToolbarItem(placement: .confirmationAction) {
174 Button("Done") { dismiss() }
175 }
176 }
177 }
178}
179
180private struct AcknowledgementsView: View {
181 @Environment(\.dismiss) private var dismiss
182
183 var body: some View {
184 Form {
185 Section("Dependencies") {
186 Text("DomainDig has no third-party dependencies. It is built entirely on Apple's frameworks.")
187 }
188 Section("License") {
189 Text("DomainDig is released under the MIT License.")
190 Text(AppLinks.copyright)
191 .foregroundStyle(Color(.appTextSecondary))
192 }
193 }
194 .navigationTitle("Acknowledgements")
195 .navigationBarTitleDisplayMode(.inline)
196 .toolbar {
197 ToolbarItem(placement: .confirmationAction) {
198 Button("Done") { dismiss() }
199 }
200 }
201 }
202}
203
204private struct ReportIssueView: View {
205 @Environment(\.dismiss) private var dismiss
206 @Environment(\.openURL) private var openURL
207
208 private let diagnostics = AppInfo.diagnosticsReport
209
210 var body: some View {
211 Form {
212 Section {
213 Text("These details are included so issues can be reproduced. They are only added when you send a report — nothing is collected in the background.")
214 .foregroundStyle(Color(.appTextSecondary))
215 }
216
217 Section("Included Diagnostics") {
218 Text(diagnostics)
219 .font(.system(.footnote, design: .monospaced))
220 .textSelection(.enabled)
221 }
222
223 Section {
224 Button {
225 if let url = AppInfoView.mailURL(
226 to: AppLinks.supportEmail,
227 subject: "DomainDig Issue Report",
228 body: "\n\n---\n\(diagnostics)"
229 ) {
230 openURL(url)
231 }
232 } label: {
233 Label("Email Report", systemImage: "envelope")
234 }
235 .accessibilityHint("Opens your mail app with the diagnostics prefilled")
236
237 Link(destination: AppLinks.sourceCode.appendingPathComponent("issues/new")) {
238 Label("Open on GitHub", systemImage: "chevron.left.forwardslash.chevron.right")
239 }
240 .accessibilityHint("Opens outside the app")
241 }
242 }
243 .navigationTitle("Report an Issue")
244 .navigationBarTitleDisplayMode(.inline)
245 .toolbar {
246 ToolbarItem(placement: .cancellationAction) {
247 Button("Done") { dismiss() }
248 }
249 }
250 }
251}