summaryrefslogtreecommitdiff
path: root/Hutch/Views/More
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-04-14 10:42:05 -0500
committerChristian Cleberg <[email protected]>2026-04-14 10:42:05 -0500
commit205c8ea9b2c6dfa62bc8b2342d1aa71e2da7d6ac (patch)
treee7e9b8190ffc15d3cd3e696da4c8acf8c472a193 /Hutch/Views/More
parentd6968ea122d910b432bd539cf2fa3d1cee962fdb (diff)
downloadhutch-3.1.6.tar.gz
hutch-3.1.6.tar.bz2
hutch-3.1.6.zip
fix: remove prototype screens from developer menuv3.1.6
- removed prototype screens - limited Recent section to 3 items - added initial support for tips Fixes: https://todo.sr.ht/~ccleberg/hutch/65
Diffstat (limited to 'Hutch/Views/More')
-rw-r--r--Hutch/Views/More/AboutView.swift146
-rw-r--r--Hutch/Views/More/MoreView.swift5
-rw-r--r--Hutch/Views/More/TipStoreViewModel.swift44
3 files changed, 195 insertions, 0 deletions
diff --git a/Hutch/Views/More/AboutView.swift b/Hutch/Views/More/AboutView.swift
new file mode 100644
index 0000000..887bef1
--- /dev/null
+++ b/Hutch/Views/More/AboutView.swift
@@ -0,0 +1,146 @@
+import StoreKit
+import SwiftUI
+
+struct AboutView: View {
+ @Environment(AppState.self) private var appState
+ private let appName = Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String
+ ?? Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String
+ ?? "Hutch"
+ private let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String
+ ?? "Unknown"
+ private let build = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String
+ ?? "Unknown"
+ @State private var developerRevealCount = 0
+ @State private var storeViewModel = TipStoreViewModel()
+
+ private var developerToolsVisible: Bool {
+ appState.isDebugModeEnabled || developerRevealCount >= 5
+ }
+
+ private var developerRevealFooterText: String {
+ developerRevealCount >= 5
+ ? "Debug toggle unlocked. Scroll down to Developer to enable it."
+ : "Tap the build number 5 times to reveal the debug toggle."
+ }
+
+ var body: some View {
+ Form {
+ Section {
+ VStack(alignment: .leading, spacing: 6) {
+ Text(appName)
+ .font(.title2.weight(.semibold))
+ Text("A native SourceHut client for iOS.")
+ .font(.subheadline)
+ .foregroundStyle(.secondary)
+ }
+ .padding(.vertical, 4)
+ .themedRow()
+
+ LabeledContent("Version", value: version)
+ .onTapGesture {
+ developerRevealCount = min(developerRevealCount + 1, 5)
+ }
+ .themedRow()
+ LabeledContent("Build", value: build)
+ .onTapGesture {
+ developerRevealCount = min(developerRevealCount + 1, 5)
+ }
+ .themedRow()
+ } footer: {
+ Text(developerRevealFooterText)
+ }
+
+ Section("Links") {
+ Link(destination: URL(string: "https://sr.ht")!) {
+ SwiftUI.Label("SourceHut", systemImage: "link")
+ }
+ .themedRow()
+ Link(destination: URL(string: "https://man.sr.ht")!) {
+ SwiftUI.Label("SourceHut Manuals", systemImage: "book")
+ }
+ .themedRow()
+ Link(destination: URL(string: "https://sr.ht/~ccleberg/Hutch")!) {
+ SwiftUI.Label("Project Repository", systemImage: "folder")
+ }
+ .themedRow()
+ }
+
+ Section("Support") {
+ Link(destination: URL(string: "mailto:[email protected]")!) {
+ SwiftUI.Label("Email Support", systemImage: "envelope")
+ }
+ .themedRow()
+ }
+
+ Section("Privacy") {
+ Text("Hutch uses your SourceHut personal access token to make requests on your behalf. The token is stored locally in the iOS keychain.")
+ .font(.subheadline)
+ .foregroundStyle(.secondary)
+ .themedRow()
+
+ Link(destination: URL(string: "https://zerolabs.sh/hutch/privacy-policy/")!) {
+ SwiftUI.Label("Privacy Policy", systemImage: "hand.raised")
+ }
+ .themedRow()
+ }
+
+ Section("Acknowledgements") {
+ Text("Built for SourceHut users who want quick access to repositories, builds, and tickets on iOS.")
+ .font(.subheadline)
+ .foregroundStyle(.secondary)
+ .themedRow()
+ }
+
+ Section {
+ if storeViewModel.isLoading {
+ ProgressView()
+ .themedRow()
+ } else if storeViewModel.products.isEmpty {
+ Text("Tips unavailable")
+ .foregroundStyle(.secondary)
+ .themedRow()
+ } else {
+ ForEach(storeViewModel.products, id: \.id) { product in
+ Button {
+ Task {
+ await storeViewModel.purchase(product)
+ }
+ } label: {
+ HStack {
+ Text(product.displayName)
+ Spacer()
+ Text(product.displayPrice)
+ .foregroundStyle(.secondary)
+ }
+ }
+ .themedRow()
+ }
+ }
+ } header: {
+ Text("Support Development")
+ } footer: {
+ Text("Tips help cover the costs of development and App Store distribution. They are one-time purchases and do not unlock any features.")
+ }
+
+ if developerToolsVisible {
+ Section {
+ Toggle("Debug Mode", isOn: Binding(
+ get: { appState.isDebugModeEnabled },
+ set: { appState.isDebugModeEnabled = $0 }
+ ))
+ .themedRow()
+ } header: {
+ Text("Developer")
+ } footer: {
+ Text("Shows raw API payloads and diagnostic details on builds and tickets screens. This stays hidden until explicitly enabled.")
+ }
+ }
+ }
+ .themedList()
+ .navigationTitle("About")
+ .navigationBarTitleDisplayMode(.inline)
+ .task {
+ await storeViewModel.loadProducts()
+ }
+ }
+}
diff --git a/Hutch/Views/More/MoreView.swift b/Hutch/Views/More/MoreView.swift
index 4666e53..5c757ef 100644
--- a/Hutch/Views/More/MoreView.swift
+++ b/Hutch/Views/More/MoreView.swift
@@ -61,6 +61,11 @@ struct MoreView: View {
Label("Settings", systemImage: "gear")
}
.themedRow()
+
+ NavigationLink(value: MoreRoute.about) {
+ Label("About Hutch", systemImage: "info.circle")
+ }
+ .themedRow()
}
Section {
diff --git a/Hutch/Views/More/TipStoreViewModel.swift b/Hutch/Views/More/TipStoreViewModel.swift
new file mode 100644
index 0000000..54868ba
--- /dev/null
+++ b/Hutch/Views/More/TipStoreViewModel.swift
@@ -0,0 +1,44 @@
+import StoreKit
+
+@Observable
+final class TipStoreViewModel {
+ var products: [Product] = []
+ var isLoading = false
+ var errorMessage: String?
+
+ private let productIDs = [
+ "net.cleberg.hutch.tip.small",
+ "net.cleberg.hutch.tip.medium",
+ "net.cleberg.hutch.tip.large"
+ ]
+
+ func loadProducts() async {
+ isLoading = true
+ defer { isLoading = false }
+ do {
+ let fetched = try await Product.products(for: productIDs)
+ // Sort by price ascending to maintain small/medium/large order
+ products = fetched.sorted { $0.price < $1.price }
+ } catch {
+ errorMessage = error.localizedDescription
+ }
+ }
+
+ func purchase(_ product: Product) async {
+ do {
+ let result = try await product.purchase()
+ switch result {
+ case .success(let verification):
+ if case .verified(let transaction) = verification {
+ await transaction.finish()
+ }
+ case .userCancelled, .pending:
+ break
+ @unknown default:
+ break
+ }
+ } catch {
+ errorMessage = error.localizedDescription
+ }
+ }
+}