Road-distance routing previously only fired once a fresh GPS fix arrived via
the location hook/onChange. A returning user on a cold launch waited for that
fix, and if onboarding/flag issues kept tracking from starting, distances
stayed straight-line. Now:
- refreshRoadDistancesIfNeeded() uses the last saved location when the live
fix isn't set yet, so it can run immediately.
- It's called explicitly on launch (already-onboarded branch), on foreground
activation, and on the existing location paths.
The completed flag lived only in app-group UserDefaults
(UserDefaults(suiteName: appGroupSuite)). On free SideStore accounts the
app-group container isn't provisioned, so the suite read as nil: save was a
silent no-op and load always returned false -> onboarding re-appeared on
every launch even after finishing and granting all permissions.
Switch to saveString/loadString (keychain-first, app-group mirror) under
onboardingCompletedKey, matching favourites/distance-unit persistence which
survives reinstall and works without a provisioning group. loadString falls
back to the app-group mirror, so anyone who previously completed on a
provisioned group keeps their flag; users on unprovisioned groups complete
once more and then it sticks.
Stations past the nearest-12 cutoff silently fell back to straight-line,
which explained the remaining app-vs-Apple-Maps gaps (e.g. 3.5 straight-line
vs 5.9 road). Raise the per-pass cap to 40 and route every station within a
25 km straight-line radius (covers the 15 mi max search window) so any station
the widget/Live Activity/list can show gets a real road distance.
Also harden the new cache tests against persistent keychain state across
invocations (reset in setUp) so they pass deterministically.
The widget/Live Activity showed straight-line haversine distance (0.8 mi)
while Apple Maps routes 1.8 mi. Road routing is too heavy for the widget
execution + ~40-70/day refresh budget, so the APP now computes it:
- New RoadDistanceService (app target): for the nearest 12 stations each
pass, calls MapKit MKDirections (free, no API key, matches Apple Maps)
and caches metres keyed by station ID.
- Cache stored in KEYCHAIN (fuelboard.roadDistances) so the widget reads it
even on free SideStore accounts with no app-group container; only valid
within 600 m of the location it was built from.
- Throttled: recompute max every 10 min, or when the user moves > 400 m;
wired into the location-update hook + location onChange.
- Widget face, app station rows + Live Activity show the cached road
distance, falling back to straight-line when absent.
- 4 new cache tests (105 total).
GeometryReader as the layout root pinned content top-left, and forcing
maxHeight:.infinity over-claimed the whole proposed height, centring the
content below true vertical centre (bigger gap above) on the Lock Screen.
Demote GeometryReader to a background side-channel that only measures the
slot width for the rich-vs-compact branch; the content stays intrinsic-sized
so the system vertically centres it itself.
User: 'Remove the white background circle from the live activity'. Drop
the explicit Circle badge and render the fuel-tinted fuelpump.fill on its
own (sized up 19 -> 28 since it no longer sits inside a disc).
fuelpump.circle.fill's first hierarchical style hits the PUMP, not the
circle, so the earlier darkened colour landed on the pump and the circle
stayed white (user: 'I didn't want the fuel pump darker I wanted the
white circle darker'). Rebuild the brand glyph from two explicit shapes:
a light-grey (.86) Circle badge + a bright fuel-tinted fuelpump.fill, so
circle vs pump colours are unambiguous and the badge centres cleanly with
the two-line text block.
Add a DEBUG-only -qaLiveActivity <fuel> launch hook that starts a Live
Activity with a long station name so the Lock Screen can be rendered in
the Simulator and verified by pixels (pump bright green 43,193,81; badge
grey 202,201,202).
A GeometryReader hands its content the full proposed frame and pins it
top-left, so the Lock Screen card sat high instead of vertically centred
when the activity container offered extra height. Wrap the branch in
.frame(maxWidth:.infinity, maxHeight:.infinity, alignment:.center).
Also darken the pump circle to 82% of the raw fuel tint (mix 18% black) so
the white pump symbol keeps contrast against bright Lock Screen/card, esp.
for the light yellow premium / cyan diesel palette colours.
Move FuelType.tintColor (green=unleaded #30D158, yellow=premium #FFD60A,
cyan=diesel #64D2FF) into Shared/FuelStore.swift so the app, widget, and
Live Activity all read one definition (was app-only in StationsView, so the
widget couldn't see it). Tint the Live Activity rich card pump icon and the
Dynamic Island compactLeading pump by context.state.fuel.tintColor.
ViewThatFits measures each child's IDEAL (untruncated) width to decide
fit, and a flexible .frame(maxWidth:.infinity) middle column does not cap
that ideal — so a long station name (e.g. 5-mi Phoenix Filling Stations)
inflated richBody's ideal past the iPhone Lock Screen width and fell back
to the compact strip. Replace the ViewThatFits branch with a
GeometryReader that reads the ACTUAL proposed width and shows the full
three-column card at/above 280pt, compact below. Content-independent, so a
long name can no longer collapse the iPhone card.
The compact CarPlay/Watch strip's second line had escaped interpolation
(\\( instead of \\() so it rendered the literal placeholder text
"context.state.stationName · context.state.distanceText" instead of the
actual station name and distance. Normalize to single-backslash interpolation.
Restore .supplementalActivityFamilies([.small]) + a ViewThatFits compact
fallback so CarPlay's small slot renders our shared compact strip (fuel type,
bold price, station · distance) instead of falling back to the Dynamic Island
compact closure, which showed the app name and omitted the distance the user
wants. Full iPhone/iPad card is preserved via the richMinWidth gate on
richBody plus its flexible, truncating middle column (long names can no longer
collapse it).
Long station names inflated richBody's IDEAL width, so ViewThatFits (which
measures ideal, untruncated text) decided 'rich doesn't fit' and fell back to
the compact strip — that's why 5-mi / long-named activities rendered small
while 10-15-mi / short names stayed full. With .supplementalActivityFamilies
already removed, the body is only ever Lock-Screen width, so drop ViewThatFits
and compactBody entirely and always render the full three-column card; the
station caption line-limits + scale-downs + tail-truncates in place.
The active activity was rendering the narrow .small form on the iPhone Lock
Screen (squeezed, small text) because .supplementalActivityFamilies([.small])
made iOS eligible to present it that way. Remove it so iPhone/iPad always get
the full three-column card; CarPlay's small form still comes from the Dynamic
Island compact closures. Keep compactBody as a narrow-width safety fallback.
The adaptive layout picks the full three-column view when there's Lock
Screen width and a compact price strip in the small CarPlay/Watch slot.
Tighten the discriminant (rich ≥290pt, compact ≤220pt) so iPhone/iPad
always get the full view and only genuinely small slots get the compact
strip. Long station names used to clip at inconsistent points across
the rich/compact/Dynamic Island views; both bodies now scale + tail-
truncate so the caption stays consistent regardless of station name.
SwiftUI's removal transition never actually played despite .id keying — the
tip banner vanished instantly on dismiss. Replace it with explicit opacity/
offset driven by withAnimation in reflectBanner (gated by a .task(id:
activeBanner) so presentation is reliable and prompt): a dismissal now
fades out + slides up over 0.35s, and a tip→network swap crossfades. Uses
the same state-driven pattern that fixed the HermesCall ticker fades.
The banner sits in a single if-let slot; without an identity key, clearing a
tip that was covering a network/offline banner swapped content in place with
no transition (instantly vanishing). Keying the banner by its value (.id)
makes every change — tip→network or tip→nil — run the shared removal
transition, so the tip slides up and fades out when it auto-dismisses or is
tapped.
- Banner floats at the very top (over the nav/title area), never pushing the
content below and staying clear of the list/pill.
- Offline banner now fires on refresh in airplane mode / no data: the mirror
live chain bypassed the HTTP cache (+10s timeout) so an offline fetch really
fails instead of silently re-serving a cached pointer+dump as a 'success'
(which kept dataStatus .live and hid the banner).
- Onboarding permissions fire at the Continue/Allow tap BEFORE advancing, so
the system prompt appears after the page is read and never covers the next
page's animation (grant auto-advances).
Position the floating banner just below the nav bar so it no longer overlays the iOS 26 Liquid Glass back/toolbar chrome behind it — tapping retry can no longer also select the control beneath. Banner now slides up + fades on dismiss/timeout (one style for all notifications); full-rect contentShape for a solid tap target.
Extract AppBanner; render offline/connection/tip through the single statusBannerCard chrome as a ZStack top overlay so it floats over content instead of shifting layout. Tip (auto-dismiss ~3.5s) takes precedence over network state.
Tip purchase results now ride the SAME banner chrome as the network/offline
strip above the tabs instead of a hard-to-see Settings footnote. TipStore is
lifted to ContentView (the banner owner) and shared down to Settings. Each
outcome maps to an icon/tint; auto-dismisses after ~3.5 s; tap to dismiss.
Renderer refactored into a shared statusBannerCard(icon:tint:title:subtitle:
trailingIcon:action:) used by both the network strip (retry) and tips (x).
A history fetch that comes back empty BECAUSE the mirror is unreachable
(pointer probe failed) was only an in-sheet retry card — no global signal.
Now it also raises the red connection banner ('Check your internet
connection / Tap to try again'), so a network problem is visible on every
tab, not just inside the Trends sheet. A successful load clears the
banner (only when the banner is the connection banner — never clobbers
the offline-dump banner).
- TrendsView: onHistoryUnavailable/onHistoryRecovered closures fired from
load() (loadFailed -> unavailable; hasAnyData -> recovered);
-forceHistoryFailure QA hook (forces the unreachable state, skips
auto-refresh like the other force-* hooks)
- FavouritesView: closures threaded through the Trends sheet init
- ContentView: wires them to dataStatus (.live -> .connectionProblem on
failure; recovered clears only .connectionProblem)
- fuelboard-development skill: hook + wiring documented
Fix 1 — the offline banner's subtitle claimed 'pull to refresh' while that
gesture only exists on the Stations tab, yet the banner pins across ALL
tabs. Copy now says where the gesture lives: 'Pull to refresh on the
Stations tab'. The banner itself stays tappable everywhere (tap = retry),
so the retry path is never trapped on one tab.
Fix 2 — fetch-failed-with-cache case showed stale saved prices with NO
feedback (looked like a working live app). New second banner state
'Check your internet connection / Tap to try again' (red wifi.exclamation
mark, tap = retry) now appears whenever the fetch fails but a saved cache
is on screen. It clears on the next successful fetch.
- ContentView: offlineDataDate:String? -> dataStatus enum (.live /
.offlineDump(date) / .connectionProblem) driving a shared statusBanner
(icon/tint/title/subtitle); -forceConnectionProblem QA launch hook;
auto-refresh skipped under either force-* hook
- Localizable.strings: subtitle key updated, +5 new keys (incl. date-less
'Offline data' fallback + a11y labels)
- Tests unchanged (101 green) — pure view-layer change
The no-network last resort (bundled real dump) now shows a tappable banner
above the tabs — 'Offline data from 15 Aug / Pull to refresh to update
prices' — so stale snapshot data is never presented as live. Banner date
comes from the dump's own data_updated stamp (FuelStore.offlineDataLabel,
ISO 8601 with/without fractional seconds). Cleared on any successful fetch.
- BundledDumpProvider.dataUpdatedStamp (JSONSerialization, app target)
- ContentView: offlineDataDate state, offlineBanner view (tip-card styling,
orange wifi.slash + arrow.clockwise, tap = force refresh), refresh()
clears/sets it, -forceOfflineDump QA launch hook (serves the dump and
skips auto-refresh so the banner stays up for captures)
- Localizable.strings: 3 keys (banner title/subtitle/a11y)
- +3 tests: OfflineDataLabelTests (101 total)
Legend rows now show each station's average over the days shown
in brackets (pence; signed +X.Xp in vs-cheapest mode, matching the
list-row deltas). Footer under the key explains the figure per mode.
averagePence helper (nil for empty series) + 3 tests; -showTrends
becomes a permanent QA hook like -showKeySheet.
LineMark lacked a series: value, so Swift Charts merged every
station's points into one polyline — only the first station's
line was recognisable. Adding series: per station splits the
chart into N coloured lines (legend colours now match).
Sheet grows from .medium to .fraction(0.6) so the footer ('Colours
match each station's rating on the list') clears the bottom edge with
comfortable margin (verified: footer clearance +33pt). Sheet background
switched from the translucent default to a solid
systemGroupedBackground via .presentationBackground so the stations
list no longer shows through.
The two bottom cards (TOP, star) previously had no padding — their
background hugged the text. Add .padding(.horizontal, 14).padding(.vertical, 12),
matching (slightly exceeding) the RAG cards' vertical padding so the
info cards read as proper cards.
Price-rating legend now renders as five accent cards (tip-section style):
Best value (green, checkmark) / Okay (orange, equal) / Pricey (red,
exclamation) each with a RAG dot + threshold (≤1.5p / ≤4p / >4p) on the
right in the row's accent colour; TOP (blue capsule) and star (yellow)
as accent cards below a divider. Footer: 'Colours match each station's
rating on the list'. Copy split into title/blurb keys in Localizable.
Sheet chrome (medium detent, Done, info button) unchanged.
Three stacked rounded cards, one per tier, each with an accent-tinted
fuel-pump icon and price (Splash & Dash #64D2FF blue, Half a Tank
#30D158 green, Fill 'Er Up #FFD60A amber — the app's fuel palette),
name + blurb on the left, bold accent price on the right. Cards sit on
clear list rows with their own rounded fill + subtle accent tint.
Purchase flow, per-tier live prices, disabled state and footer copy
unchanged.
Adds the three tip IAPs (tip099/tip299/tip499 at £0.99/£2.99/£4.99) to
the USER-owner ASC tasks, and the In-App Purchase capability to the
App ID blocker alongside the App Group.
Splash & Dash (£0.99), Half a Tank (£2.99), Fill 'Er Up (£4.99) —
each a consumable (com.apt.fuelboard.tip099/tip299/tip499) with its own
blurb row. TipStore now loads all three products, prices fall back to
the intended storefront values until ASC products exist, and the
Transaction.updates listener acknowledges any of the three tiers.
Footer copy: 'A tip helps keep this app free and supports development.
Thank you'.
The £4.99 consumable was never finished — StoreKit kept re-delivering
it through Transaction.updates on every launch. Verified purchases now
call transaction.finish() (direct path), and a Transaction.updates
listener finishes + acknowledges purchases that complete outside the
direct call (Ask to Buy approvals, other-device purchases on the same
Apple ID). Untrusted (unverified) transactions are never finished or
delivered; only our own product ID is handled.
- AppIconPreview now byte-identical to the real AppIcon.png (the preview
had been pointing at an older Design/ export); added AppIconPreviewDark
from AppIcon-dark.png so the mock shows the correct icon per appearance
- Re-captured 9-widgets-dark + 10-widgets-light
- 95/95 tests green; Release IPA rebuilt + re-served
- PreviewWallpaper image set: the actual iPhone wallpaper extracted from the
iOS 26.3 sim runtime (ActionButtonSelector Display.ca, 1320x2870 native
res, JPEG ~180 KB) — replaces the gradient backdrop in the widget mock
- -seedFavourite <substring> launch hook: pins the first matching station
for Unleaded via the normal keychain+app-group save path, so the
Favourites tab can be captured populated (Ring Road Service Station,
129.9, 59.1 mi, TOP badge)
- Re-captured 2-favourites (light), 6-favourites-dark, 9-widgets-dark,
10-widgets-light with real data
- 95/95 tests green; Release IPA rebuilt + re-served
- Small widget card sized to actual system-small content (158×158pt + padding)
instead of filling available width; medium keeps 338×158pt
- Wallpaper now mimics the iOS default (dark navy→black in dark mode,
soft blue-grey in light) via colorScheme-adaptive gradient
- Added 10-widgets-light.png; re-captured 9-widgets-dark.png with data
- 95/95 tests green; Release IPA rebuilt + re-served
- Move widget entry view into Shared/FuelPriceWidgetViews.swift (FuelPriceWidgetContent,
FuelPriceWidgetEntryData) so the app can render the REAL widget faces; widget
extension now wraps it (FuelPriceEntry.data) — one source of truth for the face
- WidgetMockScreen (-widgets launch arg): app icon + small + medium widget previews
for App Store captures; AppIconPreview image set (1024 drip master)
- AppStoreScreenshots: 5-8 dark-mode tab captures with real Halifax data, 9 widgets
- 95/95 tests green; Release IPA rebuilt + re-served
Raw tab captures for the yuzu-hub App Store Screenshot Generator:
stations, favourites, alerts, settings. Captured via simulator launch
args (-skipOnboarding / -tab).
b634c06 fired on page arrival, so swiping stacked Location + Notifications
before a screen was read. Now each permission fires only when the user
ADVANCES PAST its page (swipe forward or Continue) — the screen has been
read by then. Forward-only (swiping back never fires), idempotent
(already-determined → status refresh), so replay and auto-advance stay
clean. Continue buttons are now pure navigation; denied paths unchanged.
Swiping past the Location/Notifications pages never triggered the system
prompts — only tapping Continue did. The prompt now fires the moment its
page comes into view (onChange(of: page)): swipe, auto-advance after a
grant, or Continue all land on the page and request the permission. Both
request methods are idempotent (already-determined → status refresh, no
duplicate alert), so replay and the auto-advance path stay clean.
The N/TOTAL stations-updated explainer now sits in a plain caption Section
cell at the bottom of the tab (font .caption, secondary) — matching the
Favourites tab's bottom callout instead of rendering as a section footer.
The header section was removed entirely — the tab now opens with Fuel type.
The explainer lives as a section footer at the bottom of the tab:
- Cheapest: 'Cheapest Unleaded within 5 miles — 46/8022 stations updated ·
tap a station for directions.'
- Closest: 'Closest Unleaded stations — nearest first, best value within
5 miles · 46/8022 stations updated'
- no-location variant keeps mode + tap-for-directions hint
Numerator = stations in the current list pool (as before); denominator =
full UK station total from the last chain fetch (loadStationCount, 8,022
mirror snapshot) — ratio hidden until the first fetch lands.
The LAN relay is unreachable from any phone outside the developer's LAN, so
for App Store users a relay fallback was dead weight AND harmful: attempting
the private IP fires the iOS Local Network prompt on a stranger's phone and
adds a timeout before the bundled dump. GitHub raw is reachable anywhere the
relay would be, and more reliably.
- LiveChainProvider: GitHub mirror first EVERYWHERE (focused alert path no
longer relay-first); LAN relay only joins behind the hidden dev flag
fuelboard.relayFallback (Settings → Debug, keychain-first, off by default)
- MirrorFuelProvider: focused() projection replaces the relay's server-side
radius endpoint (fuel filter + within-radius + nearest-first + limit 500)
— the alert path re-checks radius/fuel itself, so behaviour is identical
- Widget: fetchFocused/fetchFuelOnly now GitHub-first via the chain (day-cache
shared through the app group); relay + widget-diag GET gated on the same
dev flag — off-LAN widgets no longer attempt local network at all
- FuelBeacon: fires only when the dev flag is on — consumers make zero
local-network attempts; /stats app-hit spike = GitHub down + dev flag on
- Onboarding: Local Network machinery removed entirely (no probe, no prompt,
no 12 s stall) — data page is now informational: prices download from the
internet, no permissions needed. Replay on cellular can no longer hang.
- Info.plist: NSLocalNetworkUsageDescription copy → optional dev-only wording
- 95 tests (6 new: relay-skip-when-disabled x2, focused prefer-mirror, mirror
focused projection x3, beacon dev-flag gate)
- MirrorFuelProvider: fetches CURRENT prices from raw.githubusercontent
(latest.json pointer → history/<day>.json full dump), app-group day-cache
so the ~2.8 MB dump is re-downloaded only when the mirror pushes a new day
- LiveChainProvider: full path (app refresh) GitHub → relay; focused path
(background alert checks) relay-first so alerts never pull the full dump
over mobile data; records which leg served for About + telemetry
- FuelBeacon: fire-and-forget X-Client app ping to the relay's existing
widget-diag route (zero relay changes) — attribution + cadence on-LAN,
silent skip off-LAN is the reachability datum; /stats app-hit spike =
GitHub path failing
- Bundled dump: FuelBoardDump dataset (real 2026-08-15 snapshot, 8,022
stations) + BundledDumpProvider + scripts/refresh_bundled_dump.sh —
no-network last resort replaces the demo sample set
- ContentView: About meta from the chain; catch falls back cached → bundled
→ sample
- 89 tests (6 new: chain order x4, cache decision, beacon URL); Release
build green; beacon route verified against the live relay (204 + logged)
The no-data states previously floated or centred vertically (Spacer
sandwich), so the sheet rendered them halfway down the page while the
chart-with-data anchors to the top. All chartArea states now share one
top-aligned container that fills the space below the pickers — the
empty state sits exactly where the chart will render, and the layout no
longer jumps between states. 83 tests, Release build green.
- AppIcon / AppIcon-dark / AppIcon-tinted swapped for the drip design
(1024px, dark + tinted variants), onboarding artwork matched
- Alerts tab: live explainer under the Radius picker — situational
guidance per option (Follow search = set-and-forget mirror of the
Stations distance; 1 = town driving; 2 = city/suburb commute;
3 = mixed; 5 = A-roads/motorway exits; 8 = long-distance) — plus the
trade-off line in the section footer
- 83 tests, Release build green
Settings → Debug → 'Debug fence (delivery test)': registers a 100 m
CLCircularRegion at the current fix that exists solely to prove iOS
delivers region events on this install. Entering it ticks 'Region events
this session', logs 'debug fence entered' in the Alert trace, and fires a
plain notification — zero alert gates. Cleared via button; survives the
station re-registration loop (update() now skips debug-fence regions);
cleared when alerts are disabled.
The Test alert / Plain test buttons now also write to the Alert trace
('test alert scheduled — …'), so the trace reflects every probe, not just
live geofence entries. 83 tests, Release build green.
ProximityMonitor now records each stage the live chain reaches (Settings →
Debug → Alert trace, in-memory, newest-first, capped at 8):
- didEnterRegion arrival (regionEventCount + entry line) — distinguishes
'geofence never fires' from 'gate blocked the alert'
- gate misses: station not in loaded list, hourly dedup (with minutes ago),
no fuel sellers within radius after fresh fetch, entered-not-cheapest
(with the cheaper station + price)
- the previously-silent fresh-fetch catch now logs the error
- 'alert scheduled' once UNUserNotificationCenter.add is called — if that
line appears but no banner shows, the failure is delivery/permission,
not alert logic
SettingsView shows the trace (symbol per kind) + region-event count; the
existing Test-alert button already probes notification permission and
reports denial inline. 83 tests, Release build green.
- Shared/FuelHistory.swift: GitHub mirror read path (latest.json pointer +
history/YYYY-MM-DD.json day files); day math (UTC noon keys), slim day decode
with the shared price band, series + deltaSeries (vs-cheapest rebase),
favourites-only app-group cache (90-day prune), parallel per-day fetches,
404/network = graph gap never error.
- FuelPriceProvider: shared priceBand + mapGrades (single source for live +
history decoding).
- FuelBoard/TrendsView.swift: line chart in the Favourites tab via toolbar
button + sheet; fuel capsule (fuels with favourites only), 7/30/90 range,
Price/vs-cheapest toggle, per-station legend, price-display style on y-axis,
empty/loading/retry states with honest copy; no widget in v1.
- Localizable.strings: Trends keys.
- 83 tests (13 history + URL regression): day math, band guard, series gaps,
delta rebasing, prune, and the appendingPathComponent regression (relative
URL resolution dropped /main — all fetches 404'd).
- Sim-verified: favourites rows + TOP/deltas; sheet controls + building-up
state with live first-snapshot date (archive has 1 day; lines render once
>=2 snapshots).
- Backlog: P1 Trends DONE (unmerged); P0 REMAINING = live provider chain +
telemetry beacon.
The iOS 18+ long-press app-icon menu (turn icon into a widget) shows
the supported SIZES of the FIRST-registered widget kind only — it
does not list widget kinds and is not keyed by intent type (two
distinct intents in 6375af4 changed nothing on-device). The bundle's
first kind (FuelPriceSmallWidget, small-first since 38b47ee) therefore
decided the menu offered only the small tile.
The early build worked because FuelPriceWidget was registered first
back then. Reordering to medium-first would fix the menu but flip the
gallery order (38b47ee). Instead: this first kind now supports BOTH
.systemSmall and .systemMedium, so the icon menu offers both size
tiles (ChatGPT/Safari behave the same way — their first kind supports
several families). The gallery still lists both widget kinds in bundle
order; the Small entry just gains a medium variant. The shared view
already branches on family, so medium renders the station list.
iOS keys the long-press app-icon contextual menu by configuration-intent
TYPE, so sharing one intent across kinds collapsed the menu to the
first-registered kind (small) — the medium 'FuelBoard Prices' vanished
from the icon menu while the + gallery (kind-keyed) still listed both.
dafc96f's skeleton fix was never the problem; the extra favourite
AppEntity params were.
FIX: FuelBoardSmallWidgetConfigurationIntent — a distinct intent TYPE
with the IDENTICAL minimal parameter set (fuel/sort/distance only, no
AppEntity params). Fresh-widget default-config resolution stays intact;
the appex metadata now registers two WidgetConfiguration actions.
Verified: build green, 45/45 tests, extract.actionsdata lists both
intents, fresh IPA packaged + signed per component.
The welcome page's green-gradient circle + SF symbol is replaced by the
app icon itself (AppIconArtwork imageset mirroring AppIcon.png), clipped
to the standard iOS rounded-rect corner radius (21pt at 96pt — ~22%),
so onboarding matches the Home Screen icon.
New Icon Composer project artwork (gas-station vector, layered + background)
rendered via ictool at 1024pt: Default / Dark / TintedLight renditions
replacing all three asset-catalog slots.