From d6550409df151043ec2cf8f6a2d03dce8e706d67 Mon Sep 17 00:00:00 2001 From: FuelBoard Contributor Date: Wed, 12 Aug 2026 09:15:04 +0100 Subject: [PATCH] Settings: test alert notification button Fires a local notification shaped exactly like a real price alert (uses the currently configured alerts fuel + radius), with permission-gated handling: prompts on first use, guides to Settings when denied. Tapping it opens FuelBoard, same as a real alert. --- FuelBoard/ContentView.swift | 2 ++ FuelBoard/ProximityMonitor.swift | 15 +++++++++ FuelBoard/SettingsView.swift | 52 +++++++++++++++++++++++++++++++- 3 files changed, 68 insertions(+), 1 deletion(-) diff --git a/FuelBoard/ContentView.swift b/FuelBoard/ContentView.swift index f50a042..60ffb89 100644 --- a/FuelBoard/ContentView.swift +++ b/FuelBoard/ContentView.swift @@ -146,6 +146,8 @@ struct ContentView: View { SettingsView( distanceUnit: $distanceUnit, + alertsFuel: alertsFuel, + alertsRadiusKM: alertsRadius, onShowOnboarding: { showOnboarding = true } ) .tabItem { Label("Settings", systemImage: "gearshape.fill") } diff --git a/FuelBoard/ProximityMonitor.swift b/FuelBoard/ProximityMonitor.swift index 4c3bf61..99599bf 100644 --- a/FuelBoard/ProximityMonitor.swift +++ b/FuelBoard/ProximityMonitor.swift @@ -172,4 +172,19 @@ final class ProximityMonitor: NSObject, ObservableObject, @preconcurrency CLLoca lastAlert = "\(brand) · \(String(format: "%.1fp", price)) · \(unit.format(radiusKM)) radius" } + + /// Fires a sample local notification shaped exactly like a real price + /// alert, for the Settings "test" button. Uses a placeholder station so + /// the user can see the alert format without a real entry event. Tapping + /// it does what tapping any FuelBoard alert does — opens the app. + static func sendTestNotification(fuel: FuelType, radiusKM: Double) { + let unit = FuelStore.loadDistanceUnit() + let content = UNMutableNotificationContent() + content.title = "Cheapest \(fuel.displayName) nearby: Sample Filling Station" + content.body = "Sample Filling Station is the cheapest within \(unit.format(radiusKM)) at 132.9p. Tap to open." + content.sound = .default + + let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil) + UNUserNotificationCenter.current().add(request) + } } diff --git a/FuelBoard/SettingsView.swift b/FuelBoard/SettingsView.swift index 2e641d7..6457c4f 100644 --- a/FuelBoard/SettingsView.swift +++ b/FuelBoard/SettingsView.swift @@ -1,15 +1,22 @@ import SwiftUI import StoreKit +import UserNotifications import WidgetKit -/// Settings tab — distance units, onboarding replay, and a tip jar. +/// Settings tab — distance units, onboarding replay, a tip jar, and a +/// test-alert button that previews what a real price notification looks like. struct SettingsView: View { @Binding var distanceUnit: DistanceUnit + /// The fuel + radius currently configured for alerts (mirrors the Alerts + /// tab) so the test notification matches what real alerts will say. + var alertsFuel: FuelType = .e10 + var alertsRadiusKM: Double = 3.0 var onShowOnboarding: () -> Void = {} @StateObject private var tipStore = TipStore() @State private var showTipAlert = false @State private var tipAlertMessage = "" + @State private var testAlertMessage: String? var body: some View { NavigationStack { @@ -41,6 +48,23 @@ struct SettingsView: View { Text("Replay the welcome screen, including the location and notification permission prompts.") } + Section { + Button { + sendTestAlert() + } label: { + Label("Test alert notification", systemImage: "bell.badge.fill") + } + if let testAlertMessage { + Text(testAlertMessage) + .font(.footnote) + .foregroundStyle(.secondary) + } + } header: { + Text("Alerts") + } footer: { + Text("Sends a notification exactly like a real price alert for \(alertsFuel.displayName.lowercased()) within \(distanceUnit.format(alertsRadiusKM)). Tapping it opens FuelBoard — just like a real alert.") + } + Section { Button { Task { await tipStore.purchase() } @@ -75,6 +99,32 @@ struct SettingsView: View { } } } + + private func sendTestAlert() { + UNUserNotificationCenter.current().getNotificationSettings { settings in + Task { @MainActor in + switch settings.authorizationStatus { + case .authorized, .provisional, .ephemeral: + ProximityMonitor.sendTestNotification(fuel: alertsFuel, radiusKM: alertsRadiusKM) + testAlertMessage = "Test alert sent — check your notification centre." + case .denied: + testAlertMessage = "Notifications are turned off for FuelBoard. Enable them in Settings → Notifications → FuelBoard, then try again." + default: + // First time — ask, then fire if granted. + UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, _ in + Task { @MainActor in + if granted { + ProximityMonitor.sendTestNotification(fuel: alertsFuel, radiusKM: alertsRadiusKM) + testAlertMessage = "Test alert sent — check your notification centre." + } else { + testAlertMessage = "Notifications weren't allowed, so no test alert was sent." + } + } + } + } + } + } + } } /// Loads the £4.99 tip product and drives its purchase.