Settings: plain test notification + foreground banner delegate
- New 'Send plain test notification' button fires with zero criteria checks (no geofence, no cheapest check, no permission gate) to isolate delivery and tap behaviour - ProximityMonitor is now the UNUserNotificationCenter delegate: banners show even in the foreground (system suppresses them otherwise), and taps complete the 'open FuelBoard' action
This commit is contained in:
@@ -8,7 +8,7 @@ import SwiftUI
|
|||||||
/// Region budget: iOS allows 20 monitored regions per app — favourites are
|
/// Region budget: iOS allows 20 monitored regions per app — favourites are
|
||||||
/// always registered first, then the closest remaining stations fill the rest.
|
/// always registered first, then the closest remaining stations fill the rest.
|
||||||
@MainActor
|
@MainActor
|
||||||
final class ProximityMonitor: NSObject, ObservableObject, @preconcurrency CLLocationManagerDelegate {
|
final class ProximityMonitor: NSObject, ObservableObject, @preconcurrency CLLocationManagerDelegate, UNUserNotificationCenterDelegate {
|
||||||
@Published var monitoredStationIDs: [String] = []
|
@Published var monitoredStationIDs: [String] = []
|
||||||
@Published var lastAlert: String?
|
@Published var lastAlert: String?
|
||||||
|
|
||||||
@@ -25,6 +25,9 @@ final class ProximityMonitor: NSObject, ObservableObject, @preconcurrency CLLoca
|
|||||||
manager.delegate = self
|
manager.delegate = self
|
||||||
manager.desiredAccuracy = kCLLocationAccuracyHundredMeters
|
manager.desiredAccuracy = kCLLocationAccuracyHundredMeters
|
||||||
manager.pausesLocationUpdatesAutomatically = true
|
manager.pausesLocationUpdatesAutomatically = true
|
||||||
|
// Show alert banners even while the app is open, and complete the tap
|
||||||
|
// action (opening FuelBoard) when the user taps a notification.
|
||||||
|
UNUserNotificationCenter.current().delegate = self
|
||||||
// Restore persisted state on background relaunch so region events work
|
// Restore persisted state on background relaunch so region events work
|
||||||
// even before the view fully appears.
|
// even before the view fully appears.
|
||||||
enabled = FuelStore.loadAlertsEnabled()
|
enabled = FuelStore.loadAlertsEnabled()
|
||||||
@@ -187,4 +190,43 @@ final class ProximityMonitor: NSObject, ObservableObject, @preconcurrency CLLoca
|
|||||||
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
|
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
|
||||||
UNUserNotificationCenter.current().add(request)
|
UNUserNotificationCenter.current().add(request)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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
|
||||||
|
|
||||||
|
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
|
||||||
|
UNUserNotificationCenter.current().add(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Notification presentation + tap
|
||||||
|
|
||||||
|
/// Show alert banners even while FuelBoard is in the foreground — without
|
||||||
|
/// this, the system silently suppresses notifications when the app is open.
|
||||||
|
nonisolated func userNotificationCenter(
|
||||||
|
_ center: UNUserNotificationCenter,
|
||||||
|
willPresent notification: UNNotification,
|
||||||
|
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
|
||||||
|
) {
|
||||||
|
Task { @MainActor in
|
||||||
|
completionHandler([.banner, .sound])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tapping a notification just opens the app (the alert's "Tap to open").
|
||||||
|
/// No extra routing — the app comes to the foreground as-is.
|
||||||
|
nonisolated func userNotificationCenter(
|
||||||
|
_ center: UNUserNotificationCenter,
|
||||||
|
didReceive response: UNNotificationResponse,
|
||||||
|
withCompletionHandler completionHandler: @escaping () -> Void
|
||||||
|
) {
|
||||||
|
completionHandler()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,10 +59,15 @@ struct SettingsView: View {
|
|||||||
.font(.footnote)
|
.font(.footnote)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
|
Button {
|
||||||
|
ProximityMonitor.sendPlainTestNotification()
|
||||||
|
} label: {
|
||||||
|
Label("Send plain test notification", systemImage: "bell.slash.fill")
|
||||||
|
}
|
||||||
} header: {
|
} header: {
|
||||||
Text("Alerts")
|
Text("Alerts")
|
||||||
} footer: {
|
} 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.")
|
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.")
|
||||||
}
|
}
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
|
|||||||
Reference in New Issue
Block a user