Fix small widget skeleton: use the proven medium intent; delete failing small intent

ROOT CAUSE (proven by A/B test on-device): the small widget's custom
intent (FuelBoardSmallWidgetConfigurationIntent with favouriteFuel +
favourite AppEntity params) fails fresh-widget default-config resolution
at the system level — the whole default config fails, no timeline is
ever requested, makeEntry never runs, widget stays on the skeleton.
A minimal small widget sharing the MEDIUM intent (fuel/sort/distance
only) populated immediately in the same extension on the same device.
Neither removing defaultResult() (4d35b08) nor the
@IntentParameterDependency (8a9e775) helped — the extra entity params
themselves break resolution.

FIX: FuelPriceSmallWidget now uses FuelBoardWidgetConfigurationIntent
exactly like the working test widget. FuelBoardSmallWidgetConfiguration
Intent, FavouriteFuel(+Query), WidgetFavourite(+Query) deleted; the
pinned-favourite picker is gone (approved tradeoff). Favourites sort
still works on both faces via the list branch — the small face renders
the cheapest favourite of the chosen fuel. Beacon tag distinguishes the
small widget in diagnostics; test widget removed, gallery order
restored (small, medium, live activity).
This commit is contained in:
FuelBoard Contributor
2026-08-13 20:43:15 +01:00
parent 553730681f
commit dafc96f845
4 changed files with 22 additions and 289 deletions
+20 -82
View File
@@ -6,9 +6,9 @@ import AppIntents
// FuelPriceWidget (medium): top 3 stations with price + distance, each row
// opens Maps. Per-widget fuel + sort (+ distance for Cheapest).
// FuelPriceSmallWidget (small): single station, whole widget opens Maps.
// Same knobs, plus a Favourite picker when Sort = Favourites (a small face
// shows ONE station, so you choose which favourite to pin; the medium face
// lists all favourites and has no picker).
// Same knobs as the medium the small face shows the first of the
// configured result (cheapest/closest station, or the cheapest favourite
// of the chosen fuel in Favourites mode).
// Taps deep-link to Apple Maps directions (maps://?daddr=). On the Home
// Screen the system either opens Maps directly or delivers the URL to
// FuelBoard, whose onOpenURL forwards it (and also still handles legacy
@@ -60,46 +60,22 @@ struct FuelPriceWidget: Widget {
}
}
// Small widget exactly ONE station. Favourites sort gains a picker to choose
// WHICH favourite to pin, because a small face can show only one (the medium
// face lists them all, so the picker exists only here). Separate kind keeps
// the extra parameter off the list widget's Edit-Widget sheet.
// Small widget exactly ONE station (the first of the configured result).
// Shares the medium widget's intent: the small intent with extra favourite
// AppEntity params failed fresh-widget default-config resolution at the
// system level (whole default config failed no timeline skeleton; proven
// by an A/B test where a small widget using this intent populated while the
// custom-intent small widget never even reached makeEntry). Favourites sort
// on the small face shows the cheapest favourite of the chosen fuel.
struct FuelPriceSmallWidget: Widget {
let kind = "FuelPriceWidgetSmall"
var body: some WidgetConfiguration {
AppIntentConfiguration(
kind: kind,
intent: FuelBoardSmallWidgetConfigurationIntent.self,
provider: FuelPriceTimelineProvider<FuelBoardSmallWidgetConfigurationIntent>()
) { entry in
FuelPriceWidgetView(entry: entry)
.containerBackground(for: .widget) {
Color(.systemBackground)
}
}
.configurationDisplayName("FuelBoard Favourite")
.description("One pinned favourite station with its price.")
.supportedFamilies([.systemSmall])
.disfavoredLocations([.carPlay], for: [.systemSmall])
}
}
// A/B TEST widget (temporary): a from-scratch SMALL widget driven by the
// PROVEN medium intent no favourite params, no dependency, no custom
// query. If this populates while FuelPriceSmallWidget skeletons, the small
// SIZE is fine and the small intent's extra machinery is the culprit. If it
// ALSO skeletons, the failure is at the small-family/system level. Removed
// once the root cause is confirmed.
struct FuelBoardTestSmallWidget: Widget {
let kind = "FuelBoardTestSmallWidget"
var body: some WidgetConfiguration {
AppIntentConfiguration(
kind: kind,
intent: FuelBoardWidgetConfigurationIntent.self,
provider: FuelPriceTimelineProvider<FuelBoardWidgetConfigurationIntent>(
beaconTag: "FuelBoardTestSmallWidget"
beaconTag: "FuelBoardSmallWidget"
)
) { entry in
FuelPriceWidgetView(entry: entry)
@@ -107,8 +83,8 @@ struct FuelBoardTestSmallWidget: Widget {
Color(.systemBackground)
}
}
.configurationDisplayName("FuelBoard Small Test")
.description("A/B test: minimal small widget using the proven medium intent.")
.configurationDisplayName("FuelBoard Small")
.description("Fuel prices near you. Configure fuel + sort per widget.")
.supportedFamilies([.systemSmall])
.disfavoredLocations([.carPlay], for: [.systemSmall])
}
@@ -239,54 +215,16 @@ struct FuelPriceTimelineProvider<Configuration: WidgetConfigurationIntent & Widg
let favourites = FuelStore.loadFavourites()
.filter { $0.station.prices[$0.fuel] != nil }
// Small widget, pinned favourite: exactly ONE station, its own
// fuel, no radius. Freshen its price when a focused fetch covers
// it. The chosen favourite wins; when it's missing or stale (fresh
// widget, favourite removed, fuel row changed) fall back to the
// favourites fuel's first favourite so the face is never blank.
if let chosen = configuration.favouriteChoice {
let favouriteFuelType = configuration.favouriteFuelChoice?.fuel ?? fuel
// Fresh widget / static default (empty pinned favourite):
// auto-populate to the FIRST favourite of the fuel. This is
// the sheet's old defaultResult() auto-populate, moved to
// entry time so default-config resolution stays keychain-free
// (storage-backed defaultResult() stranding fresh small
// widgets on the skeleton placeholder).
let pinned: FavouriteEntry?
if chosen.stationID.isEmpty {
pinned = favourites
.filter { $0.fuel == favouriteFuelType && $0.station.prices[favouriteFuelType] != nil }
.first
} else {
pinned = favourites.first { $0.id == chosen.id }
?? favourites
.filter { $0.fuel == favouriteFuelType && $0.station.prices[favouriteFuelType] != nil }
.min { lhs, rhs in
(lhs.station.prices[favouriteFuelType] ?? .infinity) <
(rhs.station.prices[favouriteFuelType] ?? .infinity)
}
}
if let pinned {
var station = pinned.station
if let location,
let fetched = await Self.fetchFocused(near: location, fuel: pinned.fuel, radiusKM: radiusKM),
let fresh = fetched.first(where: { $0.id == pinned.station.id }) {
station = fresh
}
return FuelPriceEntry(
date: Date(), stations: [station],
fuel: pinned.fuel, sort: .cheapest, isFavourites: true,
location: location, locationSource: source, unit: unit
)
}
}
// List mode (medium): pinned stations for THIS fuel, cheapest-first.
// Not radius-bound a favourite in Edinburgh shows on a widget in
// Pinned stations for THIS fuel, cheapest-first. Not
// radius-bound a favourite in Edinburgh shows on a widget in
// London. Prices come from the keychain snapshot (the app
// refreshes favourite prices into keychain after every fetch), or
// fresher from a focused relay fetch when a favourite happens to
// be in range.
// be in range. The small face renders the first of this list
// the cheapest favourite of the chosen fuel. (The old per-widget
// pinned-favourite picker was removed: its AppEntity params made
// fresh-widget default-config resolution fail at the system level
// see FuelPriceSmallWidget.)
let fuelFavourites = favourites
.filter { $0.fuel == fuel && $0.station.prices[fuel] != nil }
.map(\.station)