import SwiftUI /// Alerts tab — enables the cheapest-station proximity alerts. The fuel being /// monitored is chosen HERE, independently of the Stations-tab selection: /// users can browse any fuel without re-targeting their alerts (and vice /// versa). The Live Activity section also lives here — it mirrors this page's /// shape (toggle + fuel + distance) so the two "notify me while driving" /// surfaces sit together. Geofence plumbing (count, 18-limit) lives in /// Settings → Debug; the user-facing page keeps a plain-language explainer /// plus a 0-stations hint. struct AlertsView: View { @Binding var enabled: Bool @Binding var radius: Double // stored in km (monitor + storage) @Binding var fuel: FuelType // the fuel alerts monitor for @Binding var favouriteDropEnabled: Bool @Binding var favouriteDropFuel: FuelType /// Whether alerts mirror the Stations-tab search distance (capped at 8 mi). @Binding var followsSearch: Bool /// Whether the "cheapest nearby" Live Activity is shown on the Lock Screen /// / Dynamic Island. Tracks its OWN fuel + radius (below) — independent of /// the Stations tab. @Binding var liveActivityEnabled: Bool /// Fuel the Live Activity tracks (independent of the Stations tab). @Binding var liveActivityFuel: FuelType /// Search radius (miles, 5/10/15) the Live Activity uses. @Binding var liveActivityRadiusMiles: Int /// Whether the Live Activity mirrors the Stations-tab search distance. @Binding var liveActivityFollowsSearch: Bool /// Current Stations-tab search distance (miles) — the value "Follow search" /// mirrors. let stationLimit: Int let distanceUnit: DistanceUnit let monitoredCount: Int let lastAlert: String? /// The Stations-tab search distance shown in the user's unit, e.g. /// "5 miles" or "8 km" — for the "Follow search" rows. private var searchInUnitLabel: String { let shown = distanceUnit.displayMiles(stationLimit) // stationLimit is always miles return "\(shown) \(distanceUnit.label(for: Double(shown)))" } /// A miles value (5/10/15) shown truthfully in the user's unit, e.g. /// "5 miles" (miles unit) or "8 km" (km unit) — picker labels must match /// what the option really means. private func distanceLabel(forMiles miles: Int) -> String { let shown = distanceUnit.displayMiles(miles) return "\(shown) \(distanceUnit.label(for: Double(shown)))" } /// Situational guidance for the currently-selected alert radius, shown /// under the picker so users know what each choice is best for. Labels /// mirror the picker row text (unit-aware) and explain the trade-off: /// smaller radii ping more often but later; larger radii ping rarely in /// town but earlier from afar. private var alertRadiusGuidance: String { switch alertChoice.wrappedValue { case .followSearch: return "Set-and-forget: alerts behave exactly like the Stations-tab search distance (capped at 8 miles for reliable geofencing)." case .fixed(let value): let optionLabel = "\(value) \(distanceUnit.label(for: Double(value)))" let body: String switch value { case 1: body = "Town driving. Fires most often — you're rarely inside the circle for long, so pings are meaningful. Best for stop-start local trips." case 2: body = "City or suburb commute. Slightly earlier heads-up than the smallest option; still fires regularly around town." case 3: body = "Mixed driving. Middle ground: heads-up a few minutes out, still works in most towns." case 5: body = "A-roads and motorway exits. Wide early warning for longer trips — but in town it rarely fires, because you're usually already inside the circle." default: body = "Long-distance journeys. Maximum reach: a ping when a cheap station is coming up from far out. In town this essentially never fires." } return "\(optionLabel) — \(body)" } } private enum AlertRadiusChoice: Hashable { case followSearch case fixed(Int) } /// The radius picker works in the user's chosen unit; the stored value /// stays km so ProximityMonitor and persistence never change. Options are /// snapped to the nearest picker value so any stored radius still selects. /// "Follow search" mirrors the Stations-tab distance (capped at 8 miles). private var alertChoice: Binding { Binding( get: { if followsSearch { return .followSearch } let inUnit = distanceUnit.fromKM(radius) let snapped = FuelStore.alertRadiusOptions .min(by: { abs(Double($0) - inUnit) < abs(Double($1) - inUnit) }) ?? 3 return .fixed(snapped) }, set: { choice in switch choice { case .followSearch: followsSearch = true case .fixed(let value): followsSearch = false radius = distanceUnit.toKM(Double(value)) } } ) } private enum LADistanceChoice: Hashable { case followSearch case fixed(Int) } private var laChoice: Binding { Binding( get: { if liveActivityFollowsSearch { return .followSearch } return .fixed(liveActivityRadiusMiles) }, set: { choice in switch choice { case .followSearch: liveActivityFollowsSearch = true case .fixed(let miles): liveActivityFollowsSearch = false liveActivityRadiusMiles = miles } } ) } var body: some View { NavigationStack { List { Section { Toggle("Cheapest favourite", isOn: $favouriteDropEnabled) Picker("Fuel", selection: $favouriteDropFuel) { ForEach(FuelType.allCases) { fuel in Text(fuel.displayName).tag(fuel) } } } header: { Text("Price-drop alerts") } footer: { Text("Watches your cheapest saved favourite for the fuel you pick here. FuelBoard alerts when a different favourite becomes the cheapest, or when your current cheapest favourite drops by at least 1.0p. This is separate from the nearby-geofence alert, so you can monitor favourites without extra location noise.") } Section { Toggle("Enabled", isOn: $enabled) Picker("Fuel", selection: $fuel) { ForEach(FuelType.allCases) { fuel in Text(fuel.displayName).tag(fuel) } } Picker("Radius", selection: alertChoice) { Text("Follow search (\(searchInUnitLabel))").tag(AlertRadiusChoice.followSearch) ForEach(FuelStore.alertRadiusOptions, id: \.self) { value in Text("\(value) \(distanceUnit.label(for: Double(value)))").tag(AlertRadiusChoice.fixed(value)) } } Text(alertRadiusGuidance) .font(.caption) .foregroundStyle(.secondary) if enabled && monitoredCount == 0 { Text("No stations monitored yet — open the Stations tab to load prices first.") .font(.caption) .foregroundStyle(.secondary) } } header: { Text("Cheapest station") } footer: { Text("When you approach a station that is the cheapest within the radius, FuelBoard sends a notification — even with the app closed. Nearby stations selling \(fuel.displayName.lowercased()) are watched — favourites get priority — and each station alerts at most once per hour. Follow search mirrors the Stations-tab distance, capped at 8 miles for reliable geofencing. Smaller radii ping more often but later; larger radii ping rarely in town but earlier from afar.") } if enabled, let lastAlert { Section("Last alert") { Label(lastAlert, systemImage: "bell.badge.fill") .font(.footnote) } } Section { Toggle("Live Activity", isOn: $liveActivityEnabled) Picker("Fuel", selection: $liveActivityFuel) { ForEach(FuelType.allCases) { fuel in Text(fuel.displayName).tag(fuel) } } Picker("Distance", selection: laChoice) { Text("Follow search (\(searchInUnitLabel))").tag(LADistanceChoice.followSearch) ForEach(FuelStore.stationRadiusOptions, id: \.self) { miles in Text(distanceLabel(forMiles: miles)).tag(LADistanceChoice.fixed(miles)) } } } header: { Text("Live Activity") } footer: { Text("Shows the cheapest station for the fuel and distance you pick here on the Lock Screen and Dynamic Island — independent of the Stations tab (choose Follow search to mirror its distance instead). Updates as you drive; tap to open directions. Live Activities can be disabled in System Settings → Live Activities.") } } .navigationTitle("Alerts") } } }