From 4d35b085339e570427ce81e85912c2259824e56a Mon Sep 17 00:00:00 2001 From: FuelBoard Contributor Date: Thu, 13 Aug 2026 19:35:46 +0100 Subject: [PATCH] =?UTF-8?q?Widget:=20keychain-free=20fresh=20defaults=20?= =?UTF-8?q?=E2=80=94=20drop=20defaultResult(),=20auto-populate=20at=20entr?= =?UTF-8?q?y=20time?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The small-widget skeleton (fresh widget stranded on the placeholder while the medium populated in the gallery) traced to the small intent's default configuration resolution: FavouriteFuelQuery/WidgetFavouriteQuery defaultResult() read keychain in the gallery-preview/fresh-add path, failing the whole default config and leaving no timeline. Configured widgets bypassed defaultResult (stored values → lenient entities) — the exact favourites-works/cheapest-closest-doesn't split. - Remove defaultResult() from both queries: fresh configs now resolve from the static @Parameter(default:) values through lenient entities(for:) with ZERO storage reads. - WidgetFavouriteQuery.entities(for:) skips the keychain name lookup for id-only (empty station) values. - Auto-populate moves to makeEntry: empty pinned favourite → first favourite of the fuel (same rendered result as the old sheet default). - Sheet keeps the dependent picker via suggestedEntities() + dependency; fresh widgets show static defaults until picked (approved tradeoff). --- FuelBoardWidgets/FuelPriceWidget.swift | 27 ++++++++--- FuelBoardWidgets/WidgetConfigIntent.swift | 59 ++++++++++------------- 2 files changed, 45 insertions(+), 41 deletions(-) diff --git a/FuelBoardWidgets/FuelPriceWidget.swift b/FuelBoardWidgets/FuelPriceWidget.swift index e358979..68e87e9 100644 --- a/FuelBoardWidgets/FuelPriceWidget.swift +++ b/FuelBoardWidgets/FuelPriceWidget.swift @@ -165,16 +165,29 @@ struct FuelPriceTimelineProvider [FavouriteFuel] { identifiers.compactMap { FuelType(rawValue: $0) }.map { FavouriteFuel(fuel: $0) } } @@ -106,15 +112,6 @@ struct FavouriteFuelQuery: EntityQuery { let fuels = Set(FuelStore.loadFavourites().map(\.fuel)) return FuelType.allCases.filter { fuels.contains($0) }.map { FavouriteFuel(fuel: $0) } } - - /// Fresh widget / unresolved value → auto-populate the Fuel row with the - /// FIRST fuel that has a favourite (Unleaded, Premium, Diesel order). - func defaultResult() async -> FavouriteFuel? { - let fuels = Set(FuelStore.loadFavourites().map(\.fuel)) - return FuelType.allCases.first { fuels.contains($0) } - .map { FavouriteFuel(fuel: $0) } - ?? FavouriteFuel(fuel: .e10) - } } // A pinned favourite station, selectable on SMALL widgets (which show a single @@ -164,13 +161,27 @@ struct WidgetFavouriteQuery: EntityQuery { // WidgetFavourite (best-effort name lookup). NEVER return nil here — the // timeline request resolves the whole configuration through this query // and a failed resolution strands a default-config widget on the skeleton - // placeholder. Fresh widgets auto-populate via defaultResult(). + // placeholder. + // + // NO defaultResult(): fresh/default configs resolve from the static + // @Parameter(default:) value (empty id "e10|") with NO storage reads — + // the old keychain-backed defaultResult() ran in the gallery-preview / + // fresh-add resolution path and failed on-device. The sheet's + // auto-populate moved to entry time (makeEntry pins the first favourite + // of the fuel); the dependent picker keeps working via suggestedEntities(). func entities(for identifiers: [String]) async throws -> [WidgetFavourite] { - let favourites = FuelStore.loadFavourites() return identifiers.map { id in let parts = id.split(separator: "|", maxSplits: 1) let fuel = FuelType(rawValue: String(parts.first ?? "")) ?? .e10 let stationID = String(parts.last ?? "") + // Id-only value (fresh widget's static default): resolve with + // zero keychain access. displayRepresentation falls back to a + // storage lookup if the name is empty, so stored picks still + // render their name. + guard !stationID.isEmpty else { + return WidgetFavourite(fuel: fuel, stationID: stationID, stationName: "") + } + let favourites = FuelStore.loadFavourites() let entry = favourites.first { $0.id == id } return WidgetFavourite( fuel: fuel, @@ -196,26 +207,6 @@ struct WidgetFavouriteQuery: EntityQuery { ) } } - - /// Fresh widget / unresolved value → auto-populate the Favourite row with - /// the FIRST favourite of the Fuel row's fuel (falling back to the first - /// favourite of any fuel if the fuel has none, e.g. mid-change). - func defaultResult() async -> WidgetFavourite? { - let favourites = FuelStore.loadFavourites() - guard !favourites.isEmpty else { return nil } - let entry: FavouriteEntry - if let fuel = smallIntent?.favouriteFuel.fuel, - let scoped = favourites.first(where: { $0.fuel == fuel }) { - entry = scoped - } else { - entry = favourites[0] - } - return WidgetFavourite( - fuel: entry.fuel, - stationID: entry.station.id, - stationName: entry.station.name - ) - } } // The knobs both the medium-list and small-single widget intents expose, so