krz/hutch
an ios client for sourcehut
clone: git clone https://gitbay.org/krz/hutch.git
v3.1.7: Hutch/Views/More/AboutView.swift · raw
1import StoreKit
2import SwiftUI
3
4struct AboutView: View {
5 @Environment(AppState.self) private var appState
6 private let appName = Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String
7 ?? Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String
8 ?? "Hutch"
9 private let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String
10 ?? "Unknown"
11 private let build = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String
12 ?? "Unknown"
13 @State private var developerRevealCount = 0
14 @State private var storeViewModel = TipStoreViewModel()
15
16 private var developerToolsVisible: Bool {
17 appState.isDebugModeEnabled || developerRevealCount >= 5
18 }
19
20 private var developerRevealFooterText: String {
21 developerRevealCount >= 5
22 ? "Debug toggle unlocked. Scroll down to Developer to enable it."
23 : "Tap the build number 5 times to reveal the debug toggle."
24 }
25
26 var body: some View {
27 Form {
28 Section {
29 VStack(alignment: .leading, spacing: 6) {
30 Text(appName)
31 .font(.title2.weight(.semibold))
32 Text("A native SourceHut client for iOS.")
33 .font(.subheadline)
34 .foregroundStyle(.secondary)
35 }
36 .padding(.vertical, 4)
37 .themedRow()
38
39 LabeledContent("Version", value: version)
40 .onTapGesture {
41 developerRevealCount = min(developerRevealCount + 1, 5)
42 }
43 .themedRow()
44 LabeledContent("Build", value: build)
45 .onTapGesture {
46 developerRevealCount = min(developerRevealCount + 1, 5)
47 }
48 .themedRow()
49 } footer: {
50 Text(developerRevealFooterText)
51 }
52
53 Section("Links") {
54 Link(destination: URL(string: "https://sr.ht")!) {
55 SwiftUI.Label("SourceHut", systemImage: "link")
56 }
57 .themedRow()
58 Link(destination: URL(string: "https://man.sr.ht")!) {
59 SwiftUI.Label("SourceHut Manuals", systemImage: "book")
60 }
61 .themedRow()
62 Link(destination: URL(string: "https://sr.ht/~ccleberg/Hutch")!) {
63 SwiftUI.Label("Project Repository", systemImage: "folder")
64 }
65 .themedRow()
66 }
67
68 Section("Support") {
69 Link(destination: URL(string: "mailto:hello@cleberg.net")!) {
70 SwiftUI.Label("Email Support", systemImage: "envelope")
71 }
72 .themedRow()
73 }
74
75 Section("Privacy") {
76 Text("Hutch uses your SourceHut personal access token to make requests on your behalf. The token is stored locally in the iOS keychain.")
77 .font(.subheadline)
78 .foregroundStyle(.secondary)
79 .themedRow()
80
81 Link(destination: URL(string: "https://zerolabs.sh/hutch/privacy-policy/")!) {
82 SwiftUI.Label("Privacy Policy", systemImage: "hand.raised")
83 }
84 .themedRow()
85 }
86
87 Section("Acknowledgements") {
88 Text("Built for SourceHut users who want quick access to repositories, builds, and tickets on iOS.")
89 .font(.subheadline)
90 .foregroundStyle(.secondary)
91 .themedRow()
92 }
93
94 Section {
95 if storeViewModel.isLoading {
96 ProgressView()
97 .themedRow()
98 } else if storeViewModel.products.isEmpty {
99 Text("Tips unavailable")
100 .foregroundStyle(.secondary)
101 .themedRow()
102 } else {
103 ForEach(storeViewModel.products, id: \.id) { product in
104 Button {
105 Task {
106 await storeViewModel.purchase(product)
107 }
108 } label: {
109 HStack {
110 Text(product.displayName)
111 Spacer()
112 Text(product.displayPrice)
113 .foregroundStyle(.secondary)
114 }
115 }
116 .themedRow()
117 }
118 }
119 } header: {
120 Text("Support Development")
121 } footer: {
122 Text("Tips help cover the costs of development and App Store distribution. They are one-time purchases and do not unlock any features.")
123 }
124
125 if developerToolsVisible {
126 Section {
127 Toggle("Debug Mode", isOn: Binding(
128 get: { appState.isDebugModeEnabled },
129 set: { appState.isDebugModeEnabled = $0 }
130 ))
131 .themedRow()
132 } header: {
133 Text("Developer")
134 } footer: {
135 Text("Shows raw API payloads and diagnostic details on builds and tickets screens. This stays hidden until explicitly enabled.")
136 }
137 }
138 }
139 .themedList()
140 .navigationTitle("About")
141 .navigationBarTitleDisplayMode(.inline)
142 .task {
143 await storeViewModel.loadProducts()
144 }
145 }
146}