Test alert uses real data: actual cheapest station within radius
The first Settings test button no longer sends a hardcoded placeholder — it runs the same criteria as a live alert (monitored fuel, alert radius, current location) against the loaded stations and sends the REAL station name, brand and price. Result line shown under the button; graceful message when no stations or no sellers within radius.
This commit is contained in:
@@ -148,6 +148,8 @@ struct ContentView: View {
|
|||||||
distanceUnit: $distanceUnit,
|
distanceUnit: $distanceUnit,
|
||||||
alertsFuel: alertsFuel,
|
alertsFuel: alertsFuel,
|
||||||
alertsRadiusKM: alertsRadius,
|
alertsRadiusKM: alertsRadius,
|
||||||
|
testAlertResult: monitor.lastTestResult,
|
||||||
|
onTestAlert: { monitor.sendTestNotification() },
|
||||||
onShowOnboarding: { showOnboarding = true }
|
onShowOnboarding: { showOnboarding = true }
|
||||||
)
|
)
|
||||||
.tabItem { Label("Settings", systemImage: "gearshape.fill") }
|
.tabItem { Label("Settings", systemImage: "gearshape.fill") }
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import SwiftUI
|
|||||||
final class ProximityMonitor: NSObject, ObservableObject, @preconcurrency CLLocationManagerDelegate, UNUserNotificationCenterDelegate {
|
final class ProximityMonitor: NSObject, ObservableObject, @preconcurrency CLLocationManagerDelegate, UNUserNotificationCenterDelegate {
|
||||||
@Published var monitoredStationIDs: [String] = []
|
@Published var monitoredStationIDs: [String] = []
|
||||||
@Published var lastAlert: String?
|
@Published var lastAlert: String?
|
||||||
|
@Published private(set) var lastTestResult: String?
|
||||||
|
|
||||||
private let manager = CLLocationManager()
|
private let manager = CLLocationManager()
|
||||||
private var stations: [FuelStation] = []
|
private var stations: [FuelStation] = []
|
||||||
@@ -176,19 +177,49 @@ final class ProximityMonitor: NSObject, ObservableObject, @preconcurrency CLLoca
|
|||||||
lastAlert = "\(brand) · \(String(format: "%.1fp", price)) · \(unit.format(radiusKM)) radius"
|
lastAlert = "\(brand) · \(String(format: "%.1fp", price)) · \(unit.format(radiusKM)) radius"
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fires a sample local notification shaped exactly like a real price
|
/// Fires a test alert built from REAL data: the cheapest station selling
|
||||||
/// alert, for the Settings "test" button. Uses a placeholder station so
|
/// the monitored fuel within the configured alert radius of the current
|
||||||
/// the user can see the alert format without a real entry event. Tapping
|
/// location — the same criteria a live geofence entry would apply, minus
|
||||||
/// it does what tapping any FuelBoard alert does — opens the app.
|
/// the geofence itself. Uses the monitor's live state (fuel, radius,
|
||||||
static func sendTestNotification(fuel: FuelType, radiusKM: Double) {
|
/// stations) so the notification reflects the Alerts-tab configuration.
|
||||||
|
func sendTestNotification() {
|
||||||
let unit = FuelStore.loadDistanceUnit()
|
let unit = FuelStore.loadDistanceUnit()
|
||||||
let content = UNMutableNotificationContent()
|
let location = FuelStore.loadLocation()
|
||||||
content.title = "Cheapest \(fuel.displayName) nearby: Sample Filling Station"
|
let sellers = stations.filter { $0.prices[fuel] != nil }
|
||||||
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)
|
// Cheapest within radius, judged around the current location —
|
||||||
UNUserNotificationCenter.current().add(request)
|
// identical to handleEntry's withinRadius logic.
|
||||||
|
let inRadius = sellers.filter { station in
|
||||||
|
guard let location else { return true }
|
||||||
|
return station.distanceKM(to: location.lat, lng2: location.lng) <= radiusKM
|
||||||
|
}
|
||||||
|
let candidate = inRadius.min { ($0.prices[fuel] ?? .infinity) < ($1.prices[fuel] ?? .infinity) }
|
||||||
|
|
||||||
|
if let candidate, let price = candidate.prices[fuel] {
|
||||||
|
let brand = candidate.brand.isEmpty ? candidate.name : candidate.brand
|
||||||
|
let content = UNMutableNotificationContent()
|
||||||
|
content.title = "Cheapest \(fuel.displayName) nearby: \(brand)"
|
||||||
|
content.body = "\(candidate.name) is the cheapest within \(unit.format(radiusKM)) at \(String(format: "%.1fp", price)). Tap to open."
|
||||||
|
content.sound = .default
|
||||||
|
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
|
||||||
|
UNUserNotificationCenter.current().add(request)
|
||||||
|
lastTestResult = "\(fuel.displayName) · \(brand) · \(String(format: "%.1fp", price)) · radius \(unit.format(radiusKM))"
|
||||||
|
} else {
|
||||||
|
// No real candidate (no stations loaded yet, or none sell the
|
||||||
|
// monitored fuel) — still fire so delivery is testable, but say
|
||||||
|
// why the real criteria found nothing.
|
||||||
|
let content = UNMutableNotificationContent()
|
||||||
|
content.title = "FuelBoard test alert"
|
||||||
|
content.body = sellers.isEmpty
|
||||||
|
? "No station data loaded yet — open the Stations tab first, then retry."
|
||||||
|
: "No \(fuel.displayName.lowercased()) station within \(unit.format(radiusKM)) of you."
|
||||||
|
content.sound = .default
|
||||||
|
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
|
||||||
|
UNUserNotificationCenter.current().add(request)
|
||||||
|
lastTestResult = sellers.isEmpty
|
||||||
|
? "No stations loaded — open the Stations tab first"
|
||||||
|
: "No \(fuel.displayName.lowercased()) seller within \(unit.format(radiusKM))"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fires a notification with NO criteria checks at all — no geofence, no
|
/// Fires a notification with NO criteria checks at all — no geofence, no
|
||||||
|
|||||||
@@ -3,14 +3,21 @@ import StoreKit
|
|||||||
import UserNotifications
|
import UserNotifications
|
||||||
import WidgetKit
|
import WidgetKit
|
||||||
|
|
||||||
/// Settings tab — distance units, onboarding replay, a tip jar, and a
|
/// Settings tab — distance units, onboarding replay, a tip jar, and
|
||||||
/// test-alert button that previews what a real price notification looks like.
|
/// 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.
|
||||||
struct SettingsView: View {
|
struct SettingsView: View {
|
||||||
@Binding var distanceUnit: DistanceUnit
|
@Binding var distanceUnit: DistanceUnit
|
||||||
/// The fuel + radius currently configured for alerts (mirrors the Alerts
|
/// The fuel + radius currently configured for alerts (mirrors the Alerts
|
||||||
/// tab) so the test notification matches what real alerts will say.
|
/// tab) so the test notification matches what real alerts will say.
|
||||||
var alertsFuel: FuelType = .e10
|
var alertsFuel: FuelType = .e10
|
||||||
var alertsRadiusKM: Double = 3.0
|
var alertsRadiusKM: Double = 3.0
|
||||||
|
/// Result line from the last real-data test alert (what it picked).
|
||||||
|
var testAlertResult: String?
|
||||||
|
/// Drives the real-data test — ContentView routes this to the live
|
||||||
|
/// ProximityMonitor so the test uses its actual stations/fuel/radius.
|
||||||
|
var onTestAlert: () -> Void = {}
|
||||||
var onShowOnboarding: () -> Void = {}
|
var onShowOnboarding: () -> Void = {}
|
||||||
|
|
||||||
@StateObject private var tipStore = TipStore()
|
@StateObject private var tipStore = TipStore()
|
||||||
@@ -52,13 +59,18 @@ struct SettingsView: View {
|
|||||||
Button {
|
Button {
|
||||||
sendTestAlert()
|
sendTestAlert()
|
||||||
} label: {
|
} label: {
|
||||||
Label("Test alert notification", systemImage: "bell.badge.fill")
|
Label("Test alert notification (real data)", systemImage: "bell.badge.fill")
|
||||||
}
|
}
|
||||||
if let testAlertMessage {
|
if let testAlertMessage {
|
||||||
Text(testAlertMessage)
|
Text(testAlertMessage)
|
||||||
.font(.footnote)
|
.font(.footnote)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
|
if let testAlertResult {
|
||||||
|
Label(testAlertResult, systemImage: "checkmark.circle.fill")
|
||||||
|
.font(.footnote)
|
||||||
|
.foregroundStyle(.green)
|
||||||
|
}
|
||||||
Button {
|
Button {
|
||||||
ProximityMonitor.sendPlainTestNotification()
|
ProximityMonitor.sendPlainTestNotification()
|
||||||
} label: {
|
} label: {
|
||||||
@@ -67,7 +79,7 @@ struct SettingsView: View {
|
|||||||
} header: {
|
} header: {
|
||||||
Text("Alerts")
|
Text("Alerts")
|
||||||
} footer: {
|
} footer: {
|
||||||
Text("The first button sends a notification shaped like a real price alert for \(alertsFuel.displayName.lowercased()) within \(distanceUnit.format(alertsRadiusKM)). The second fires with no criteria at all — no geofence, no cheapest check — just to verify a notification appears and tapping it opens FuelBoard.")
|
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 and price. The second fires with no criteria at all, just to verify a notification appears and tapping it opens FuelBoard.")
|
||||||
}
|
}
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
@@ -110,8 +122,7 @@ struct SettingsView: View {
|
|||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
switch settings.authorizationStatus {
|
switch settings.authorizationStatus {
|
||||||
case .authorized, .provisional, .ephemeral:
|
case .authorized, .provisional, .ephemeral:
|
||||||
ProximityMonitor.sendTestNotification(fuel: alertsFuel, radiusKM: alertsRadiusKM)
|
onTestAlert()
|
||||||
testAlertMessage = "Test alert sent — check your notification centre."
|
|
||||||
case .denied:
|
case .denied:
|
||||||
testAlertMessage = "Notifications are turned off for FuelBoard. Enable them in Settings → Notifications → FuelBoard, then try again."
|
testAlertMessage = "Notifications are turned off for FuelBoard. Enable them in Settings → Notifications → FuelBoard, then try again."
|
||||||
default:
|
default:
|
||||||
@@ -119,8 +130,7 @@ struct SettingsView: View {
|
|||||||
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, _ in
|
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, _ in
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
if granted {
|
if granted {
|
||||||
ProximityMonitor.sendTestNotification(fuel: alertsFuel, radiusKM: alertsRadiusKM)
|
onTestAlert()
|
||||||
testAlertMessage = "Test alert sent — check your notification centre."
|
|
||||||
} else {
|
} else {
|
||||||
testAlertMessage = "Notifications weren't allowed, so no test alert was sent."
|
testAlertMessage = "Notifications weren't allowed, so no test alert was sent."
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user