Settings: move test notifications into hidden Debug section + dev flag

- Test-alert buttons moved out of the Alerts section into a Debug section.
- Debug section only renders when the debug flag is on; completely hidden
  otherwise (no user-facing toggle).
- Hidden toggle: tap Settings → About → Version five times to flip the
  flag (keychain + app-group storage, survives reinstall).
- About section added with version/build number.
- 35 tests pass.
This commit is contained in:
FuelBoard Contributor
2026-08-12 14:10:07 +01:00
parent 5246f41c48
commit 07521b1c98
2 changed files with 94 additions and 28 deletions
+54 -5
View File
@@ -3,10 +3,13 @@ import StoreKit
import UserNotifications
import WidgetKit
/// Settings tab distance units, onboarding replay, a tip jar, and
/// test-alert buttons. The first test button uses REAL data (the actual
/// cheapest station for the monitored fuel within the radius, exactly like a
/// live alert) via `onTestAlert`; the second fires with no criteria at all.
/// Settings tab distance units, onboarding replay, a tip jar, and an About
/// section. Test-notification buttons live in a Debug section that is hidden
/// unless the developer debug flag is on (tap the About Version row five
/// times to toggle it no user-facing switch). The first test button uses
/// REAL data (the actual cheapest station for the monitored fuel within the
/// radius, exactly like a live alert) via `onTestAlert`; the second fires
/// with no criteria at all.
struct SettingsView: View {
@Binding var distanceUnit: DistanceUnit
/// The fuel + radius currently configured for alerts (mirrors the Alerts
@@ -27,6 +30,11 @@ struct SettingsView: View {
@State private var showTipAlert = false
@State private var tipAlertMessage = ""
@State private var testAlertMessage: String?
/// Hidden developer flag the Debug section only appears when on. Toggled
/// by tapping the About Version row five times (NOT a user-facing switch).
@State private var debugMode: Bool = FuelStore.loadDebugMode()
@State private var versionTapCount = 0
@State private var lastVersionTap = Date.distantPast
var body: some View {
NavigationStack {
@@ -58,6 +66,7 @@ struct SettingsView: View {
Text("Replay the welcome screen, including the location and notification permission prompts.")
}
if debugMode {
Section {
Button {
sendTestAlert()
@@ -80,10 +89,11 @@ struct SettingsView: View {
Label("Send plain test notification", systemImage: "bell.slash.fill")
}
} header: {
Text("Alerts")
Text("Debug")
} footer: {
Text("The first button applies the real criteria — cheapest \(alertsFuel.displayName.lowercased()) station within \(distanceUnit.format(alertsRadiusKM)) of you — and sends the actual station's name, price and coordinates. The second fires with no criteria at all (nearest seller), still carrying a real station, just to verify a notification appears and tapping it opens directions.")
}
}
Section {
Button {
@@ -105,6 +115,22 @@ struct SettingsView: View {
Text("A small tip helps keep the data relay and app development going. Thank you!")
}
Section {
HStack {
Text("Version")
Spacer()
Text(appVersion)
.foregroundStyle(.secondary)
.monospacedDigit()
.contentShape(Rectangle())
.onTapGesture { handleVersionTap() }
}
} header: {
Text("About")
} footer: {
Text("FuelBoard \(appVersion)")
}
if let message = tipStore.message {
Section {
Text(message)
@@ -120,6 +146,29 @@ struct SettingsView: View {
}
}
private var appVersion: String {
let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "1.0"
let build = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? "1"
return "\(version) (\(build))"
}
/// Hidden debug-mode toggle: five taps on the Version row flips the flag.
/// Nothing in the UI advertises this the Debug section simply appears or
/// disappears. Deliberately NOT a visible switch so end users never see it.
private func handleVersionTap() {
let now = Date()
if now.timeIntervalSince(lastVersionTap) > 0.6 {
versionTapCount = 0
}
lastVersionTap = now
versionTapCount += 1
if versionTapCount >= 5 {
versionTapCount = 0
debugMode.toggle()
FuelStore.saveDebugMode(debugMode)
}
}
private func sendTestAlert() {
UNUserNotificationCenter.current().getNotificationSettings { settings in
Task { @MainActor in
+17
View File
@@ -261,6 +261,7 @@ struct FuelStore {
static let alertsEnabledKey = "fuelboard.alertsEnabled" // Bool
static let alertsRadiusKey = "fuelboard.alertsRadius" // Double km
static let alertsFuelKey = "fuelboard.alertsFuel" // FuelType raw value
static let debugModeKey = "fuelboard.debugMode" // Bool hidden dev flag
static let onboardingCompletedKey = "fuelboard.onboardingCompleted" // Bool
static let lastRefreshKey = "fuelboard.lastRefresh" // TimeInterval (seconds since 1970)
@@ -401,6 +402,22 @@ struct FuelStore {
saveString(unit.rawValue, service: distanceUnitKey)
}
// MARK: Debug mode
/// Hidden developer flag. NOT exposed in the UI: toggled by tapping the
/// Settings About Version row five times. When on, the Settings tab
/// shows the Debug section (test notification buttons); when off, that
/// section is completely hidden from the UI. Stored keychain-first like
/// every other small setting so it survives app deletion during the
/// delete reinstall test loop.
static func loadDebugMode() -> Bool {
loadString(service: debugModeKey) == "1"
}
static func saveDebugMode(_ enabled: Bool) {
saveString(enabled ? "1" : "0", service: debugModeKey)
}
// MARK: Favourites
/// A favourite pins a station FOR ONE fuel type. Starring a row while