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 @@
515 ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 515 ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
516 CODE_SIGN_ENTITLEMENTS = Hutch/Hutch.entitlements; 516 CODE_SIGN_ENTITLEMENTS = Hutch/Hutch.entitlements;
517 CODE_SIGN_STYLE = Automatic; 517 CODE_SIGN_STYLE = Automatic;
518 CURRENT_PROJECT_VERSION = 73; 518 CURRENT_PROJECT_VERSION = 74;
519 DEVELOPMENT_TEAM = ZCNAX3VL9D; 519 DEVELOPMENT_TEAM = ZCNAX3VL9D;
520 ENABLE_PREVIEWS = YES; 520 ENABLE_PREVIEWS = YES;
521 GENERATE_INFOPLIST_FILE = YES; 521 GENERATE_INFOPLIST_FILE = YES;
@@ -532,7 +532,7 @@
532 "$(inherited)", 532 "$(inherited)",
533 "@executable_path/Frameworks", 533 "@executable_path/Frameworks",
534 ); 534 );
535 MARKETING_VERSION = 3.1.5; 535 MARKETING_VERSION = 3.1.6;
536 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch; 536 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch;
537 PRODUCT_NAME = "$(TARGET_NAME)"; 537 PRODUCT_NAME = "$(TARGET_NAME)";
538 STRING_CATALOG_GENERATE_SYMBOLS = YES; 538 STRING_CATALOG_GENERATE_SYMBOLS = YES;
@@ -552,7 +552,7 @@
552 ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 552 ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
553 CODE_SIGN_ENTITLEMENTS = Hutch/Hutch.entitlements; 553 CODE_SIGN_ENTITLEMENTS = Hutch/Hutch.entitlements;
554 CODE_SIGN_STYLE = Automatic; 554 CODE_SIGN_STYLE = Automatic;
555 CURRENT_PROJECT_VERSION = 73; 555 CURRENT_PROJECT_VERSION = 74;
556 DEVELOPMENT_TEAM = ZCNAX3VL9D; 556 DEVELOPMENT_TEAM = ZCNAX3VL9D;
557 ENABLE_PREVIEWS = YES; 557 ENABLE_PREVIEWS = YES;
558 GENERATE_INFOPLIST_FILE = YES; 558 GENERATE_INFOPLIST_FILE = YES;
@@ -569,7 +569,7 @@
569 "$(inherited)", 569 "$(inherited)",
570 "@executable_path/Frameworks", 570 "@executable_path/Frameworks",
571 ); 571 );
572 MARKETING_VERSION = 3.1.5; 572 MARKETING_VERSION = 3.1.6;
573 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch; 573 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch;
574 PRODUCT_NAME = "$(TARGET_NAME)"; 574 PRODUCT_NAME = "$(TARGET_NAME)";
575 STRING_CATALOG_GENERATE_SYMBOLS = YES; 575 STRING_CATALOG_GENERATE_SYMBOLS = YES;
@@ -632,7 +632,7 @@
632 APPLICATION_EXTENSION_API_ONLY = YES; 632 APPLICATION_EXTENSION_API_ONLY = YES;
633 CODE_SIGN_ENTITLEMENTS = HutchWidgetExtension/HutchWidgetExtension.entitlements; 633 CODE_SIGN_ENTITLEMENTS = HutchWidgetExtension/HutchWidgetExtension.entitlements;
634 CODE_SIGN_STYLE = Automatic; 634 CODE_SIGN_STYLE = Automatic;
635 CURRENT_PROJECT_VERSION = 73; 635 CURRENT_PROJECT_VERSION = 74;
636 DEVELOPMENT_TEAM = ZCNAX3VL9D; 636 DEVELOPMENT_TEAM = ZCNAX3VL9D;
637 GENERATE_INFOPLIST_FILE = NO; 637 GENERATE_INFOPLIST_FILE = NO;
638 INFOPLIST_FILE = HutchWidgetExtension/Info.plist; 638 INFOPLIST_FILE = HutchWidgetExtension/Info.plist;
@@ -642,7 +642,7 @@
642 "@executable_path/Frameworks", 642 "@executable_path/Frameworks",
643 "@executable_path/../../Frameworks", 643 "@executable_path/../../Frameworks",
644 ); 644 );
645 MARKETING_VERSION = 3.1.5; 645 MARKETING_VERSION = 3.1.6;
646 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchWidgetExtension; 646 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchWidgetExtension;
647 PRODUCT_NAME = "$(TARGET_NAME)"; 647 PRODUCT_NAME = "$(TARGET_NAME)";
648 SKIP_INSTALL = YES; 648 SKIP_INSTALL = YES;
@@ -661,7 +661,7 @@
661 APPLICATION_EXTENSION_API_ONLY = YES; 661 APPLICATION_EXTENSION_API_ONLY = YES;
662 CODE_SIGN_ENTITLEMENTS = HutchWidgetExtension/HutchWidgetExtension.entitlements; 662 CODE_SIGN_ENTITLEMENTS = HutchWidgetExtension/HutchWidgetExtension.entitlements;
663 CODE_SIGN_STYLE = Automatic; 663 CODE_SIGN_STYLE = Automatic;
664 CURRENT_PROJECT_VERSION = 73; 664 CURRENT_PROJECT_VERSION = 74;
665 DEVELOPMENT_TEAM = ZCNAX3VL9D; 665 DEVELOPMENT_TEAM = ZCNAX3VL9D;
666 GENERATE_INFOPLIST_FILE = NO; 666 GENERATE_INFOPLIST_FILE = NO;
667 INFOPLIST_FILE = HutchWidgetExtension/Info.plist; 667 INFOPLIST_FILE = HutchWidgetExtension/Info.plist;
@@ -671,7 +671,7 @@
671 "@executable_path/Frameworks", 671 "@executable_path/Frameworks",
672 "@executable_path/../../Frameworks", 672 "@executable_path/../../Frameworks",
673 ); 673 );
674 MARKETING_VERSION = 3.1.5; 674 MARKETING_VERSION = 3.1.6;
675 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchWidgetExtension; 675 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchWidgetExtension;
676 PRODUCT_NAME = "$(TARGET_NAME)"; 676 PRODUCT_NAME = "$(TARGET_NAME)";
677 SKIP_INSTALL = YES; 677 SKIP_INSTALL = YES;
Hutch/App/RootView.swift +3
@@ -336,6 +336,7 @@ enum MoreRoute: Hashable {
336 case profile 336 case profile
337 case systemStatus 337 case systemStatus
338 case settings 338 case settings
339 case about
339 case mailingList(InboxMailingListReference) 340 case mailingList(InboxMailingListReference)
340 case thread(InboxThreadSummary) 341 case thread(InboxThreadSummary)
341 case manPageBrowser 342 case manPageBrowser
@@ -363,6 +364,8 @@ private struct MoreNavigationRoot: View {
363 SystemStatusView() 364 SystemStatusView()
364 case .settings: 365 case .settings:
365 SettingsView() 366 SettingsView()
367 case .about:
368 AboutView()
366 case .mailingList(let mailingList): 369 case .mailingList(let mailingList):
367 MailingListDetailView(mailingList: mailingList) 370 MailingListDetailView(mailingList: mailingList)
368 case .thread(let thread): 371 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 {
118 private var recentSection: some View { 118 private var recentSection: some View {
119 if !recentItems.isEmpty { 119 if !recentItems.isEmpty {
120 Section("Recent") { 120 Section("Recent") {
121 ForEach(recentItems) { item in 121 ForEach(recentItems.prefix(3)) { item in
122 Button { 122 Button {
123 openRecentItem(item) 123 openRecentItem(item)
124 } label: { 124 } label: {
Hutch/Views/Lookup/LookupView.swift +2
@@ -441,6 +441,8 @@ struct LookupView: View {
441 SystemStatusView() 441 SystemStatusView()
442 case .settings: 442 case .settings:
443 SettingsView() 443 SettingsView()
444 case .about:
445 AboutView()
444 case .mailingList(let mailingList): 446 case .mailingList(let mailingList):
445 MailingListDetailView(mailingList: mailingList) 447 MailingListDetailView(mailingList: mailingList)
446 case .thread(let thread): 448 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 {
61 Label("Settings", systemImage: "gear") 61 Label("Settings", systemImage: "gear")
62 } 62 }
63 .themedRow() 63 .themedRow()
64
65 NavigationLink(value: MoreRoute.about) {
66 Label("About Hutch", systemImage: "info.circle")
67 }
68 .themedRow()
64 } 69 }
65 70
66 Section { 71 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 {
14 appearanceSection() 14 appearanceSection()
15 behaviorSection() 15 behaviorSection()
16 authenticationSection() 16 authenticationSection()
17 aboutSection()
18 } 17 }
19 .themedList() 18 .themedList()
20 .navigationTitle("Settings") 19 .navigationTitle("Settings")
@@ -142,17 +141,6 @@ struct SettingsView: View {
142 } 141 }
143 } 142 }
144 143
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 }
156} 144}
157 145
158func settingsBioAttributedString(_ markdown: String) -> AttributedString { 146func settingsBioAttributedString(_ markdown: String) -> AttributedString {
@@ -191,125 +179,3 @@ private enum SettingsDestructiveAction {
191 } 179 }
192} 180}
193 181
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}