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
+77 -28
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,31 +66,33 @@ struct SettingsView: View {
Text("Replay the welcome screen, including the location and notification permission prompts.")
}
Section {
Button {
sendTestAlert()
} label: {
Label("Test alert notification (real data)", systemImage: "bell.badge.fill")
if debugMode {
Section {
Button {
sendTestAlert()
} label: {
Label("Test alert notification (real data)", systemImage: "bell.badge.fill")
}
if let testAlertMessage {
Text(testAlertMessage)
.font(.footnote)
.foregroundStyle(.secondary)
}
if let testAlertResult {
Label(testAlertResult, systemImage: "checkmark.circle.fill")
.font(.footnote)
.foregroundStyle(.green)
}
Button {
onPlainTestAlert()
} label: {
Label("Send plain test notification", systemImage: "bell.slash.fill")
}
} header: {
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.")
}
if let testAlertMessage {
Text(testAlertMessage)
.font(.footnote)
.foregroundStyle(.secondary)
}
if let testAlertResult {
Label(testAlertResult, systemImage: "checkmark.circle.fill")
.font(.footnote)
.foregroundStyle(.green)
}
Button {
onPlainTestAlert()
} label: {
Label("Send plain test notification", systemImage: "bell.slash.fill")
}
} header: {
Text("Alerts")
} 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 {
@@ -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