From 1cd4d86a6967ed0392a5f9de665e7ec5c4c4def9 Mon Sep 17 00:00:00 2001 From: FuelBoard Contributor Date: Sat, 17 Oct 2026 12:18:54 +0100 Subject: [PATCH] Alerts tab: clearer copy, dynamic descriptions, section rename --- FuelBoard/AlertsView.swift | 111 ++++++++++++++----------- FuelBoard/en.lproj/Localizable.strings | 24 +++--- 2 files changed, 75 insertions(+), 60 deletions(-) diff --git a/FuelBoard/AlertsView.swift b/FuelBoard/AlertsView.swift index 4307e0d..75a3112 100644 --- a/FuelBoard/AlertsView.swift +++ b/FuelBoard/AlertsView.swift @@ -6,8 +6,8 @@ import SwiftUI /// 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. +/// Settings → Debug; the user-facing page keeps short descriptions that follow +/// the selected alert type and radius, plus a 0-stations hint. struct AlertsView: View { @Binding var enabled: Bool @Binding var radius: Double // stored in km (monitor + storage) @@ -55,31 +55,40 @@ struct AlertsView: View { return String(format: String(localized: "%lld %@", comment: "Distance label"), 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 { + /// The distance the geofence actually caps "Follow search" at, in miles + /// (FuelStore stores the cap in km so the monitor and storage are unit-free). + private var followSearchCapMiles: Int { + Int((FuelStore.alertFollowCapKM / FuelSavingsCalculator.kilometresPerMile).rounded()) + } + + /// What the selected alert type actually does. Swapped with the picker so + /// the copy can never describe the wrong mode. + private var alertModeDescription: String { + switch alertMode { + case .cheapest: + return String(localized: "Get notified when you're near one of the cheapest stations for your chosen fuel.", comment: "Cheapest station proximity alert description") + case .bestValue: + return String(localized: "Get notified when you're near a station that could save you money after travel costs.", comment: "Best Value proximity alert description") + } + } + + /// The radius the selected option really monitors, stated in the user's + /// unit — e.g. "Get alerts when you're within 2 miles of a matching + /// station." "Follow search" mirrors the Stations-tab distance, which the + /// geofence caps; said plainly, and only when the search distance really + /// exceeds the cap, so the sentence never contradicts the picker row. + private var alertRadiusDescription: String { switch alertChoice.wrappedValue { case .followSearch: - return String(localized: "Set-and-forget: alerts behave exactly like the Stations-tab search distance (capped at 8 miles for reliable geofencing).", comment: "Alert radius guidance for following search distance") - case .fixed(let value): - let optionLabel = String(format: String(localized: "%lld %@", comment: "Distance label"), value, distanceUnit.label(for: Double(value))) - let body: String - switch value { - case 1: - body = String(localized: "Town driving. Fires most often — you're rarely inside the circle for long, so pings are meaningful. Best for stop-start local trips.", comment: "Alert radius guidance") - case 2: - body = String(localized: "City or suburb commute. Slightly earlier heads-up than the smallest option; still fires regularly around town.", comment: "Alert radius guidance") - case 3: - body = String(localized: "Mixed driving. Middle ground: heads-up a few minutes out, still works in most towns.", comment: "Alert radius guidance") - case 5: - body = String(localized: "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.", comment: "Alert radius guidance") - default: - body = String(localized: "Long-distance journeys. Maximum reach: a ping when a cheap station is coming up from far out. In town this essentially never fires.", comment: "Alert radius guidance") + let capped = stationLimit > followSearchCapMiles + let label = distanceLabel(forMiles: capped ? followSearchCapMiles : stationLimit) + if capped { + return String(format: String(localized: "Get alerts when you're within %@ of a matching station (your search distance is capped for reliable alerts).", comment: "Proximity alert radius description, follow search capped"), label) } - return String(format: String(localized: "%@ — %@", comment: "Alert radius guidance with option label"), optionLabel, body) + return String(format: String(localized: "Get alerts when you're within %@ of a matching station.", comment: "Proximity alert radius description"), label) + case .fixed(let value): + let label = String(format: String(localized: "%lld %@", comment: "Distance label"), value, distanceUnit.label(for: Double(value))) + return String(format: String(localized: "Get alerts when you're within %@ of a matching station.", comment: "Proximity alert radius description"), label) } } @@ -148,10 +157,11 @@ struct AlertsView: View { } Button(String(localized: "Check now", comment: "Manual favourite price-drop check button"), action: onCheckFavouriteDropNow) .disabled(!favouriteDropEnabled) + Text(String(localized: "Get notified when a different favourite becomes the cheapest, or your cheapest favourite drops in price.", comment: "Favourite price alerts section description")) + .font(.footnote) + .foregroundStyle(.secondary) } header: { - Text(String(localized: "Price-drop alerts", comment: "Favourite price-drop alerts section title")) - } footer: { - Text(String(localized: "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.", comment: "Favourite price-drop alerts section footer")) + Text(String(localized: "Favourite price alerts", comment: "Favourite price alerts section title")) } Section { @@ -163,7 +173,19 @@ struct AlertsView: View { } if !bestValueConfigured { Text(String(localized: "Set up Best Value first so Best Value alerts can account for your fill size, MPG and travel cost.", comment: "Best Value alert unavailable hint")) - .font(.caption) + .font(.footnote) + .foregroundStyle(.secondary) + } + Text(alertModeDescription) + .font(.footnote) + .foregroundStyle(.secondary) + if alertMode == .bestValue { + // The threshold is edited in Settings → Fuel Savings: it + // labels station rows BEST VALUE as well as gating this + // alert, so it lives with the fill size and MPG that + // produce the saving. Read back here, not offered twice. + Text(String(format: String(localized: "Minimum saving %@ — change in Settings, Fuel Savings.", comment: "Best Value alert threshold read-back"), FuelSavingsFormat.money(bestValueThresholdGBP))) + .font(.footnote) .foregroundStyle(.secondary) } Picker(String(localized: "Fuel", comment: "Fuel picker title"), selection: $fuel) { @@ -177,30 +199,19 @@ struct AlertsView: View { Text(String(format: String(localized: "%lld %@", comment: "Distance label"), value, distanceUnit.label(for: Double(value)))).tag(AlertRadiusChoice.fixed(value)) } } - Text(alertRadiusGuidance) - .font(.caption) + Text(alertRadiusDescription) + .font(.footnote) .foregroundStyle(.secondary) - if alertMode == .bestValue { - // The threshold is edited in Settings → Fuel Savings: it - // labels station rows BEST VALUE as well as gating this - // alert, so it lives with the fill size and MPG that - // produce the saving. Read back here, not offered twice. - Text(String(format: String(localized: "Minimum saving %@ — change in Settings, Fuel Savings.", comment: "Best Value alert threshold read-back"), FuelSavingsFormat.money(bestValueThresholdGBP))) - .font(.caption) - .foregroundStyle(.secondary) - } if enabled && monitoredCount == 0 { Text(String(localized: "No stations monitored yet — open the Stations tab to load prices first.", comment: "No monitored stations hint")) - .font(.caption) + .font(.footnote) .foregroundStyle(.secondary) } + Text(String(localized: "Background alerts depend on when your iPhone allows them; each station alerts at most once an hour.", comment: "Proximity alerts background limitation note")) + .font(.footnote) + .foregroundStyle(.secondary) } header: { Text(String(localized: "Proximity alerts", comment: "Proximity alerts section title")) - } footer: { - let modeDescription = alertMode == .bestValue - ? String(format: String(localized: "When you approach the Best Value station within the radius, FuelBoard sends a notification if it could leave you at least %@ better off after extra return travel. Nearby stations selling %@ are watched — favourites get priority — and each station alerts at most once per hour.", comment: "Best Value proximity alerts section footer"), FuelSavingsFormat.money(bestValueThresholdGBP), fuel.displayName.lowercased()) - : String(format: String(localized: "When you approach a station that is the cheapest within the radius, FuelBoard sends a notification — even with the app closed. Nearby stations selling %@ 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.", comment: "Proximity alerts section footer"), fuel.displayName.lowercased()) - Text(modeDescription) } if enabled, let lastAlert { @@ -223,10 +234,16 @@ struct AlertsView: View { Text(distanceLabel(forMiles: miles)).tag(LADistanceChoice.fixed(miles)) } } + if liveActivityFollowsSearch { + Text(String(localized: "Uses the same distance as your station search.", comment: "Live Activity distance description, follow search")) + .font(.footnote) + .foregroundStyle(.secondary) + } + Text(String(localized: "See the cheapest nearby station on your Lock Screen and Dynamic Island, with updates as you travel.", comment: "Live Activity section description")) + .font(.footnote) + .foregroundStyle(.secondary) } header: { Text(String(localized: "Live Activity", comment: "Live Activity section title")) - } footer: { - Text(String(localized: "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.", comment: "Live Activity section footer")) } } .navigationTitle(String(localized: "Alerts", comment: "Alerts tab title")) diff --git a/FuelBoard/en.lproj/Localizable.strings b/FuelBoard/en.lproj/Localizable.strings index 9dd9bde..b5de580 100644 --- a/FuelBoard/en.lproj/Localizable.strings +++ b/FuelBoard/en.lproj/Localizable.strings @@ -58,11 +58,9 @@ "Radius" = "Radius"; "%lld %@" = "%lld %@"; "Follow search (%@)" = "Follow search (%@)"; -"When you approach a station that is the cheapest within the radius, FuelBoard sends a notification — even with the app closed. Nearby stations selling %@ 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." = "When you approach a station that is the cheapest within the radius, FuelBoard sends a notification — even with the app closed. Nearby stations selling %@ 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."; "No stations monitored yet — open the Stations tab to load prices first." = "No stations monitored yet — open the Stations tab to load prices first."; "Last alert" = "Last alert"; "Live Activity" = "Live Activity"; -"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." = "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."; /* Settings tab */ "Units" = "Units"; @@ -214,8 +212,6 @@ /* Alerts and favourites localisation pass */ "Cheapest favourite" = "Cheapest favourite"; "Check now" = "Check now"; -"Price-drop alerts" = "Price-drop alerts"; -"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." = "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."; "Cheapest station" = "Cheapest station"; "Alert type" = "Alert type"; "Best Value" = "Best Value"; @@ -224,16 +220,7 @@ "Minimum saving %@ — change in Settings, Fuel Savings." = "Minimum saving %@ — change in Settings, Fuel Savings."; "Set up Best Value first so FuelBoard can account for your fill size, MPG and travel cost." = "Set up Best Value first so FuelBoard can account for your fill size, MPG and travel cost."; "Set up Best Value first so Best Value alerts can account for your fill size, MPG and travel cost." = "Set up Best Value first so Best Value alerts can account for your fill size, MPG and travel cost."; -"When you approach the Best Value station within the radius, FuelBoard sends a notification if it could leave you at least %@ better off after extra return travel. Nearby stations selling %@ are watched — favourites get priority — and each station alerts at most once per hour." = "When you approach the Best Value station within the radius, FuelBoard sends a notification if it could leave you at least %@ better off after extra return travel. Nearby stations selling %@ are watched — favourites get priority — and each station alerts at most once per hour."; "Proximity alerts" = "Proximity alerts"; -"When you approach a station that is the cheapest within the radius, FuelBoard sends a notification — even with the app closed. Nearby stations selling %@ 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." = "When you approach a station that is the cheapest within the radius, FuelBoard sends a notification — even with the app closed. Nearby stations selling %@ 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."; -"Set-and-forget: alerts behave exactly like the Stations-tab search distance (capped at 8 miles for reliable geofencing)." = "Set-and-forget: alerts behave exactly like the Stations-tab search distance (capped at 8 miles for reliable geofencing)."; -"Town driving. Fires most often — you're rarely inside the circle for long, so pings are meaningful. Best for stop-start local trips." = "Town driving. Fires most often — you're rarely inside the circle for long, so pings are meaningful. Best for stop-start local trips."; -"City or suburb commute. Slightly earlier heads-up than the smallest option; still fires regularly around town." = "City or suburb commute. Slightly earlier heads-up than the smallest option; still fires regularly around town."; -"Mixed driving. Middle ground: heads-up a few minutes out, still works in most towns." = "Mixed driving. Middle ground: heads-up a few minutes out, still works in most towns."; -"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." = "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."; -"Long-distance journeys. Maximum reach: a ping when a cheap station is coming up from far out. In town this essentially never fires." = "Long-distance journeys. Maximum reach: a ping when a cheap station is coming up from far out. In town this essentially never fires."; -"%@ — %@" = "%@ — %@"; /* Best Value calculation and station price age */ "£ compares the litres your budget buys at each station; Litres compares one fixed quantity." = "£ compares the litres your budget buys at each station; Litres compares one fixed quantity."; @@ -246,3 +233,14 @@ "May be outdated" = "May be outdated"; "Fuel you'll receive" = "Fuel you'll receive"; "Compared with your closest station." = "Compared with your closest station."; + +/* Alerts tab — refined copy (sections, descriptions, limitations) */ +"Favourite price alerts" = "Favourite price alerts"; +"Get notified when a different favourite becomes the cheapest, or your cheapest favourite drops in price." = "Get notified when a different favourite becomes the cheapest, or your cheapest favourite drops in price."; +"Get notified when you're near one of the cheapest stations for your chosen fuel." = "Get notified when you're near one of the cheapest stations for your chosen fuel."; +"Get notified when you're near a station that could save you money after travel costs." = "Get notified when you're near a station that could save you money after travel costs."; +"Get alerts when you're within %@ of a matching station." = "Get alerts when you're within %@ of a matching station."; +"Get alerts when you're within %@ of a matching station (your search distance is capped for reliable alerts)." = "Get alerts when you're within %@ of a matching station (your search distance is capped for reliable alerts)."; +"Uses the same distance as your station search." = "Uses the same distance as your station search."; +"See the cheapest nearby station on your Lock Screen and Dynamic Island, with updates as you travel." = "See the cheapest nearby station on your Lock Screen and Dynamic Island, with updates as you travel."; +"Background alerts depend on when your iPhone allows them; each station alerts at most once an hour." = "Background alerts depend on when your iPhone allows them; each station alerts at most once an hour.";