Favourites are fuel-scoped: starring pins the station for one fuel only

Fixing the bug where favouriting an Unleaded entry also created a Diesel
favourite. Favourites are now [FavouriteEntry] = (station, fuel) pairs:
- Starring a row pins it only for the fuel being viewed
- Favourites tabs show only fuels that actually have favourites
- Geofence monitor gives priority slots only to favourites matching the
  monitored fuel
- Tests updated + new fuel-scoping test (31/31 passing)
This commit is contained in:
FuelBoard Contributor
2026-08-12 08:31:08 +01:00
parent 0dbf88ee7d
commit 35e0adaf0b
6 changed files with 88 additions and 54 deletions
+28 -17
View File
@@ -17,6 +17,16 @@ struct Coordinate: Equatable, Codable {
let lng: Double
}
/// A favourite = a station pinned for ONE fuel type. Starring a row while
/// viewing Unleaded only creates an Unleaded favourite, so the same station
/// can be favourite for Diesel independently (or not at all).
struct FavouriteEntry: Identifiable, Codable, Equatable {
var station: FuelStation
let fuel: FuelType
var id: String { "\(fuel.rawValue)|\(station.id)" }
}
enum SortMode: String, Codable, CaseIterable, Identifiable {
case cheapest
case closest
@@ -234,7 +244,7 @@ struct FuelStore {
static let sortModeKey = "fuelboard.sortMode" // SortMode raw value
static let stationLimitKey = "fuelboard.stationLimitMiles" // Int miles (5/10/15)
static let distanceUnitKey = "fuelboard.distanceUnit" // DistanceUnit raw value
static let favouritesKey = "fuelboard.favourites" // [FuelStation] JSON
static let favouritesKey = "fuelboard.favourites" // [FavouriteEntry] JSON
static let alertsEnabledKey = "fuelboard.alertsEnabled" // Bool
static let alertsRadiusKey = "fuelboard.alertsRadius" // Double km
static let onboardingCompletedKey = "fuelboard.onboardingCompleted" // Bool
@@ -348,24 +358,27 @@ struct FuelStore {
// MARK: Favourites
static func loadFavourites() -> [FuelStation] {
/// A favourite pins a station FOR ONE fuel type. Starring a row while
/// viewing Unleaded only creates an Unleaded favourite the station is
/// not automatically favourited for Premium or Diesel.
static func loadFavourites() -> [FavouriteEntry] {
if let data = keychainData(service: favouritesKey),
let favs = try? JSONDecoder().decode([FuelStation].self, from: data) {
return favs.map { fav in
var f = fav
f.name = fav.name.sanitizedStationTitle
return f
let entries = try? JSONDecoder().decode([FavouriteEntry].self, from: data) {
return entries.map { entry in
var e = entry
e.station.name = entry.station.name.sanitizedStationTitle
return e
}
}
if let defaults = UserDefaults(suiteName: appGroupSuite),
let data = defaults.data(forKey: favouritesKey),
let favs = try? JSONDecoder().decode([FuelStation].self, from: data) {
return favs
let entries = try? JSONDecoder().decode([FavouriteEntry].self, from: data) {
return entries
}
return []
}
static func saveFavourites(_ favourites: [FuelStation]) {
static func saveFavourites(_ favourites: [FavouriteEntry]) {
if let data = try? JSONEncoder().encode(favourites) {
UserDefaults(suiteName: appGroupSuite)?.set(data, forKey: favouritesKey)
writeKeychain(data: data, service: favouritesKey)
@@ -374,14 +387,12 @@ struct FuelStore {
/// Returns favourites with fresh prices applied from the given station list
/// (favourites keep their cached snapshot when not in the current results).
static func refreshedFavourites(_ favourites: [FuelStation], from stations: [FuelStation]) -> [FuelStation] {
var updated = favourites
for (i, fav) in favourites.enumerated() {
if let fresh = stations.first(where: { $0.id == fav.id }) {
updated[i] = fresh
}
/// The fuel scoping is preserved per entry.
static func refreshedFavourites(_ favourites: [FavouriteEntry], from stations: [FuelStation]) -> [FavouriteEntry] {
favourites.map { entry in
guard let fresh = stations.first(where: { $0.id == entry.station.id }) else { return entry }
return FavouriteEntry(station: fresh, fuel: entry.fuel)
}
return updated
}
// MARK: Alerts