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:
FuelBoard Contributor
2026-08-12 10:06:48 +01:00
parent a964a45d49
commit 9032c4a551
3 changed files with 58 additions and 20 deletions
+52 -18
View File
@@ -195,7 +195,13 @@ final class ProximityMonitor: NSObject, ObservableObject, @preconcurrency CLLoca
/// 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
/// 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 = [
"stationName": station.name,
"stationLat": station.lat,
@@ -212,17 +218,20 @@ final class ProximityMonitor: NSObject, ObservableObject, @preconcurrency CLLoca
snapshotter.start { [content] snapshot, error in
Task { @MainActor in
let finalContent = content
var status = "map: no image"
if let snapshot, error == nil,
let image = Self.mapImage(snapshot: snapshot, station: station) {
let url = FileManager.default.temporaryDirectory
.appendingPathComponent("alert-map-\(UUID().uuidString).png")
if let data = image.pngData() {
try? data.write(to: url)
if let attachment = try? UNNotificationAttachment(identifier: "map", url: url, options: nil) {
finalContent.attachments = [attachment]
}
if let data = image.pngData(), (try? data.write(to: url)) != nil,
let attachment = try? UNNotificationAttachment(identifier: "map", url: url, options: nil) {
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)
UNUserNotificationCenter.current().add(request)
}
@@ -272,8 +281,11 @@ final class ProximityMonitor: NSObject, ObservableObject, @preconcurrency CLLoca
content.title = "Cheapest \(fuel.displayName) nearby: \(brand)"
content.body = "\(candidate.name) · \(distanceText) away · \(String(format: "%.1fp", price)). Tap for directions."
content.sound = .default
addAlertRequest(content: content, station: candidate)
lastTestResult = "\(fuel.displayName) · \(brand) · \(String(format: "%.1fp", price)) · \(distanceText) away · radius \(unit.format(radiusKM))"
let baseResult = "\(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 {
// No real candidate (no stations loaded yet, or none sell the
// 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
/// cheapest-within-radius, no dedup, no permission gate, no fuel/radius
/// coupling. Purely tests "does a notification appear, and does tapping
/// it open the app". If notifications are denied system-wide nothing can
/// display, but this fires regardless of every FuelBoard condition.
static func sendPlainTestNotification() {
let content = UNMutableNotificationContent()
content.title = "FuelBoard test notification"
content.body = "This is a plain test alert — no criteria were checked. Tap to open FuelBoard."
content.sound = .default
/// coupling. Still carries a REAL station (the nearest one selling the
/// monitored fuel) with its real name, price, distance and coordinates, so
/// the tap action directions to that station is testable regardless of
/// every alert condition. If notifications are denied system-wide nothing
/// can display, but this fires regardless of every FuelBoard condition.
func sendPlainTestNotification() {
let unit = FuelStore.loadDistanceUnit()
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)
UNUserNotificationCenter.current().add(request)
let content = UNMutableNotificationContent()
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