App Store prep: privacy manifests, 1.0.1 (2), OGL attribution, UK empty state, screenshot harness

- PrivacyInfo.xcprivacy in app + widget targets (UserDefaults 1C8F.1;
  app declares Location linked + Diagnostics not-linked, no tracking)
- Version bump 1.0.1 (2) in App + Widget plists
- Settings → Data source footer: OGL attribution + 'refresh up to twice a day'
- Stations empty state: friendly UK-only explainer + 'Browse all UK stations'
- ContentView launch-arg harness (pattern of -showKeySheet): -skipOnboarding
  and -tab stations|favourites|alerts|settings for screenshot capture
- APPSTORE.md updated (blockers struck, tasks done, yuzu-hub workflow)
- BACKLOG.md hygiene: GitHub-first header, Trends merged note, offline-first-run DONE
This commit is contained in:
FuelBoard Contributor
2026-08-15 15:50:26 +01:00
parent 06dd8a919c
commit b5a49fc68d
302 changed files with 200 additions and 43 deletions
+24 -17
View File
@@ -24,9 +24,8 @@ The two 2026-08-11 blockers are RESOLVED by P0 (live chain):
2. **[USER] Register App ID `com.apt.fuelboard`** in the developer portal,
with the **App Group `group.com.apt.fuelboard`** capability enabled (the
widget + app-group cache depend on it).
3. **[ME] `PrivacyInfo.xcprivacy`** — REQUIRED for all submissions since
May 2024; the repo has none. Add to the app target (Xcode can generate;
must declare location-data reasons + any required-reason APIs used).
3. ~~[ME] `PrivacyInfo.xcprivacy`~~**DONE 2026-08-16** (app + widget
manifests added; UserDefaults `1C8F.1`, Location + Diagnostics declared).
4. **[ME] Real Distribution signing** — Distribution certificate + App Store
provisioning profile (or Xcode automatic signing once the account is in
Xcode); then `xcodebuild archive` (no more `CODE_SIGNING_ALLOWED=NO`).
@@ -35,21 +34,29 @@ The two 2026-08-11 blockers are RESOLVED by P0 (live chain):
## Required tasks — owner [ME] (technical, do after account exists)
- [ ] PrivacyInfo.xcprivacy (blocker #3 above)
- [ ] Distribution archive + TestFlight upload
- [ ] Bump version: `CFBundleShortVersionString 1.0.1` / `CFBundleVersion 2`
- [ ] OGL attribution + "prices updated twice daily" footnote in
Settings → About (2.3.1 hygiene; data is 12 h-cache, UI says "cheapest")
- [ ] Out-of-UK empty state: reviewer may not be in the UK → friendlier copy
than "No X stations found." (explain UK-only data + browse-all option)
- [ ] Optional hardening: gate the dev-only surface (Settings Debug section,
`fuelboard.relayFallback`, FuelBeacon, relay IP) behind `#if DEBUG` so
the Release binary ships clean — currently Release contains the hidden
dev flag + a private IP string (no credentials, but a "test build"
smell; not a rejection risk on its own)
- [x] **PrivacyInfo.xcprivacy** — app + widget manifests added (2026-08-16):
UserDefaults reason `1C8F.1`; app also declares Location (linked, not
tracking) + Diagnostics (not linked, not tracking — the dev beacon)
- [ ] Distribution archive + TestFlight upload (needs Apple account)
- [x] **Version bump**`1.0.1` (build 2), app + widget plists
- [x] **OGL attribution** — Settings → Data source footer now reads "Prices
use the UK Fuel Finder dataset (Open Government Licence) and refresh
up to twice a day." (2.3.1 hygiene)
- [x] **Out-of-UK empty state** — "No X stations within N miles" + UK-only
explainer + "Browse all UK stations" button (switches to Closest)
- [ ] ~~Gate dev surface behind #if DEBUG~~ **DECLINED — kept in Release on
purpose**: the user's home-testing installs the Release IPA and toggles
`fuelboard.relayFallback` via Settings → Debug (revealed only by tapping
About → Version ×5 — invisible to reviewers; no credentials involved).
Gating it out of Release would break that workflow for zero review gain.
- [ ] Final pass: `swift test` green (95 tests), archive smoke test on device
- [ ] Capture screenshots from simulator (6.7″ 1290×2796 required; 6.5″
1242×2688 recommended) for App Store Connect
- [ ] ~~Capture screenshots from simulator~~ **RAW CAPTURES DONE (2026-08-16)**
native 6.9″ 1320×2868 PNGs of all four tabs (Stations, Favourites,
Alerts, Settings) at `/tmp/fuelboard-shots/asc/`; assembled into ASC
frames by the user in the yuzu-hub App Store Screenshot Generator
(https://yuzu-hub.github.io/appscreen/). Screenshot harness: launch
args `-skipOnboarding` / `-tab stations|favourites|alerts|settings`
(ContentView, same pattern as `-showKeySheet`).
## Required tasks — owner [USER] (App Store Connect, cannot be done by me)
+12 -10
View File
@@ -1,8 +1,10 @@
# FuelBoard Backlog
FuelBoard — England-wide petrol-station price widget + app.
Relay mirrors the UK gov data (hourly CSV); app fetches the full-UK dump
(~8,038 stations, all prices) once per 12 h and filters/sorts on-device.
GitHub mirror (`aptonline/fuelboard-data`, daily) holds the full-UK dump
(~8,022 stations, all prices); app fetches it once per 12 h and filters/
sorts on-device, with a bundled snapshot as the no-network last resort.
LAN relay is dev-only (hidden `fuelboard.relayFallback` flag).
Priority guide: P0 = next build, P1 = soon, P2 = later, P3 = maybe/idea.
Status: TODO / IN PROGRESS / DONE / BLOCKED.
@@ -15,9 +17,8 @@ Status: TODO / IN PROGRESS / DONE / BLOCKED.
(App Intents, iOS 16+), dialog + price-card snippet, cached full-UK dump first
with freshness label ("as of 6am"), last-known-location fallback. Constraint:
fresh-data path needs a reachable source off-LAN — cached answers work anywhere
the app has previously fetched (see P3 data mirror). Interim: the relay on
Tailscale (100.120.217.98:8789) is reachable off-LAN for testing; relay will
move to a permanent box or the GitHub mirror long-term. *DONE — radius-scoped
the app has previously fetched (the GitHub mirror serves this now). *DONE —
radius-scoped
lookup `037c241` (real-data regression: GULF HISTON 100.9p 132 mi away),
per-fuel literal-word shortcuts `e55078c`, names = app words `a3b6f37`, plus
Directions-to-cheapest `caadf18` + spoken hand-off `aed99e3`.*
@@ -31,7 +32,7 @@ Status: TODO / IN PROGRESS / DONE / BLOCKED.
capsule fuel picker; range control 7/30/90 days; respects the price-display
toggle; honest empty-state copy in Localizable.strings ("Prices are recorded from
each refresh — check back in a few days"). No widget in v1 (one-kind rule).
Depends on the P0 archive. *DONE 2026-08-15 on feature/trends-history (unmerged):
Depends on the P0 archive. *DONE 2026-08-15, merged to main 2026-08-16:
`Shared/FuelHistory.swift` (FuelHistoryStore: day math, slim day decode w/ shared
band, series + deltaSeries, app-group favourites-only cache, parallel per-day
fetches) + `FuelBoard/TrendsView.swift` (chart, fuel capsule, 7/30/90, Price/vs
@@ -39,8 +40,8 @@ Status: TODO / IN PROGRESS / DONE / BLOCKED.
FavouritesView + strings. 83 tests incl. URL regression (appendingPathComponent
— URL(string:relativeTo:) dropped /main, all fetches 404'd). Sim-verified:
favourites rows + TOP/deltas; sheet controls + honest empty state w/ live first-
snapshot date. Pending: merge to main after device sideload test; lines render
once ≥2 snapshots (archive warming daily).*
snapshot date. Pending: lines render once ≥2 snapshots (archive warming daily,
~Aug 17).*
## P2 — Later
@@ -82,8 +83,9 @@ Status: TODO / IN PROGRESS / DONE / BLOCKED.
user-configurable (currently fixed 300 m trigger / 1 per station per hour).
- [ ] **Pull-to-refresh spinner state** — surface refresh in-flight state
(currently `refreshable` fires but no visible progress in the row list).
- [ ] **Offline first-run**first launch without network currently shows empty
states; bundle a sample dump so the app demos offline.
- [x] **Offline first-run**bundled REAL 8,022-station dump (`FuelBoardDump`,
refreshed by `scripts/refresh_bundled_dump.sh`) is the no-network last resort
in the live chain (P0, 2026-08-16); first launch demos the full dataset offline.
- [ ] **Localization** — UK English only today; audit string catalog for l10n.
- [ ] **Dark mode polish** — verify brand logos (white-circle clips) and RAG dot
contrast under dark appearance on-device.
+7 -7
View File
@@ -16,6 +16,8 @@
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0.1</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
@@ -27,18 +29,16 @@
</array>
</dict>
</array>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<string>2</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSLocationWhenInUseUsageDescription</key>
<string>FuelBoard uses your location to find the cheapest nearby petrol stations.</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>FuelBoard uses Always location to alert you when you approach the cheapest station nearby.</string>
<key>NSLocalNetworkUsageDescription</key>
<string>FuelBoard can optionally connect to the FuelBoard Relay on your local network for faster alerts during development.</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>FuelBoard uses Always location to alert you when you approach the cheapest station nearby.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>FuelBoard uses your location to find the cheapest nearby petrol stations.</string>
<key>NSSupportsLiveActivities</key>
<true/>
<key>UIBackgroundModes</key>
+4 -4
View File
@@ -17,15 +17,15 @@
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<string>1.0.1</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>FuelBoard uses your location to show the cheapest nearby petrol stations.</string>
<string>2</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.widgetkit-extension</string>
</dict>
<key>NSLocationWhenInUseUsageDescription</key>
<string>FuelBoard uses your location to show the cheapest nearby petrol stations.</string>
</dict>
</plist>
+24 -2
View File
@@ -33,6 +33,7 @@ struct ContentView: View {
@State private var isLoading = false
@State private var statusMessage = ""
@State private var showOnboarding = false
@State private var selectedTab = 0
@State private var locationManager = LocationManager()
@StateObject private var monitor = ProximityMonitor()
@@ -137,12 +138,14 @@ struct ContentView: View {
}
var body: some View {
TabView {
TabView(selection: $selectedTab) {
stationsTab
.tabItem { Label("Stations", systemImage: "fuelpump.fill") }
.tag(0)
favouritesTab
.tabItem { Label("Favourites", systemImage: "star.fill") }
.tag(1)
AlertsView(
enabled: $alertsEnabled,
@@ -159,8 +162,10 @@ struct ContentView: View {
lastAlert: monitor.lastAlert
)
.tabItem { Label("Alerts", systemImage: "bell.fill") }
.tag(2)
settingsTab
.tag(3)
}
.fullScreenCover(isPresented: $showOnboarding) {
OnboardingView {
@@ -171,11 +176,28 @@ struct ContentView: View {
StationMapView(request: request)
}
.onAppear {
// Launch-arg hooks for the screenshot/UI-test harness (same
// pattern as StationsView's `-showKeySheet`): `-tab <name>`
// opens the given tab; `-skipOnboarding` skips onboarding
// without touching the stored flag.
let args = ProcessInfo.processInfo.arguments
if let i = args.firstIndex(of: "-tab"), i + 1 < args.count {
switch args[i + 1] {
case "favourites": selectedTab = 1
case "alerts": selectedTab = 2
case "settings": selectedTab = 3
default: selectedTab = 0
}
}
// Onboarding runs first on a fresh install it owns the initial
// permission prompts (location, notifications, and the data/local
// network probe on the Data page). Location tracking and the first
// network fetch start once it's done.
if FuelStore.loadHasCompletedOnboarding() {
// Launch-arg hook (UI-testing/screenshot harness, same pattern as
// StationsView's `-showKeySheet`): skip onboarding without
// touching the stored flag.
if FuelStore.loadHasCompletedOnboarding()
|| args.contains("-skipOnboarding") {
locationManager.startForegroundTracking()
// Geofences must follow the user even in the background:
// wire the delegate hook (fires on every fix incl. background
+40
View File
@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyTracking</key>
<false/>
<key>NSPrivacyTrackingDomains</key>
<array/>
<key>NSPrivacyCollectedDataTypes</key>
<array>
<dict>
<key>NSPrivacyCollectedDataType</key>
<string>NSPrivacyCollectedDataTypeLocation</string>
<key>NSPrivacyCollectedDataTypeLinked</key>
<true/>
<key>NSPrivacyCollectedDataTypeTracking</key>
<false/>
</dict>
<dict>
<key>NSPrivacyCollectedDataType</key>
<string>NSPrivacyCollectedDataTypeDiagnostics</string>
<key>NSPrivacyCollectedDataTypeLinked</key>
<false/>
<key>NSPrivacyCollectedDataTypeTracking</key>
<false/>
</dict>
</array>
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>1C8F.1</string>
</array>
</dict>
</array>
</dict>
</plist>
+1 -1
View File
@@ -229,7 +229,7 @@ struct SettingsView: View {
} header: {
Text("Data source")
} footer: {
Text("Connection shows whether prices come from the official Fuel Finder API or the CSV mirror. Data updated is the latest price change reported by the GOV.UK server itself.")
Text("Connection shows whether prices come from the official Fuel Finder API or the CSV mirror. Data updated is the latest price change reported by the GOV.UK server itself. Prices use the UK Fuel Finder dataset (Open Government Licence) and refresh up to twice a day.")
}
Section {
+13 -1
View File
@@ -101,8 +101,20 @@ struct StationsView: View {
Text("Fetching prices…")
}
} else if stations.isEmpty {
Text("No \(selectedFuel.displayName) stations found.")
let miles = distanceUnit.displayMiles(stationLimit)
let unit = distanceUnit.label(for: Double(miles))
VStack(alignment: .leading, spacing: 6) {
Text("No \(selectedFuel.displayName) stations within \(miles) \(unit).")
.foregroundStyle(.secondary)
Text("FuelBoard covers England, Scotland and Wales — try Closest to browse every UK station.")
.font(.caption)
.foregroundStyle(.secondary)
Button("Browse all UK stations") {
sortMode = .closest
}
.font(.caption)
}
.padding(.vertical, 4)
} else {
ForEach(Array(stations.prefix(visibleCount)), id: \.id) { station in
StationRow(
+23
View File
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyTracking</key>
<false/>
<key>NSPrivacyTrackingDomains</key>
<array/>
<key>NSPrivacyCollectedDataTypes</key>
<array/>
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>1C8F.1</string>
</array>
</dict>
</array>
</dict>
</plist>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>logFormatVersion</key>
<integer>12</integer>
<key>logs</key>
<dict/>
</dict>
</plist>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>logFormatVersion</key>
<integer>12</integer>
<key>logs</key>
<dict/>
</dict>
</plist>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>logFormatVersion</key>
<integer>12</integer>
<key>logs</key>
<dict/>
</dict>
</plist>
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>logFormatVersion</key>
<integer>12</integer>
<key>logs</key>
<dict/>
</dict>
</plist>

Some files were not shown because too many files have changed in this diff Show More