Commit 0bb46ffe82

0bb46ffe82138f9e6c6bf51ec846740be05fe2d7

parent: 3078d39ae2

Unsigned

cmc <hello@cleberg.net> · 2026-04-14 15:42 UTC

fix: remove prototype screens from developer menu

- removed prototype screens
- limited Recent section to 3 items
- added initial support for tips

Fixes: https://todo.sr.ht/~ccleberg/hutch/65

Layout: unified · split

Hutch.xcodeproj/project.pbxproj +8 −8
@@ -515,7 +515,7 @@
515515 ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
516516 CODE_SIGN_ENTITLEMENTS = Hutch/Hutch.entitlements;
517517 CODE_SIGN_STYLE = Automatic;
518 CURRENT_PROJECT_VERSION = 73;
518 CURRENT_PROJECT_VERSION = 74;
519519 DEVELOPMENT_TEAM = ZCNAX3VL9D;
520520 ENABLE_PREVIEWS = YES;
521521 GENERATE_INFOPLIST_FILE = YES;
@@ -532,7 +532,7 @@
532532 "$(inherited)",
533533 "@executable_path/Frameworks",
534534 );
535 MARKETING_VERSION = 3.1.5;
535 MARKETING_VERSION = 3.1.6;
536536 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch;
537537 PRODUCT_NAME = "$(TARGET_NAME)";
538538 STRING_CATALOG_GENERATE_SYMBOLS = YES;
@@ -552,7 +552,7 @@
552552 ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
553553 CODE_SIGN_ENTITLEMENTS = Hutch/Hutch.entitlements;
554554 CODE_SIGN_STYLE = Automatic;
555 CURRENT_PROJECT_VERSION = 73;
555 CURRENT_PROJECT_VERSION = 74;
556556 DEVELOPMENT_TEAM = ZCNAX3VL9D;
557557 ENABLE_PREVIEWS = YES;
558558 GENERATE_INFOPLIST_FILE = YES;
@@ -569,7 +569,7 @@
569569 "$(inherited)",
570570 "@executable_path/Frameworks",
571571 );
572 MARKETING_VERSION = 3.1.5;
572 MARKETING_VERSION = 3.1.6;
573573 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch;
574574 PRODUCT_NAME = "$(TARGET_NAME)";
575575 STRING_CATALOG_GENERATE_SYMBOLS = YES;
@@ -632,7 +632,7 @@
632632 APPLICATION_EXTENSION_API_ONLY = YES;
633633 CODE_SIGN_ENTITLEMENTS = HutchWidgetExtension/HutchWidgetExtension.entitlements;
634634 CODE_SIGN_STYLE = Automatic;
635 CURRENT_PROJECT_VERSION = 73;
635 CURRENT_PROJECT_VERSION = 74;
636636 DEVELOPMENT_TEAM = ZCNAX3VL9D;
637637 GENERATE_INFOPLIST_FILE = NO;
638638 INFOPLIST_FILE = HutchWidgetExtension/Info.plist;
@@ -642,7 +642,7 @@
642642 "@executable_path/Frameworks",
643643 "@executable_path/../../Frameworks",
644644 );
645 MARKETING_VERSION = 3.1.5;
645 MARKETING_VERSION = 3.1.6;
646646 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchWidgetExtension;
647647 PRODUCT_NAME = "$(TARGET_NAME)";
648648 SKIP_INSTALL = YES;
@@ -661,7 +661,7 @@
661661 APPLICATION_EXTENSION_API_ONLY = YES;
662662 CODE_SIGN_ENTITLEMENTS = HutchWidgetExtension/HutchWidgetExtension.entitlements;
663663 CODE_SIGN_STYLE = Automatic;
664 CURRENT_PROJECT_VERSION = 73;
664 CURRENT_PROJECT_VERSION = 74;
665665 DEVELOPMENT_TEAM = ZCNAX3VL9D;
666666 GENERATE_INFOPLIST_FILE = NO;
667667 INFOPLIST_FILE = HutchWidgetExtension/Info.plist;
@@ -671,7 +671,7 @@
671671 "@executable_path/Frameworks",
672672 "@executable_path/../../Frameworks",
673673 );
674 MARKETING_VERSION = 3.1.5;
674 MARKETING_VERSION = 3.1.6;
675675 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchWidgetExtension;
676676 PRODUCT_NAME = "$(TARGET_NAME)";
677677 SKIP_INSTALL = YES;
Hutch/App/RootView.swift +3
@@ -336,6 +336,7 @@ enum MoreRoute: Hashable {
336336 case profile
337337 case systemStatus
338338 case settings
339 case about
339340 case mailingList(InboxMailingListReference)
340341 case thread(InboxThreadSummary)
341342 case manPageBrowser
@@ -363,6 +364,8 @@ private struct MoreNavigationRoot: View {
363364 SystemStatusView()
364365 case .settings:
365366 SettingsView()
367 case .about:
368 AboutView()
366369 case .mailingList(let mailingList):
367370 MailingListDetailView(mailingList: mailingList)
368371 case .thread(let thread):
Hutch/Views/Home/HomePrototypeView.swift deleted −7
@@ -1,7 +0,0 @@
1import SwiftUI
2
3struct HomePrototypeView: View {
4 var body: some View {
5 HomeView()
6 }
7}
Hutch/Views/Home/HomeView.swift +1 −1
@@ -118,7 +118,7 @@ struct HomeView: View {
118118 private var recentSection: some View {
119119 if !recentItems.isEmpty {
120120 Section("Recent") {
121 ForEach(recentItems) { item in
121 ForEach(recentItems.prefix(3)) { item in
122122 Button {
123123 openRecentItem(item)
124124 } label: {
Hutch/Views/Lookup/LookupView.swift +2
@@ -441,6 +441,8 @@ struct LookupView: View {
441441 SystemStatusView()
442442 case .settings:
443443 SettingsView()
444 case .about:
445 AboutView()
444446 case .mailingList(let mailingList):
445447 MailingListDetailView(mailingList: mailingList)
446448 case .thread(let thread):
Hutch/Views/More/AboutView.swift added +146
@@ -0,0 +1,146 @@
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}
Hutch/Views/More/MoreView.swift +5
@@ -61,6 +61,11 @@ struct MoreView: View {
6161 Label("Settings", systemImage: "gear")
6262 }
6363 .themedRow()
64
65 NavigationLink(value: MoreRoute.about) {
66 Label("About Hutch", systemImage: "info.circle")
67 }
68 .themedRow()
6469 }
6570
6671 Section {
Hutch/Views/More/TipStoreViewModel.swift added +44
@@ -0,0 +1,44 @@
1import StoreKit
2
3@Observable
4final class TipStoreViewModel {
5 var products: [Product] = []
6 var isLoading = false
7 var errorMessage: String?
8
9 private let productIDs = [
10 "net.cleberg.hutch.tip.small",
11 "net.cleberg.hutch.tip.medium",
12 "net.cleberg.hutch.tip.large"
13 ]
14
15 func loadProducts() async {
16 isLoading = true
17 defer { isLoading = false }
18 do {
19 let fetched = try await Product.products(for: productIDs)
20 // Sort by price ascending to maintain small/medium/large order
21 products = fetched.sorted { $0.price < $1.price }
22 } catch {
23 errorMessage = error.localizedDescription
24 }
25 }
26
27 func purchase(_ product: Product) async {
28 do {
29 let result = try await product.purchase()
30 switch result {
31 case .success(let verification):
32 if case .verified(let transaction) = verification {
33 await transaction.finish()
34 }
35 case .userCancelled, .pending:
36 break
37 @unknown default:
38 break
39 }
40 } catch {
41 errorMessage = error.localizedDescription
42 }
43 }
44}
Hutch/Views/Settings/SettingsView.swift −134
@@ -14,7 +14,6 @@ struct SettingsView: View {
1414 appearanceSection()
1515 behaviorSection()
1616 authenticationSection()
17 aboutSection()
1817 }
1918 .themedList()
2019 .navigationTitle("Settings")
@@ -142,17 +141,6 @@ struct SettingsView: View {
142141 }
143142 }
144143
145 @ViewBuilder
146 private func aboutSection() -> some View {
147 Section("App") {
148 NavigationLink {
149 AboutView()
150 } label: {
151 SwiftUI.Label("About Hutch", systemImage: "info.circle")
152 }
153 .themedRow()
154 }
155 }
156144}
157145
158146func settingsBioAttributedString(_ markdown: String) -> AttributedString {
@@ -191,125 +179,3 @@ private enum SettingsDestructiveAction {
191179 }
192180}
193181
194private struct AboutView: View {
195 @Environment(AppState.self) private var appState
196 private let appName = Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String
197 ?? Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String
198 ?? "Hutch"
199 private let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String
200 ?? "Unknown"
201 private let build = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String
202 ?? "Unknown"
203 @State private var developerRevealCount = 0
204
205 private var developerToolsVisible: Bool {
206 appState.isDebugModeEnabled || developerRevealCount >= 5
207 }
208
209 private var developerRevealFooterText: String {
210 developerRevealCount >= 5
211 ? "Debug toggle unlocked. Scroll down to Developer to enable it."
212 : "Tap the build number 5 times to reveal the debug toggle."
213 }
214
215 var body: some View {
216 Form {
217 Section {
218 VStack(alignment: .leading, spacing: 6) {
219 Text(appName)
220 .font(.title2.weight(.semibold))
221 Text("A native SourceHut client for iOS.")
222 .font(.subheadline)
223 .foregroundStyle(.secondary)
224 }
225 .padding(.vertical, 4)
226 .themedRow()
227
228 LabeledContent("Version", value: version)
229 .onTapGesture {
230 developerRevealCount = min(developerRevealCount + 1, 5)
231 }
232 .themedRow()
233 LabeledContent("Build", value: build)
234 .onTapGesture {
235 developerRevealCount = min(developerRevealCount + 1, 5)
236 }
237 .themedRow()
238 } footer: {
239 Text(developerRevealFooterText)
240 }
241
242 Section("Links") {
243 Link(destination: URL(string: "https://sr.ht")!) {
244 SwiftUI.Label("SourceHut", systemImage: "link")
245 }
246 .themedRow()
247 Link(destination: URL(string: "https://man.sr.ht")!) {
248 SwiftUI.Label("SourceHut Manuals", systemImage: "book")
249 }
250 .themedRow()
251 Link(destination: URL(string: "https://sr.ht/~ccleberg/Hutch")!) {
252 SwiftUI.Label("Project Repository", systemImage: "folder")
253 }
254 .themedRow()
255 }
256
257 Section("Support") {
258 Link(destination: URL(string: "mailto:hello@cleberg.net")!) {
259 SwiftUI.Label("Email Support", systemImage: "envelope")
260 }
261 .themedRow()
262 }
263
264 Section("Privacy") {
265 Text("Hutch uses your SourceHut personal access token to make requests on your behalf. The token is stored locally in the iOS keychain.")
266 .font(.subheadline)
267 .foregroundStyle(.secondary)
268 .themedRow()
269
270 Link(destination: URL(string: "https://zerolabs.sh/hutch/privacy-policy/")!) {
271 SwiftUI.Label("Privacy Policy", systemImage: "hand.raised")
272 }
273 .themedRow()
274 }
275
276 Section("Acknowledgements") {
277 Text("Built for SourceHut users who want quick access to repositories, builds, and tickets on iOS.")
278 .font(.subheadline)
279 .foregroundStyle(.secondary)
280 .themedRow()
281 }
282
283 if developerToolsVisible {
284 Section {
285 Toggle("Debug Mode", isOn: Binding(
286 get: { appState.isDebugModeEnabled },
287 set: { appState.isDebugModeEnabled = $0 }
288 ))
289 .themedRow()
290
291 NavigationLink {
292 HomePrototypeView()
293 } label: {
294 SwiftUI.Label("Home Prototype", systemImage: "house")
295 }
296 .themedRow()
297
298 NavigationLink {
299 WorkPrototypeView()
300 } label: {
301 SwiftUI.Label("Work Prototype", systemImage: "tray.full")
302 }
303 .themedRow()
304 } header: {
305 Text("Developer")
306 } footer: {
307 Text("Shows raw API payloads and diagnostic details on builds and tickets screens. Home Prototype explores the dashboard structure, while Work Prototype evaluates the personal queue surface. This stays hidden until explicitly enabled.")
308 }
309 }
310 }
311 .themedList()
312 .navigationTitle("About")
313 .navigationBarTitleDisplayMode(.inline)
314 }
315}
Hutch/Views/Work/WorkPrototypeView.swift deleted −7
@@ -1,7 +0,0 @@
1import SwiftUI
2
3struct WorkPrototypeView: View {
4 var body: some View {
5 WorkView()
6 }
7}