Both test buttons carry real station coords; map status surfaced
- Plain test notification now picks the nearest station selling the monitored fuel and carries its real name, price, distance and coordinates (was a static no-station notification) - addAlertRequest reports whether the map snapshot attached or why it failed, appended to the Settings result line instead of silently delivering a notification without the embedded map - Settings routes both test buttons through the live monitor
This commit is contained in:
@@ -150,6 +150,7 @@ struct ContentView: View {
|
|||||||
alertsRadiusKM: alertsRadius,
|
alertsRadiusKM: alertsRadius,
|
||||||
testAlertResult: monitor.lastTestResult,
|
testAlertResult: monitor.lastTestResult,
|
||||||
onTestAlert: { monitor.sendTestNotification() },
|
onTestAlert: { monitor.sendTestNotification() },
|
||||||
|
onPlainTestAlert: { monitor.sendPlainTestNotification() },
|
||||||
onShowOnboarding: { showOnboarding = true }
|
onShowOnboarding: { showOnboarding = true }
|
||||||
)
|
)
|
||||||
.tabItem { Label("Settings", systemImage: "gearshape.fill") }
|
.tabItem { Label("Settings", systemImage: "gearshape.fill") }
|
||||||
|
|||||||
@@ -195,7 +195,13 @@ final class ProximityMonitor: NSObject, ObservableObject, @preconcurrency CLLoca
|
|||||||
/// station's coordinates ride in `userInfo` so the tap handler can route
|
/// station's coordinates ride in `userInfo` so the tap handler can route
|
||||||
/// to Apple Maps (or an in-app fallback), and a map snapshot with a pin is
|
/// to Apple Maps (or an in-app fallback), and a map snapshot with a pin is
|
||||||
/// attached so the expanded notification shows where the station is.
|
/// attached so the expanded notification shows where the station is.
|
||||||
private func addAlertRequest(content: UNMutableNotificationContent, station: FuelStation) {
|
/// `mapStatus` reports whether the snapshot attached (or why not), so the
|
||||||
|
/// test UI can show it instead of failing silently.
|
||||||
|
private func addAlertRequest(
|
||||||
|
content: UNMutableNotificationContent,
|
||||||
|
station: FuelStation,
|
||||||
|
mapStatus: ((String) -> Void)? = nil
|
||||||
|
) {
|
||||||
content.userInfo = [
|
content.userInfo = [
|
||||||
"stationName": station.name,
|
"stationName": station.name,
|
||||||
"stationLat": station.lat,
|
"stationLat": station.lat,
|
||||||
@@ -212,17 +218,20 @@ final class ProximityMonitor: NSObject, ObservableObject, @preconcurrency CLLoca
|
|||||||
snapshotter.start { [content] snapshot, error in
|
snapshotter.start { [content] snapshot, error in
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
let finalContent = content
|
let finalContent = content
|
||||||
|
var status = "map: no image"
|
||||||
if let snapshot, error == nil,
|
if let snapshot, error == nil,
|
||||||
let image = Self.mapImage(snapshot: snapshot, station: station) {
|
let image = Self.mapImage(snapshot: snapshot, station: station) {
|
||||||
let url = FileManager.default.temporaryDirectory
|
let url = FileManager.default.temporaryDirectory
|
||||||
.appendingPathComponent("alert-map-\(UUID().uuidString).png")
|
.appendingPathComponent("alert-map-\(UUID().uuidString).png")
|
||||||
if let data = image.pngData() {
|
if let data = image.pngData(), (try? data.write(to: url)) != nil,
|
||||||
try? data.write(to: url)
|
let attachment = try? UNNotificationAttachment(identifier: "map", url: url, options: nil) {
|
||||||
if let attachment = try? UNNotificationAttachment(identifier: "map", url: url, options: nil) {
|
finalContent.attachments = [attachment]
|
||||||
finalContent.attachments = [attachment]
|
status = "map: attached"
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
} else if let error {
|
||||||
|
status = "map: snapshot failed (\(error.localizedDescription))"
|
||||||
}
|
}
|
||||||
|
mapStatus?(status)
|
||||||
let request = UNNotificationRequest(identifier: UUID().uuidString, content: finalContent, trigger: nil)
|
let request = UNNotificationRequest(identifier: UUID().uuidString, content: finalContent, trigger: nil)
|
||||||
UNUserNotificationCenter.current().add(request)
|
UNUserNotificationCenter.current().add(request)
|
||||||
}
|
}
|
||||||
@@ -272,8 +281,11 @@ final class ProximityMonitor: NSObject, ObservableObject, @preconcurrency CLLoca
|
|||||||
content.title = "Cheapest \(fuel.displayName) nearby: \(brand)"
|
content.title = "Cheapest \(fuel.displayName) nearby: \(brand)"
|
||||||
content.body = "\(candidate.name) · \(distanceText) away · \(String(format: "%.1fp", price)). Tap for directions."
|
content.body = "\(candidate.name) · \(distanceText) away · \(String(format: "%.1fp", price)). Tap for directions."
|
||||||
content.sound = .default
|
content.sound = .default
|
||||||
addAlertRequest(content: content, station: candidate)
|
let baseResult = "\(fuel.displayName) · \(brand) · \(String(format: "%.1fp", price)) · \(distanceText) away · radius \(unit.format(radiusKM))"
|
||||||
lastTestResult = "\(fuel.displayName) · \(brand) · \(String(format: "%.1fp", price)) · \(distanceText) away · radius \(unit.format(radiusKM))"
|
addAlertRequest(content: content, station: candidate) { status in
|
||||||
|
self.lastTestResult = "\(baseResult) · \(status)"
|
||||||
|
}
|
||||||
|
lastTestResult = baseResult
|
||||||
} else {
|
} else {
|
||||||
// No real candidate (no stations loaded yet, or none sell the
|
// No real candidate (no stations loaded yet, or none sell the
|
||||||
// monitored fuel) — still fire so delivery is testable, but say
|
// monitored fuel) — still fire so delivery is testable, but say
|
||||||
@@ -294,17 +306,39 @@ final class ProximityMonitor: NSObject, ObservableObject, @preconcurrency CLLoca
|
|||||||
|
|
||||||
/// Fires a notification with NO criteria checks at all — no geofence, no
|
/// Fires a notification with NO criteria checks at all — no geofence, no
|
||||||
/// cheapest-within-radius, no dedup, no permission gate, no fuel/radius
|
/// cheapest-within-radius, no dedup, no permission gate, no fuel/radius
|
||||||
/// coupling. Purely tests "does a notification appear, and does tapping
|
/// coupling. Still carries a REAL station (the nearest one selling the
|
||||||
/// it open the app". If notifications are denied system-wide nothing can
|
/// monitored fuel) with its real name, price, distance and coordinates, so
|
||||||
/// display, but this fires regardless of every FuelBoard condition.
|
/// the tap action — directions to that station — is testable regardless of
|
||||||
static func sendPlainTestNotification() {
|
/// every alert condition. If notifications are denied system-wide nothing
|
||||||
let content = UNMutableNotificationContent()
|
/// can display, but this fires regardless of every FuelBoard condition.
|
||||||
content.title = "FuelBoard test notification"
|
func sendPlainTestNotification() {
|
||||||
content.body = "This is a plain test alert — no criteria were checked. Tap to open FuelBoard."
|
let unit = FuelStore.loadDistanceUnit()
|
||||||
content.sound = .default
|
let location = FuelStore.loadLocation()
|
||||||
|
let sellers = stations.filter { $0.prices[fuel] != nil }
|
||||||
|
let nearest = sellers.min { lhs, rhs in
|
||||||
|
let ld = location.map { lhs.distanceKM(to: $0.lat, lng2: $0.lng) } ?? 0
|
||||||
|
let rd = location.map { rhs.distanceKM(to: $0.lat, lng2: $0.lng) } ?? 0
|
||||||
|
return ld < rd
|
||||||
|
}
|
||||||
|
|
||||||
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
|
let content = UNMutableNotificationContent()
|
||||||
UNUserNotificationCenter.current().add(request)
|
if let nearest, let price = nearest.prices[fuel] {
|
||||||
|
let brand = nearest.brand.isEmpty ? nearest.name : nearest.brand
|
||||||
|
let distanceText: String = {
|
||||||
|
guard let location else { return unit.format(0) }
|
||||||
|
return unit.format(nearest.distanceKM(to: location.lat, lng2: location.lng))
|
||||||
|
}()
|
||||||
|
content.title = "\(brand) — plain test notification"
|
||||||
|
content.body = "\(nearest.name) · \(distanceText) away · \(String(format: "%.1fp", price)). Tap for directions."
|
||||||
|
content.sound = .default
|
||||||
|
addAlertRequest(content: content, station: nearest)
|
||||||
|
} else {
|
||||||
|
content.title = "FuelBoard test notification"
|
||||||
|
content.body = "No station data loaded yet — open the Stations tab first."
|
||||||
|
content.sound = .default
|
||||||
|
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
|
||||||
|
UNUserNotificationCenter.current().add(request)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Notification presentation + tap
|
// MARK: - Notification presentation + tap
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ struct SettingsView: View {
|
|||||||
/// Drives the real-data test — ContentView routes this to the live
|
/// Drives the real-data test — ContentView routes this to the live
|
||||||
/// ProximityMonitor so the test uses its actual stations/fuel/radius.
|
/// ProximityMonitor so the test uses its actual stations/fuel/radius.
|
||||||
var onTestAlert: () -> Void = {}
|
var onTestAlert: () -> Void = {}
|
||||||
|
/// Drives the plain test (no criteria) — routed to the live monitor so it
|
||||||
|
/// can carry a real station's name, price and coordinates.
|
||||||
|
var onPlainTestAlert: () -> Void = {}
|
||||||
var onShowOnboarding: () -> Void = {}
|
var onShowOnboarding: () -> Void = {}
|
||||||
|
|
||||||
@StateObject private var tipStore = TipStore()
|
@StateObject private var tipStore = TipStore()
|
||||||
@@ -72,14 +75,14 @@ struct SettingsView: View {
|
|||||||
.foregroundStyle(.green)
|
.foregroundStyle(.green)
|
||||||
}
|
}
|
||||||
Button {
|
Button {
|
||||||
ProximityMonitor.sendPlainTestNotification()
|
onPlainTestAlert()
|
||||||
} label: {
|
} label: {
|
||||||
Label("Send plain test notification", systemImage: "bell.slash.fill")
|
Label("Send plain test notification", systemImage: "bell.slash.fill")
|
||||||
}
|
}
|
||||||
} header: {
|
} header: {
|
||||||
Text("Alerts")
|
Text("Alerts")
|
||||||
} footer: {
|
} 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 and price. The second fires with no criteria at all, 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, 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 {
|
Section {
|
||||||
|
|||||||
Reference in New Issue
Block a user