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 -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 {
+14 -2
View File
@@ -101,8 +101,20 @@ struct StationsView: View {
Text("Fetching prices…")
}
} else if stations.isEmpty {
Text("No \(selectedFuel.displayName) stations found.")
.foregroundStyle(.secondary)
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(