import SwiftUI import StoreKit import WidgetKit /// Settings tab — distance units, onboarding replay, and a tip jar. struct SettingsView: View { @Binding var distanceUnit: DistanceUnit var onShowOnboarding: () -> Void = {} @StateObject private var tipStore = TipStore() @State private var showTipAlert = false @State private var tipAlertMessage = "" var body: some View { NavigationStack { List { Section { Picker("Distance", selection: $distanceUnit) { ForEach(DistanceUnit.allCases) { unit in Text(unit.displayName).tag(unit) } } .pickerStyle(.segmented) .onChange(of: distanceUnit) { _, newValue in FuelStore.saveDistanceUnit(newValue) WidgetCenter.shared.reloadAllTimelines() } } header: { Text("Units") } footer: { Text("Distances and search radii across the app, widget and alerts are shown in this unit.") } Section { Button { onShowOnboarding() } label: { Label("Show introduction", systemImage: "sparkles") } } footer: { Text("Replay the welcome screen, including the location and notification permission prompts.") } Section { Button { Task { await tipStore.purchase() } } label: { HStack { Label("Leave a tip", systemImage: "heart.fill") .foregroundStyle(.pink) Spacer() Text(tipStore.displayPrice) .foregroundStyle(.secondary) .monospacedDigit() } } .disabled(tipStore.purchaseInProgress) } header: { Text("Support FuelBoard") } footer: { Text("A small tip helps keep the data relay and app development going. Thank you!") } if let message = tipStore.message { Section { Text(message) .font(.footnote) .foregroundStyle(.secondary) } } } .navigationTitle("Settings") .onAppear { Task { await tipStore.load() } } } } } /// Loads the £4.99 tip product and drives its purchase. @MainActor final class TipStore: ObservableObject { /// Product ID for the £4.99 tip (App Store Connect — consumable). static let productID = "com.apt.fuelboard.tip499" @Published private(set) var product: Product? @Published private(set) var purchaseInProgress = false @Published private(set) var message: String? var displayPrice: String { product?.displayPrice ?? "£4.99" } func load() async { // Refreshes product state on every visit so a newly-approved product // (or a restored transaction) is picked up. do { let products = try await Product.products(for: [Self.productID]) product = products.first } catch { // No product yet (sideloaded build) — the button still shows the // intended price and reports the purchase attempt gracefully. product = nil } } func purchase() async { guard !purchaseInProgress else { return } purchaseInProgress = true defer { purchaseInProgress = false } // If the product hasn't loaded (e.g. not configured in App Store // Connect yet), still allow the attempt so the user sees a clear // outcome rather than a dead button. guard let product else { message = "The tip isn't available in this build yet — check back after an App Store release." return } do { let result = try await product.purchase() switch result { case .success(let verification): switch verification { case .verified: message = "Thank you! Your tip has been received. ⛽" case .unverified: message = "The purchase couldn't be verified. Please try again." } case .userCancelled: message = nil // silent — the user just closed the sheet case .pending: message = "Your tip is pending approval. It'll finish automatically." @unknown default: message = nil } } catch { message = "The tip couldn't be completed: \(error.localizedDescription)" } } }