krz/hutch

an ios client for sourcehut

clone: git clone https://gitbay.org/krz/hutch.git

v3.1.6: 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}