Add cheapest-favourite price-drop alerts

This commit is contained in:
FuelBoard Contributor
2026-08-17 16:33:46 +01:00
parent 63084322f6
commit 6f0fc43085
4 changed files with 276 additions and 64 deletions
+73
View File
@@ -31,6 +31,16 @@ struct FavouriteEntry: Identifiable, Codable, Equatable {
var id: String { "\(fuel.rawValue)|\(station.id)" }
}
/// Persisted baseline for the per-fuel cheapest-favourite price-drop alert.
/// Stored separately from the favourites list so refresh-time comparison can
/// survive app relaunches without immediately alerting on first fetch.
struct FavouriteAlertSnapshot: Codable, Equatable {
let fuel: FuelType
let stationID: String
let stationName: String
let price: Double
}
enum SortMode: String, Codable, CaseIterable, Identifiable {
case cheapest
case closest
@@ -318,6 +328,9 @@ struct FuelStore {
static let alertsRadiusKey = "fuelboard.alertsRadius" // Double km
static let alertsFuelKey = "fuelboard.alertsFuel" // FuelType raw value
static let alertsFollowSearchKey = "fuelboard.alertsFollowSearch" // Bool alerts mirror the Stations-tab distance
static let favouriteDropAlertsEnabledKey = "fuelboard.favouriteDropAlertsEnabled" // Bool
static let favouriteDropAlertsFuelKey = "fuelboard.favouriteDropAlertsFuel" // FuelType raw value
static let favouriteAlertSnapshotsKey = "fuelboard.favouriteAlertSnapshots" // [fuel: FavouriteAlertSnapshot] JSON
static let liveActivityFollowSearchKey = "fuelboard.liveActivityFollowSearch" // Bool Live Activity mirrors the Stations-tab distance
static let debugModeKey = "fuelboard.debugMode" // Bool hidden dev flag
static let relayFallbackKey = "fuelboard.relayFallback" // Bool dev-only relay fallback
@@ -627,6 +640,24 @@ struct FuelStore {
}
}
/// The cheapest favourite for one fuel, preserving the user's stored order
/// on price ties. Used by the favourite price-drop alert so a tie does not
/// flap between equally-priced favourites.
static func cheapestFavourite(in favourites: [FavouriteEntry], fuel: FuelType) -> FavouriteEntry? {
var best: FavouriteEntry?
for entry in favourites where entry.fuel == fuel {
guard let price = entry.station.prices[fuel] else { continue }
guard let current = best, let currentPrice = current.station.prices[fuel] else {
best = entry
continue
}
if price < currentPrice {
best = entry
}
}
return best
}
/// Reorders ONE fuel's favourites within the global array (drag-and-drop in
/// the Favourites tab). The moved fuel's block stays at its original
/// position in the array; other fuels keep their relative order. The array
@@ -710,6 +741,48 @@ struct FuelStore {
saveString(enabled ? "1" : "0", service: alertsFollowSearchKey)
}
// MARK: Favourite price-drop alerts
static func loadFavouriteDropAlertsEnabled() -> Bool {
loadString(service: favouriteDropAlertsEnabledKey) == "1"
}
static func saveFavouriteDropAlertsEnabled(_ enabled: Bool) {
saveString(enabled ? "1" : "0", service: favouriteDropAlertsEnabledKey)
}
static func loadFavouriteDropAlertsFuel() -> FuelType {
if let raw = loadString(service: favouriteDropAlertsFuelKey), let fuel = FuelType(rawValue: raw) {
return fuel
}
return .e10
}
static func saveFavouriteDropAlertsFuel(_ fuel: FuelType) {
saveString(fuel.rawValue, service: favouriteDropAlertsFuelKey)
}
static func loadFavouriteAlertSnapshots() -> [FuelType: FavouriteAlertSnapshot] {
if let data = keychainData(service: favouriteAlertSnapshotsKey),
let stored = try? JSONDecoder().decode([String: FavouriteAlertSnapshot].self, from: data) {
var result: [FuelType: FavouriteAlertSnapshot] = [:]
for (key, value) in stored {
if let fuel = FuelType(rawValue: key) {
result[fuel] = value
}
}
return result
}
return [:]
}
static func saveFavouriteAlertSnapshots(_ snapshots: [FuelType: FavouriteAlertSnapshot]) {
let keyed = Dictionary(uniqueKeysWithValues: snapshots.map { ($0.key.rawValue, $0.value) })
if let data = try? JSONEncoder().encode(keyed) {
writeKeychain(data: data, service: favouriteAlertSnapshotsKey)
}
}
// MARK: Live Activity
static func loadLiveActivityEnabled() -> Bool {