krz/domain-dig

an ios app for DNS & SSL analysis

clone: git clone https://gitbay.org/krz/domain-dig.git

v5.0.1: 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}