Compare commits
@@ -1,5 +1,9 @@
|
||||
build/
|
||||
Builds/
|
||||
build-*/
|
||||
build-release/
|
||||
build-sim/
|
||||
build-watch/
|
||||
.DS_Store
|
||||
*.xcuserstate
|
||||
xcuserdata/
|
||||
|
||||
@@ -1,86 +1,231 @@
|
||||
# FuelBoard App Store Approval Review
|
||||
# FuelBoard App Store Submission Plan
|
||||
|
||||
Review date: 2026-08-11 · Target: App Store submission readiness.
|
||||
This is the companion to SECURITY.md — focuses on App Store *review* gate
|
||||
(guideline compliance), not code security.
|
||||
Updated: 2026-09-26 · Target: safe App Store submission.
|
||||
Companion to SECURITY.md (code security) — this file is the *review-gate*
|
||||
checklist: what must happen, who owns it, and the timeline.
|
||||
|
||||
## Status: NOT SUBMISSION-READY
|
||||
## Status: SUBMITTABLE after the checklist below
|
||||
|
||||
Two blockers remain before a real submission (see below). Everything else
|
||||
checks out for review.
|
||||
The two 2026-08-11 blockers are RESOLVED by P0 (live chain):
|
||||
- ~~LAN relay data source~~ → **app fetches from public HTTPS
|
||||
(raw.githubusercontent.com mirror) with a bundled real 8,022-station dump
|
||||
as the no-network fallback.** No private IP in the consumer path. Reviewers
|
||||
on Apple's network see real data.
|
||||
- ~~Always-location risk~~ → **Always is requested only when the user enables
|
||||
Alerts** (ProximityMonitor escalates WhenInUse → Always on alert opt-in).
|
||||
Onboarding requests WhenInUse only. FuelBoard does **not** declare the
|
||||
persistent real-time background location mode; optional alerts use
|
||||
CoreLocation region monitoring/significant-change behaviour instead.
|
||||
|
||||
## Blockers
|
||||
## StoreKit sandbox: verified working configuration and caveats
|
||||
|
||||
### 1. Data source — LAN relay (BLOCKER)
|
||||
- The app fetches from `http://192.168.1.131:8788` — a private LAN host. Off
|
||||
the user's network it fails and falls back to sample data. Reviewers on
|
||||
Apple's network will see empty/sample results → 2.1 (performance) rejection,
|
||||
and hardcoded IPs look like test builds.
|
||||
- **Fix**: the govuk API switch (BACKLOG P0). App must talk to a public HTTPS
|
||||
endpoint before submission. The relay (HTTPS domain) or direct gov.uk OAuth
|
||||
both work; the code path is already stubbed.
|
||||
### Verified configuration
|
||||
|
||||
### 2. Always-location justification (BLOCKER without review notes)
|
||||
- `NSLocationAlwaysAndWhenInUseUsageDescription` + `UIBackgroundModes: location`
|
||||
present, but guideline 5.1.1 requires the feature to be obvious AND the app
|
||||
must visibly use background location. The geofenced "cheapest station" alerts
|
||||
satisfy this, but the reviewer must see it:
|
||||
- Provide detailed App Review notes explaining the alert flow (enable Alerts →
|
||||
geofence trigger → notification when approaching the cheapest station).
|
||||
- Ensure the Alerts tab is discoverable and functional in review.
|
||||
- Also: "Always" prompt should only appear after the user opts into alerts
|
||||
(in-progress UX; currently the permission prompt may fire too early).
|
||||
The FuelBoard consumable tip products are:
|
||||
|
||||
## Review-relevant configuration — PASS
|
||||
- `com.apt.fuelboard.tip099`
|
||||
- `com.apt.fuelboard.tip299`
|
||||
- `com.apt.fuelboard.tip499`
|
||||
|
||||
- [x] **App icon**: was MISSING entirely (invalid-binary rejection). Fixed —
|
||||
1024×1024 `AppIcon.appiconset` + `ASSETCATALOG_COMPILER_APPICON_NAME`.
|
||||
- [x] **Launch screen**: `UILaunchScreen` empty dict present (iOS 14+ style).
|
||||
- [x] **Device family**: iPhone-only (`TARGETED_DEVICE_FAMILY = 1`) — no iPad
|
||||
multitasking requirements.
|
||||
- [x] **Versioning**: `CFBundleShortVersionString 1.0`, `CFBundleVersion 1`
|
||||
(bump per release).
|
||||
- [x] **Permissions**: only location (WhenInUse + Always), with clear usage
|
||||
strings. No contacts/photos/mic/camera.
|
||||
- [x] **No third-party SDKs** → no SDK privacy manifests or third-party
|
||||
attribution required.
|
||||
- [x] **No account creation / login** → no account-deletion requirement (5.1.1(v)).
|
||||
- [x] **ATS**: no `NSAllowsArbitraryLoads` (raw-IP cleartext is ATS-exempt; once
|
||||
the relay is HTTPS this is moot).
|
||||
- [x] **Export compliance**: no crypto beyond keychain/SecItem → no
|
||||
documentation needed.
|
||||
The app requests these exact **Product IDs** with StoreKit 2
|
||||
`Product.products(for:)`; the UI names are display labels and are not used for
|
||||
lookup. The app bundle ID, App Store Connect app, explicit App ID, development
|
||||
team, and Sandbox Apple Account must belong to the same developer account.
|
||||
|
||||
## Content / IP review
|
||||
Two test environments must remain distinct:
|
||||
|
||||
- **Brand logos (Shell, BP, Tesco…)**: bundled from Simple Icons (CC0) +
|
||||
Wikimedia Commons. Nominative use (identifying stations) is standard practice
|
||||
(cf. GasBuddy). Low risk, but keep the sources documented.
|
||||
- **Price data**: gov.uk Fuel Finder data is OGL-licensed — include attribution
|
||||
in the app (About/credits) once live data ships. Sample fallback data must be
|
||||
clearly demo-labelled (or removed for review) so reviewers don't think the
|
||||
prices are fake — 2.3.1 accuracy risk.
|
||||
- **"Cheapest" claims**: data is 12h-old cache. The UI says "cheapest" — fine,
|
||||
but a small "prices from the UK Fuel Finder dataset, updated twice daily"
|
||||
footnote is good hygiene for 2.3.1.
|
||||
- **Local StoreKit testing:** select `/Users/apt/workspace/fuelboard/FuelBoardTips.storekit`
|
||||
in Xcode, under Product > Scheme > Edit Scheme > Run > Options. This bypasses
|
||||
App Store Connect and confirms the app's StoreKit loading and purchase code.
|
||||
- **Apple sandbox testing:** select **None** for StoreKit Configuration, then
|
||||
use Product > Run on a physical device with the development-signed build and
|
||||
a Sandbox Apple Account signed in under Settings > Developer. This exercises
|
||||
the real App Store Connect catalog.
|
||||
|
||||
## Suggested review notes (copy-paste for App Store Connect)
|
||||
The local configuration returning all three products does not prove that the
|
||||
Apple sandbox catalog is ready. Conversely, a successful local run showing `$`
|
||||
does not mean the App Store Connect price is USD: `Product.displayPrice` is
|
||||
formatted for the active StoreKit/device storefront. Local StoreKit prices are
|
||||
placeholders and do not upload to App Store Connect.
|
||||
|
||||
### Availability versus pricing
|
||||
|
||||
App Store Connect has separate controls:
|
||||
|
||||
- **Availability** controls the storefronts where the IAP is offered. The
|
||||
verified FuelBoard configuration uses **United Kingdom only**.
|
||||
- **Price schedule** may show generated prices for **175 countries and
|
||||
regions**. This is expected and does not make the IAP available in all 175
|
||||
storefronts; it is Apple's price conversion schedule.
|
||||
|
||||
The Sandbox Apple Account's App Store Country or Region must match an enabled
|
||||
IAP storefront. After changing the sandbox tester's storefront, sign out and
|
||||
back in under Settings > Developer so the storefront change activates.
|
||||
|
||||
### Important propagation/cache caveat
|
||||
|
||||
During sandbox diagnosis, `Product.products(for:)` completed successfully but
|
||||
returned zero products with the IAP availability set to UK. Temporarily
|
||||
changing one product's availability to all regions caused it to appear in the
|
||||
sandbox catalog; changing it back to UK left it working. This indicates an
|
||||
Apple-side catalog refresh or propagation/cache issue, not a change required in
|
||||
the StoreKit code. Apple documents that product metadata changes can take up to
|
||||
approximately one hour to appear in sandbox.
|
||||
|
||||
If an otherwise valid product returns zero products:
|
||||
|
||||
1. Confirm the Sandbox Apple Account belongs to the same developer team.
|
||||
2. Confirm its storefront is an enabled availability region.
|
||||
3. Sign out and back in to the sandbox account on the device.
|
||||
4. Delete/reinstall the development build and retry with StoreKit Configuration
|
||||
set to **None**.
|
||||
5. If necessary, temporarily broaden one product's availability to all
|
||||
regions, allow propagation, verify the product, then restore the intended
|
||||
availability and allow propagation again.
|
||||
|
||||
Record whether the diagnostic result is **request failed**, **completed with
|
||||
zero products**, or **a partial product set**. These are different failure
|
||||
classes. Do not add fallback prices or change product lookup to display names.
|
||||
|
||||
Sandbox testing does not require prior App Review approval of the IAP. It does
|
||||
require active developer membership, an active Paid Applications Agreement,
|
||||
complete banking and tax information, valid product metadata and pricing, the
|
||||
correct app/team association, and a matching Sandbox Apple Account.
|
||||
|
||||
## Hard blockers (must happen before ANY submission)
|
||||
|
||||
0. **[USER] Paid Applications Agreement in effect** — REQUIRED for paid IAP
|
||||
configuration and sandbox testing. App Store Connect →
|
||||
Business > Paid Applications Agreement → View and Agree. **This is the most
|
||||
common cause of a 2.1(b) "unable to purchase item / error message" rejection
|
||||
— without it every sandbox purchase fails.** (REJECTED once for this:
|
||||
review date 2026-09-25, version 1.0 (21), iPad Air 11″ iPadOS 27.)
|
||||
**Status 2026-09-25: agreement clicked Agree but shows "Pending user info" —
|
||||
this is NOT in effect yet.** Complete the Banking (account + sort code/IBAN)
|
||||
and Tax (W-8/W-9/UK tax ref, e.g. sole-trader UTR) sections under Business >
|
||||
Payments & Tax until the agreement shows "Active" and every tab is green.
|
||||
Only then do sandbox IAPs start working.
|
||||
0. **[USER] Tip IAPs configured and available to the sandbox** — the three
|
||||
consumables may exist but must ALSO have complete metadata, a current price,
|
||||
and an enabled storefront availability. Prior App Review approval or
|
||||
`Cleared for Sale` status is **not required for sandbox product discovery**.
|
||||
They must be backed by the **In-App Purchase capability on the App ID**
|
||||
`com.apt.fuelboard` (developer portal → Identifiers → App ID capability).
|
||||
3-tier tip code is already correct (retry lookup `7cb23e0`); do NOT churn
|
||||
SettingsView.swift. Test product discovery and purchase with a Sandbox
|
||||
Apple Account first: Settings → Support FuelBoard.
|
||||
|
||||
1. **[USER] Apple Developer Program membership** — the app has never been
|
||||
signed (all builds are `CODE_SIGNING_ALLOWED=NO` + ad-hoc re-sign for
|
||||
SideStore). Requires a paid membership ($99/yr, up to 48 h to approve) and
|
||||
a developer account.
|
||||
2. **[USER] Register App ID `com.apt.fuelboard`** in the developer portal,
|
||||
with the **App Group `group.com.apt.fuelboard`** capability AND the
|
||||
**In-App Purchase** capability enabled (the widget + app-group cache
|
||||
depend on the group; the tip jar needs IAP).
|
||||
3. ~~[ME] `PrivacyInfo.xcprivacy`~~ — **DONE** (app, widget, watch app, and
|
||||
watch wrapper manifests added; UserDefaults `1C8F.1`; app declares Location).
|
||||
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`).
|
||||
5. **[USER] Privacy policy URL** in App Store Connect — mandatory; the app
|
||||
collects location. Policy text is now in `PRIVACY.md`; publish it via the
|
||||
FuelBoard marketing site or GitHub Pages and paste that public URL into App
|
||||
Store Connect.
|
||||
|
||||
## Required tasks — owner [ME] (technical, do after account exists)
|
||||
|
||||
- [x] **PrivacyInfo.xcprivacy** — app, widget, watch app, and watch wrapper
|
||||
manifests added: UserDefaults reason `1C8F.1`; app also declares Location
|
||||
(linked, not tracking). Diagnostic network beacon removed with LAN relay.
|
||||
- [ ] Distribution archive + TestFlight upload (needs Apple account)
|
||||
- [x] **Version/build aligned** → `1.0.0` (build 23), app + widget + watch targets
|
||||
- [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)
|
||||
- [x] **Remove LAN relay fallback entirely** — no private IP, Local Network
|
||||
permission string, relay toggle, or diagnostic network beacon remains in
|
||||
the app/widget/watch binary. Hidden Debug still exists for local notification
|
||||
and widget diagnostics only.
|
||||
- [ ] Final pass: `swift test` green (95 tests), archive smoke test on device
|
||||
- [ ] ~~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)
|
||||
|
||||
- [ ] Apple Developer Program membership + App ID (blockers #1–#2)
|
||||
- [ ] **Tip jar: create the three consumable IAPs** in App Store Connect →
|
||||
In-App Purchases (all type **Consumable**, exact product IDs, UK price
|
||||
tier; tip jars are an accepted App Review pattern):
|
||||
- `com.apt.fuelboard.tip099` — **£0.99** — "Splash & Dash" (Just enough to keep things moving.)
|
||||
- `com.apt.fuelboard.tip299` — **£2.99** — "Half a Tank" (A generous top-up for development.)
|
||||
- `com.apt.fuelboard.tip499` — **£4.99** — "Fill 'Er Up" (Keeping the app on the road.)
|
||||
App-side is ready (`41e4c91`): Settings → Support FuelBoard shows all
|
||||
three tiers with per-tier prices; sideloaded builds gracefully show
|
||||
"not available in this build yet" until the products exist.
|
||||
- [ ] App Store Connect app record: name, bundle ID `com.apt.fuelboard`,
|
||||
category (Utilities), pricing (Free), availability (UK first?)
|
||||
- [ ] Description, keywords, support URL, marketing URL, privacy policy URL
|
||||
- [ ] Privacy nutrition labels questionnaire (location: yes, linked to user
|
||||
for alerts/nearby; no tracking; no ATT prompt needed)
|
||||
- [ ] Age rating questionnaire (4+; no objectionable content)
|
||||
- [ ] Export compliance: HTTPS-only + keychain → standard encryption
|
||||
exemption (5A992a), no documentation needed
|
||||
- [ ] Approve the TestFlight build and do a real-device pass before submit
|
||||
- [ ] Final "Submit for Review" click + stand by to answer review questions
|
||||
|
||||
## Review notes (copy-paste into App Store Connect)
|
||||
|
||||
> FuelBoard shows the cheapest petrol, diesel, and premium fuel near the user
|
||||
> using the UK government Fuel Finder dataset (OGL). Prices are fetched up to
|
||||
> twice a day; the app filters 5/10/15-mile radii on-device. Location is used
|
||||
> to (a) rank stations by distance and (b) power the optional Alerts feature:
|
||||
> when enabled, the app geofences the cheapest station in the user's chosen
|
||||
> radius and sends a notification when they approach it. Always-location is
|
||||
> only requested when the user enables alerts. The app also provides a
|
||||
> home-screen widget showing the current cheapest station nearby.
|
||||
> using the UK government Fuel Finder dataset (Open Government Licence).
|
||||
> Prices are downloaded from the internet up to twice a day; the app filters
|
||||
> 5/10/15-mile radii on-device and works offline from a bundled snapshot.
|
||||
> Location is used to (a) rank stations by distance and (b) power the
|
||||
> optional Alerts feature: when the user enables alerts, the app registers
|
||||
> CoreLocation regions around the cheapest station in their radius and sends
|
||||
> a notification when they approach it. FuelBoard does not use persistent
|
||||
> real-time background location tracking; it relies on iOS region monitoring
|
||||
> and significant-change behaviour for optional alerts. Always-location is
|
||||
> requested only when the user enables alerts. The app also provides a
|
||||
> home-screen widget showing the current cheapest station nearby, and a
|
||||
> price-history Trends chart for starred stations. All data is UK-wide (Fuel
|
||||
> Finder covers England, Scotland, Wales and Northern Ireland); users outside
|
||||
> the UK see an empty state explaining this.
|
||||
|
||||
## Release checklist (before every submission)
|
||||
## Review risks (know before submitting)
|
||||
|
||||
1. [ ] Relay/govuk data source live on a public HTTPS endpoint
|
||||
2. [ ] Always-permission flow tied to explicit user opt-in
|
||||
3. [ ] Bump `CFBundleShortVersionString` / `CFBundleVersion`
|
||||
4. [ ] Sample-data fallback labelled or removed in the release build
|
||||
5. [ ] OGL attribution present in-app
|
||||
6. [ ] `swift test` green (FuelBoardTests, 28 tests)
|
||||
7. [ ] Security audit pass (see SECURITY.md recurring checks)
|
||||
8. [ ] Icon + screenshots for all required sizes in App Store Connect
|
||||
- **Always-location (5.1.1 / 2.5.4)** — the feature is visible (Alerts tab)
|
||||
and opt-in; the app does not declare persistent background location mode,
|
||||
and the review notes describe region monitoring/significant-change rather
|
||||
than continuous tracking.
|
||||
- **Reviewer outside the UK** — empty radius; mitigated by the empty-state
|
||||
task above + review notes.
|
||||
- **"Cheapest" claims (2.3.1)** — mitigated by the OGL/footnote task.
|
||||
- **Live Activities** — reviewers may ask why; answer: price alerts at a
|
||||
glance. It is declared in the plist already.
|
||||
- **Brand logos (Shell, BP, Tesco…)** — Simple Icons (CC0) + Wikimedia,
|
||||
nominative use (cf. GasBuddy); low risk, keep sources documented.
|
||||
- **GitHub raw as data source** — public HTTPS data, no credentials or Local
|
||||
Network permission; bundled dump covers any regional GitHub unreachability.
|
||||
|
||||
## Timeline (best case)
|
||||
|
||||
| Day | Owner | Milestone |
|
||||
|-----|-------|-----------|
|
||||
| 0–2 | USER | Developer Program enrollment + App ID + app group capability |
|
||||
| 2–3 | ME | Signing (automatic), PrivacyInfo.xcprivacy, attribution, empty state, version bump |
|
||||
| 3–4 | ME | Archive → TestFlight; screenshots captured |
|
||||
| 4–5 | USER | TestFlight install + real-device pass (onboarding, alerts, widget, offline) |
|
||||
| 5–6 | USER | App Store Connect metadata + labels + age rating + privacy URL |
|
||||
| 6–7 | USER/ME | Submit for review |
|
||||
| 7–14 | Apple | Review (typically 1–3 days, can be a week); answer questions same-day |
|
||||
|
||||
Rollback note: nothing about submission changes the repo's rollback posture
|
||||
(`main` history is intact; release builds are tagged per submission).
|
||||
|
||||
|
After Width: | Height: | Size: 273 KiB |
|
After Width: | Height: | Size: 841 KiB |
|
After Width: | Height: | Size: 306 KiB |
|
After Width: | Height: | Size: 354 KiB |
|
After Width: | Height: | Size: 278 KiB |
|
After Width: | Height: | Size: 444 KiB |
|
After Width: | Height: | Size: 266 KiB |
|
After Width: | Height: | Size: 346 KiB |
|
After Width: | Height: | Size: 268 KiB |
|
After Width: | Height: | Size: 815 KiB |
|
After Width: | Height: | Size: 724 KiB |
|
After Width: | Height: | Size: 686 KiB |
|
After Width: | Height: | Size: 303 KiB |
|
After Width: | Height: | Size: 280 KiB |
|
After Width: | Height: | Size: 349 KiB |
@@ -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.
|
||||
FuelBoard — UK-wide petrol-station price widget + app.
|
||||
GitHub mirror (`aptonline/fuelboard-data`, 2×/day 10:00+16:00) 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 fallback has been removed from the app binary; the public GitHub mirror is the only live network data source.
|
||||
|
||||
Priority guide: P0 = next build, P1 = soon, P2 = later, P3 = maybe/idea.
|
||||
Status: TODO / IN PROGRESS / DONE / BLOCKED.
|
||||
@@ -11,18 +13,60 @@ Status: TODO / IN PROGRESS / DONE / BLOCKED.
|
||||
|
||||
## P1 — Soon
|
||||
|
||||
- [x] **Move relay + mirror off the Mac mini (DONE 2026-08-16)** — dedicated
|
||||
Debian 13 LXC on Proxmox via community-scripts/ProxmoxVE `ct/debian.sh`
|
||||
(CT **111**, hostname **`fuelboard-relay`**, 1 core/512 MB/2 GB, static IP
|
||||
**192.168.1.113**, plain LXC — NO Docker layer). Scope: relay AND mirror
|
||||
both moved (full Mac independence; relays bind **loopback only**, GitHub is
|
||||
the only public surface). What landed: Python 3.13.5 (trixie) boots both
|
||||
relays (relay 8788 demo/CSV + relay-api 8789 `source: api`, 8,024 stations,
|
||||
~2 min first sync, 0 failures); source at `/root/workspace/fuelboard-{relay,relay-api}`
|
||||
(venvs + pip, `.env` chmod 600); deploy key `fuelboard_deploy` +
|
||||
`github-fuelboard-data` ssh config + `fuelboard-data` clone at
|
||||
`/root/workspace/fuelboard-data` (origin = ssh alias, green + synced);
|
||||
`mirror_push.py` → `/root/scripts/mirror_push.py` (RELAY line already
|
||||
`127.0.0.1:8789`); systemd `fuelboard-relay.service` +
|
||||
`fuelboard-relay-api.service` (Restart=always, `--host 127.0.0.1`) +
|
||||
`fuelboard-mirror.service` (oneshot) + `.timer` (10:00 + 16:00, Persistent,
|
||||
armed — next Mon 10:00); first live push VERIFIED from outside
|
||||
(commit `735a292`, latest.json `data_updated 16:50:53Z`); Mac launchd
|
||||
`com.apt.fuelboard-{relay,relay-api,mirror}` unloaded + plists parked in
|
||||
`~/Library/LaunchAgents/fuelboard-migrated-to-ct/`; editor 8790 + IPA 8765
|
||||
stay on the Mac; price watchdog `relay_price_watchdog.py` now fetches via
|
||||
SSH (`~/.ssh/fuelboard_ct` → `/root/scripts/watchdog_fetch.py`). Logs via
|
||||
`journalctl -u fuelboard-*` on the CT; SSH into CT: `ssh -i ~/.ssh/fuelboard_ct root@192.168.1.113`.
|
||||
**Open: rootfs is 2 GB (1.1 GB free) — mirror retention ~940 MB will get
|
||||
tight; rootfs grown to 8 GB on pve 2026-08-16 (5.9 GB free).**
|
||||
|
||||
- [x] **Siri: "Cheapest [fuel] near me"** — `AppShortcutsProvider` + `CheapestFuelIntent`
|
||||
(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`.*
|
||||
- [ ] **Siri: "Closest [fuel] station"** — same plumbing as cheapest, sort by
|
||||
distance instead of price. Free second phrase in the same `AppShortcutsProvider`.
|
||||
- [x] **Favourites Trends graph** — price history chart for starred stations.
|
||||
Swift Charts (iOS 26 target) line chart in the Favourites tab; one series per
|
||||
favourite, fuel-scoped (station, fuel) pairs map 1:1 to the archive. Default =
|
||||
absolute price lines; toggle "vs cheapest favourite" re-baselines to 0 as signed
|
||||
pence (mirrors the Cheapest/Closest delta pattern users already know). Reuse the
|
||||
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, 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
|
||||
cheapest, per-station legend, empty/loading/retry states) + toolbar entry in
|
||||
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: lines render once ≥2 snapshots (archive warming daily,
|
||||
~Aug 17).*
|
||||
|
||||
## P2 — Later
|
||||
|
||||
@@ -62,13 +106,48 @@ Status: TODO / IN PROGRESS / DONE / BLOCKED.
|
||||
scene (CPListTemplate) for navigation deep-links.
|
||||
- [ ] **Alert region tuning** — make alert trigger radius + dedup window
|
||||
user-configurable (currently fixed 300 m trigger / 1 per station per hour).
|
||||
- [ ] **Restore Best Value proximity alerts after fixing evaluation parity** —
|
||||
the option is intentionally removed from the Alerts tab and legacy selections
|
||||
fall back to `Cheapest station` until this is implemented. Root cause: the live
|
||||
geofence path calculates journeys from the entered station, making that station
|
||||
its own zero-saving baseline and therefore unable to clear any positive minimum
|
||||
saving, while Test alert calculates from the user's location and can pass.
|
||||
Future implementation must obtain a fresh user fix on region entry; fetch,
|
||||
radius-filter, choose the closest eligible baseline and calculate every journey
|
||||
from that user location; then notify only when the entered station is the best
|
||||
qualifying candidate. Real and Test alerts must call one shared pure evaluator.
|
||||
Apply one disclosed qualification rule everywhere (real alert, Test alert and
|
||||
BEST VALUE badge): remove the live path's hidden extra £0.50 estimate margin,
|
||||
compare the same penny-rounded amount shown to the user while retaining raw
|
||||
precision for ranking, and pass the current threshold explicitly into station
|
||||
rows instead of reading persistence per row. Regression coverage: entered
|
||||
station can qualify, a better nearby candidate blocks it, real/Test parity,
|
||||
exact and display-rounding boundaries, estimated/confirmed parity, immediate
|
||||
threshold changes, and below-threshold rejection.
|
||||
- [ ] **Price-drop alerts: threshold control + Check now** — keep the current
|
||||
cache-first / 12 h refresh policy, but add a per-alert sensitivity control
|
||||
for favourite price-drop alerts instead of a fetch-frequency slider. Scope:
|
||||
`Minimum change` picker (e.g. 0.5p / 1.0p / 2.0p), plus a `Check now`
|
||||
action in the Alerts tab that forces a fetch and immediately re-evaluates the
|
||||
cheapest-favourite alert. Copy should set expectation honestly: standard
|
||||
mode checks when FuelBoard refreshes prices (up to twice daily).
|
||||
- [ ] **Price-drop alerts: optional frequent-check mode** — if the app ever
|
||||
relaxes the current "twice daily max" data policy, expose this as a clearly
|
||||
separate mode rather than a freeform frequency slider. Example framing:
|
||||
`Standard` (current behaviour) vs `Frequent` (best-effort extra checks),
|
||||
with conservative caps (e.g. every 4–6 h, not hourly) and copy that does not
|
||||
promise exact timing under iOS background scheduling.
|
||||
- [ ] **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.
|
||||
- [ ] **Trends: national average overlay + per-day min/max** — the full dump is
|
||||
already held, so the UK/regional average line is computable locally; archive can
|
||||
store per-day min/max so the chart shows intraday spikes, not just the daily point.
|
||||
|
||||
## P3 — Ideas / Research
|
||||
|
||||
@@ -78,19 +157,55 @@ Status: TODO / IN PROGRESS / DONE / BLOCKED.
|
||||
- [ ] **App Intents testing** — AppIntentsTesting (iOS 27) for intent coverage
|
||||
instead of XCUITest when the Siri features land.
|
||||
|
||||
- [ ] **Production data mirror (GitHub)** — relay pushes the synced dump
|
||||
(`stations.json` + `latest.json` version pointer) to a PUBLIC GitHub repo; the
|
||||
app fetches `raw.githubusercontent.com` with the LAN relay as fallback source.
|
||||
Public repo required for App Review (reviewer must fetch the data themselves);
|
||||
HTTPS/ATS fine; plain JSON data is not "remote code" (2.5.2-safe); bundle a small
|
||||
fallback dump so the app never demos empty. Needs GitHub PAT (aptonline account).
|
||||
- [ ] **Fuel-price prediction** — trend from historical dumps (relay could keep a
|
||||
rolling archive) to show "price tends up/down".
|
||||
- [ ] **Fuel-price prediction** — "price tends up/down" arrow derived from the
|
||||
price-history archive (P0). Cheap once the archive + Trends graph exist; keep a
|
||||
separate phase. Backlog ancestry: this was the original "relay could keep a
|
||||
rolling archive" idea — the archive is now P0 and the Trends graph (P1) is its
|
||||
visible half.
|
||||
- [ ] **Foresight Live Activity (heading-aware next station)** — investigate a
|
||||
driving-time surface that predicts the likely next station ahead of the user
|
||||
from cached data only (no extra source-data refresh on motion). Best v1 shape:
|
||||
Live Activity option, not widget-first, because widget space/refresh budgets
|
||||
are tight and the lock-screen/CarPlay compact slot better matches the use case.
|
||||
Prefer ONE FuelBoard Live Activity with a mode selector (e.g. `Nearby cheapest`
|
||||
vs `Foresight / next ahead`) rather than trying to run two simultaneous
|
||||
FuelBoard activities; iOS can support multiple activities, but FuelBoard's
|
||||
current implementation is single-activity and the compact surfaces are too
|
||||
constrained for two competing driving modes anyway. Viable MVP: infer heading
|
||||
from recent motion/course, keep only stations in a forward cone, score
|
||||
candidates by forward distance + lateral offset, and show the next-ahead
|
||||
station with all three fuel prices or a compact rotating/focused fuel
|
||||
treatment if space allows. Key caveat: heading is not full route awareness,
|
||||
so avoid overpromising "the next station you will reach" on motorways/opposite
|
||||
carriageways; frame as "next ahead" / "likely next stop" unless route-quality
|
||||
heuristics improve. Must include hysteresis/speed thresholds to avoid flapping,
|
||||
and should be tested specifically against compact Live Activity/CarPlay layouts
|
||||
where space is extremely limited.
|
||||
- [ ] **Widget sparkline** — medium-widget mini sparkline of a favourite's 30-day
|
||||
trend, after the Trends UI lands (one-kind rule preserved).
|
||||
- [ ] **Price alerts for a specific station** — notify when a starred station's
|
||||
price drops below a user threshold.
|
||||
- [ ] **Price-drop alerts: mode = Cheapest favourite / All favourites** — keep
|
||||
`Cheapest favourite` as the default low-noise mode, but explore an optional
|
||||
`All favourites` mode for one fuel. Guardrails if built: threshold required,
|
||||
per-station dedupe based on last-alerted price (not just time), and combine
|
||||
multiple drops from one refresh into a single summary notification rather
|
||||
than one alert per station.
|
||||
- [ ] **Share sheet** — share "Cheapest Unleaded near me" as text/image card.
|
||||
- [ ] **Watch app** — glanceable cheapest-price complication (WatchKit + shared
|
||||
app-group store; watch target would need to be added to the project).
|
||||
- [ ] **Watch app v1 — favourites + Check now** — small Apple Watch companion,
|
||||
not a phone-app clone. Favourites-first home screen: fuel picker, `Cheapest
|
||||
favourite` summary card, top few favourites for that fuel, last-updated time,
|
||||
and a prominent `Check now` action that forces a refresh + re-evaluates
|
||||
favourite prices. Keep the watch UX pull-based and glanceable; avoid implying
|
||||
high-frequency background refresh. Shared-store architecture preferred over a
|
||||
second watch-only data model.
|
||||
- [ ] **Watch complications / Smart Stack** — complication/widget surfaces for
|
||||
the same watch concept: show the cheapest favourite price, fuel, and short
|
||||
station name at a glance. Keep copy minimal and honest about freshness.
|
||||
- [ ] **Watch app v2 — favourite detail + handoff** — tap a favourite row for a
|
||||
detail screen (station, brand, price, last updated) with actions like
|
||||
`Directions` and `Open on iPhone`. Reuse existing maps handoff patterns where
|
||||
possible instead of inventing watch-only navigation logic.
|
||||
- [ ] **App Clip** — 10 MB budget for "find cheapest fuel nearby" without install.
|
||||
- [ ] **Widget picker: dual-unit distance labels** — the Edit-Widget Distance row
|
||||
label is cached by the system at sheet-render time and only re-resolves on
|
||||
@@ -109,6 +224,17 @@ Status: TODO / IN PROGRESS / DONE / BLOCKED.
|
||||
|
||||
## Done (recent)
|
||||
|
||||
- [x] **GitHub price-history mirror + LIVE provider chain (P0, DONE 2026-08-16)** —
|
||||
push side (relay → public `aptonline/fuelboard-data`, daily 10:00 launchd, SSH
|
||||
deploy key, 180-day retention) + history read side (Trends, P1) + **live chain
|
||||
(`feature/p0-live-chain`)**: `MirrorFuelProvider` fetches CURRENT prices from
|
||||
raw.githubusercontent (latest.json → history/<day>.json, app-group day-cache) →
|
||||
**GitHub mirror only everywhere** (alert checks + widget too); no LAN relay
|
||||
fallback, private IP, Local Network permission, or diagnostic network beacon
|
||||
ships in the app binary. Bundled REAL dump (`FuelBoardDump` dataset, refreshed
|
||||
by `scripts/refresh_bundled_dump.sh`) is the app's no-network last resort.
|
||||
Local Network permission removed entirely — data page is informational, replay
|
||||
on cellular can no longer hang. 107+ tests.
|
||||
- [x] Switch dataset to govuk API data — official Fuel Finder OAuth API via
|
||||
relay-api :8789 (source: api, ~8,012 stations); app baseURL → :8789; credentials
|
||||
live in relay `.env` only, never in the IPA
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
@@ -27,23 +29,23 @@
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</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 connects to the FuelBoard Relay on your local network to download the latest fuel prices.</string>
|
||||
<key>NSLocationWhenInUseUsageDescription</key>
|
||||
<string>FuelBoard uses your location to find the cheapest nearby petrol stations.</string>
|
||||
<key>NSSupportsLiveActivities</key>
|
||||
<true/>
|
||||
<key>BGTaskSchedulerPermittedIdentifiers</key>
|
||||
<array>
|
||||
<string>com.apt.fuelboard.smart-refresh</string>
|
||||
</array>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>location</string>
|
||||
<string>fetch</string>
|
||||
</array>
|
||||
<key>UILaunchScreen</key>
|
||||
<dict/>
|
||||
|
||||
@@ -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>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.com.apt.fuelboard</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,34 @@
|
||||
<?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>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>FuelBoardWatch</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionAttributes</key>
|
||||
<dict>
|
||||
<key>WKAppBundleIdentifier</key>
|
||||
<string>com.apt.fuelboard.watchapp</string>
|
||||
</dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.watchkit</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,33 @@
|
||||
<?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>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>FuelBoard</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>FuelBoard</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
</array>
|
||||
<key>WKCompanionAppBundleIdentifier</key>
|
||||
<string>com.apt.fuelboard</string>
|
||||
<key>WKWatchKitApp</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -17,15 +17,15 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>NSLocationWhenInUseUsageDescription</key>
|
||||
<string>FuelBoard uses your location to show the cheapest nearby petrol stations.</string>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</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>
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
// !$*UTF8*$!
|
||||
// FuelBoard — hand-crafted project (objectVersion 77, synchronized folders).
|
||||
// Targets: FuelBoard (app) + FuelBoardWidgets (widget extension, embedded).
|
||||
// Shared/ is a synchronized root group included in BOTH targets.
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
@@ -10,10 +7,37 @@
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
ADCE5B5A3033A32C008C131C /* FuelBoardWatch Watch App.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = ADCE5B4F3033A329008C131C /* FuelBoardWatch Watch App.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
ADCE5B623033A32D008C131C /* FuelBoardWatch Watch App Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = ADCE5B633033A32D008C131C /* FuelBoardWatch Watch App Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
BB0000000000000000000001 /* FuelBoardWidgets.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = BB0000000000000000000002 /* FuelBoardWidgets.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
FBA000000000000000000001 /* DataRefreshMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBA000000000000000000101 /* DataRefreshMode.swift */; };
|
||||
FBA000000000000000000002 /* FuelHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBA000000000000000000102 /* FuelHistory.swift */; };
|
||||
FBA000000000000000000003 /* FuelPriceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBA000000000000000000103 /* FuelPriceProvider.swift */; };
|
||||
FBA000000000000000000004 /* FuelStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBA000000000000000000104 /* FuelStore.swift */; };
|
||||
FBA000000000000000000005 /* MirrorFuelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBA000000000000000000105 /* MirrorFuelProvider.swift */; };
|
||||
FBA000000000000000000006 /* SiriCheapestLookup.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBA000000000000000000106 /* SiriCheapestLookup.swift */; };
|
||||
FBA000000000000000000007 /* WidgetLocationFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBA000000000000000000107 /* WidgetLocationFetcher.swift */; };
|
||||
FBA000000000000000000008 /* FuelBoardTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBA000000000000000000108 /* FuelBoardTests.swift */; };
|
||||
FBA000000000000000000009 /* FuelHistoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBA000000000000000000109 /* FuelHistoryTests.swift */; };
|
||||
FBA00000000000000000000A /* LiveChainTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBA00000000000000000010A /* LiveChainTests.swift */; };
|
||||
FBA00000000000000000000B /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FBA00000000000000000010B /* XCTest.framework */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
ADCE5B643033A32D008C131C /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = BB0000000000000000000004 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = ADCE5B4E3033A329008C131C;
|
||||
remoteInfo = "FuelBoardWatch Watch App Extension";
|
||||
};
|
||||
ADCE5B6F3033A32F008C131C /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = BB0000000000000000000004 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = ADCE5B673033A32D008C131C;
|
||||
remoteInfo = "FuelBoardWatch Watch App";
|
||||
};
|
||||
BB0000000000000000000003 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = BB0000000000000000000004 /* Project object */;
|
||||
@@ -24,6 +48,28 @@
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
ADCE5B5B3033A32C008C131C /* Embed Watch Content */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "$(CONTENTS_FOLDER_PATH)/Watch";
|
||||
dstSubfolderSpec = 16;
|
||||
files = (
|
||||
ADCE5B5A3033A32C008C131C /* FuelBoardWatch Watch App.app in Embed Watch Content */,
|
||||
);
|
||||
name = "Embed Watch Content";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
ADCE5B653033A32D008C131C /* Embed App Extensions */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 13;
|
||||
files = (
|
||||
ADCE5B623033A32D008C131C /* FuelBoardWatch Watch App Extension.appex in Embed App Extensions */,
|
||||
);
|
||||
name = "Embed App Extensions";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
BB0000000000000000000006 /* Embed App Extensions */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@@ -38,11 +84,36 @@
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
AD22FCFF30682512009859D4 /* FuelBoardTips.storekit */ = {isa = PBXFileReference; lastKnownFileType = text; path = FuelBoardTips.storekit; sourceTree = "<group>"; };
|
||||
AD64FC4C30739A4B008F27A9 /* FuelBoardSharedTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FuelBoardSharedTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
ADCE5B4F3033A329008C131C /* FuelBoardWatch Watch App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "FuelBoardWatch Watch App.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
ADCE5B633033A32D008C131C /* FuelBoardWatch Watch App Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "FuelBoardWatch Watch App Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
BB0000000000000000000002 /* FuelBoardWidgets.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = FuelBoardWidgets.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
BB0000000000000000000007 /* FuelBoard.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FuelBoard.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
FBA000000000000000000101 /* DataRefreshMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataRefreshMode.swift; sourceTree = "<group>"; };
|
||||
FBA000000000000000000102 /* FuelHistory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FuelHistory.swift; sourceTree = "<group>"; };
|
||||
FBA000000000000000000103 /* FuelPriceProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FuelPriceProvider.swift; sourceTree = "<group>"; };
|
||||
FBA000000000000000000104 /* FuelStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FuelStore.swift; sourceTree = "<group>"; };
|
||||
FBA000000000000000000105 /* MirrorFuelProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MirrorFuelProvider.swift; sourceTree = "<group>"; };
|
||||
FBA000000000000000000106 /* SiriCheapestLookup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiriCheapestLookup.swift; sourceTree = "<group>"; };
|
||||
FBA000000000000000000107 /* WidgetLocationFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetLocationFetcher.swift; sourceTree = "<group>"; };
|
||||
FBA000000000000000000108 /* FuelBoardTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FuelBoardTests.swift; sourceTree = "<group>"; };
|
||||
FBA000000000000000000109 /* FuelHistoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FuelHistoryTests.swift; sourceTree = "<group>"; };
|
||||
FBA00000000000000000010A /* LiveChainTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveChainTests.swift; sourceTree = "<group>"; };
|
||||
FBA00000000000000000010B /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = System/Library/Frameworks/XCTest.framework; sourceTree = SDKROOT; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||
ADCE5B503033A329008C131C /* FuelBoardWatch Watch App */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
path = "FuelBoardWatch Watch App";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
ADCE5B663033A32D008C131C /* FuelBoardWatch Wrapper */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
path = "FuelBoardWatch Wrapper";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
BB0000000000000000000008 /* FuelBoard */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
path = FuelBoard;
|
||||
@@ -61,6 +132,20 @@
|
||||
/* End PBXFileSystemSynchronizedRootGroup section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
ADCE5B4C3033A329008C131C /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
ADCE5B6D3033A32E008C131C /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
BB000000000000000000000B /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@@ -75,16 +160,37 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
FBA000000000000000000302 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
FBA00000000000000000000B /* XCTest.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
AD64FC4B30739A4B008F27A9 /* Recovered References */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
FBA00000000000000000010B /* XCTest.framework */,
|
||||
);
|
||||
name = "Recovered References";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
BB000000000000000000000D = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
FBA000000000000000000200 /* FuelBoardTests */,
|
||||
AD22FCFF30682512009859D4 /* FuelBoardTips.storekit */,
|
||||
BB0000000000000000000008 /* FuelBoard */,
|
||||
BB0000000000000000000009 /* FuelBoardWidgets */,
|
||||
BB000000000000000000000A /* Shared */,
|
||||
ADCE5B663033A32D008C131C /* FuelBoardWatch Wrapper */,
|
||||
ADCE5B503033A329008C131C /* FuelBoardWatch Watch App */,
|
||||
BB000000000000000000000E /* Products */,
|
||||
AD64FC4B30739A4B008F27A9 /* Recovered References */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
@@ -93,37 +199,95 @@
|
||||
children = (
|
||||
BB0000000000000000000007 /* FuelBoard.app */,
|
||||
BB0000000000000000000002 /* FuelBoardWidgets.appex */,
|
||||
ADCE5B4F3033A329008C131C /* FuelBoardWatch Watch App.app */,
|
||||
ADCE5B633033A32D008C131C /* FuelBoardWatch Watch App Extension.appex */,
|
||||
AD64FC4C30739A4B008F27A9 /* FuelBoardSharedTests.xctest */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
FBA000000000000000000200 /* FuelBoardTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
FBA000000000000000000201 /* Sources */,
|
||||
FBA000000000000000000202 /* Tests */,
|
||||
);
|
||||
path = FuelBoardTests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
FBA000000000000000000201 /* Sources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
FBA000000000000000000101 /* DataRefreshMode.swift */,
|
||||
FBA000000000000000000102 /* FuelHistory.swift */,
|
||||
FBA000000000000000000103 /* FuelPriceProvider.swift */,
|
||||
FBA000000000000000000104 /* FuelStore.swift */,
|
||||
FBA000000000000000000105 /* MirrorFuelProvider.swift */,
|
||||
FBA000000000000000000106 /* SiriCheapestLookup.swift */,
|
||||
FBA000000000000000000107 /* WidgetLocationFetcher.swift */,
|
||||
);
|
||||
name = Sources;
|
||||
path = Sources/FuelBoardShared;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
FBA000000000000000000202 /* Tests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
FBA000000000000000000108 /* FuelBoardTests.swift */,
|
||||
FBA000000000000000000109 /* FuelHistoryTests.swift */,
|
||||
FBA00000000000000000010A /* LiveChainTests.swift */,
|
||||
);
|
||||
name = Tests;
|
||||
path = Tests/FuelBoardSharedTests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
BB000000000000000000000F /* FuelBoard */ = {
|
||||
ADCE5B4E3033A329008C131C /* FuelBoardWatch Watch App Extension */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = BB0000000000000000000010 /* Build configuration list for PBXNativeTarget "FuelBoard" */;
|
||||
buildConfigurationList = ADCE5B593033A32B008C131C /* Build configuration list for PBXNativeTarget "FuelBoardWatch Watch App Extension" */;
|
||||
buildPhases = (
|
||||
BB0000000000000000000011 /* Sources */,
|
||||
BB000000000000000000000B /* Frameworks */,
|
||||
BB0000000000000000000012 /* Resources */,
|
||||
BB0000000000000000000006 /* Embed App Extensions */,
|
||||
ADCE5B4B3033A329008C131C /* Sources */,
|
||||
ADCE5B4C3033A329008C131C /* Frameworks */,
|
||||
ADCE5B4D3033A329008C131C /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
BB0000000000000000000013 /* PBXTargetDependency */,
|
||||
);
|
||||
fileSystemSynchronizedGroups = (
|
||||
BB0000000000000000000008 /* FuelBoard */,
|
||||
BB000000000000000000000A /* Shared */,
|
||||
ADCE5B503033A329008C131C /* FuelBoardWatch Watch App */,
|
||||
);
|
||||
name = FuelBoard;
|
||||
name = "FuelBoardWatch Watch App Extension";
|
||||
packageProductDependencies = (
|
||||
);
|
||||
productName = FuelBoard;
|
||||
productReference = BB0000000000000000000007 /* FuelBoard.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
productName = "FuelBoardWatch Watch App Extension";
|
||||
productReference = ADCE5B633033A32D008C131C /* FuelBoardWatch Watch App Extension.appex */;
|
||||
productType = "com.apple.product-type.watchkit2-extension";
|
||||
};
|
||||
ADCE5B673033A32D008C131C /* FuelBoardWatch Watch App */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = ADCE5B6A3033A32D008C131C /* Build configuration list for PBXNativeTarget "FuelBoardWatch Watch App" */;
|
||||
buildPhases = (
|
||||
ADCE5B6E3033A32E008C131C /* Resources */,
|
||||
ADCE5B653033A32D008C131C /* Embed App Extensions */,
|
||||
ADCE5B6D3033A32E008C131C /* Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
ADCE5B683033A32D008C131C /* PBXTargetDependency */,
|
||||
);
|
||||
fileSystemSynchronizedGroups = (
|
||||
ADCE5B663033A32D008C131C /* FuelBoardWatch Wrapper */,
|
||||
);
|
||||
name = "FuelBoardWatch Watch App";
|
||||
packageProductDependencies = (
|
||||
);
|
||||
productName = "FuelBoardWatch Watch App";
|
||||
productReference = ADCE5B4F3033A329008C131C /* FuelBoardWatch Watch App.app */;
|
||||
productType = "com.apple.product-type.application.watchapp2";
|
||||
};
|
||||
BB0000000000000000000005 /* FuelBoardWidgets */ = {
|
||||
isa = PBXNativeTarget;
|
||||
@@ -148,6 +312,50 @@
|
||||
productReference = BB0000000000000000000002 /* FuelBoardWidgets.appex */;
|
||||
productType = "com.apple.product-type.app-extension";
|
||||
};
|
||||
BB000000000000000000000F /* FuelBoard */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = BB0000000000000000000010 /* Build configuration list for PBXNativeTarget "FuelBoard" */;
|
||||
buildPhases = (
|
||||
BB0000000000000000000011 /* Sources */,
|
||||
BB000000000000000000000B /* Frameworks */,
|
||||
BB0000000000000000000012 /* Resources */,
|
||||
ADCE5B5B3033A32C008C131C /* Embed Watch Content */,
|
||||
BB0000000000000000000006 /* Embed App Extensions */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
ADCE5B703033A32F008C131C /* PBXTargetDependency */,
|
||||
BB0000000000000000000013 /* PBXTargetDependency */,
|
||||
);
|
||||
fileSystemSynchronizedGroups = (
|
||||
BB0000000000000000000008 /* FuelBoard */,
|
||||
BB000000000000000000000A /* Shared */,
|
||||
);
|
||||
name = FuelBoard;
|
||||
packageProductDependencies = (
|
||||
);
|
||||
productName = FuelBoard;
|
||||
productReference = BB0000000000000000000007 /* FuelBoard.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
FBA000000000000000000300 /* FuelBoardSharedTests */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = FBA000000000000000000400 /* Build configuration list for PBXNativeTarget "FuelBoardSharedTests" */;
|
||||
buildPhases = (
|
||||
FBA000000000000000000301 /* Sources */,
|
||||
FBA000000000000000000302 /* Frameworks */,
|
||||
FBA000000000000000000303 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = FuelBoardSharedTests;
|
||||
productName = FuelBoardSharedTests;
|
||||
productReference = AD64FC4C30739A4B008F27A9 /* FuelBoardSharedTests.xctest */;
|
||||
productType = "com.apple.product-type.bundle.unit-test";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
@@ -155,15 +363,23 @@
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = 1;
|
||||
LastSwiftUpdateCheck = 1600;
|
||||
LastSwiftUpdateCheck = 2630;
|
||||
LastUpgradeCheck = 1600;
|
||||
TargetAttributes = {
|
||||
BB000000000000000000000F = {
|
||||
CreatedOnToolsVersion = 16.0;
|
||||
ADCE5B4E3033A329008C131C = {
|
||||
CreatedOnToolsVersion = 26.3;
|
||||
};
|
||||
BB0000000000000000000005 = {
|
||||
CreatedOnToolsVersion = 16.0;
|
||||
};
|
||||
BB000000000000000000000F = {
|
||||
CreatedOnToolsVersion = 16.0;
|
||||
SystemCapabilities = {
|
||||
com.apple.InAppPurchase = {
|
||||
enabled = 1;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = BB0000000000000000000017 /* Build configuration list for PBXProject "FuelBoard" */;
|
||||
@@ -182,11 +398,28 @@
|
||||
targets = (
|
||||
BB000000000000000000000F /* FuelBoard */,
|
||||
BB0000000000000000000005 /* FuelBoardWidgets */,
|
||||
ADCE5B673033A32D008C131C /* FuelBoardWatch Watch App */,
|
||||
ADCE5B4E3033A329008C131C /* FuelBoardWatch Watch App Extension */,
|
||||
FBA000000000000000000300 /* FuelBoardSharedTests */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
ADCE5B4D3033A329008C131C /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
ADCE5B6E3033A32E008C131C /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
BB0000000000000000000012 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@@ -201,9 +434,23 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
FBA000000000000000000303 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
ADCE5B4B3033A329008C131C /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
BB0000000000000000000011 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@@ -218,9 +465,36 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
FBA000000000000000000301 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
FBA000000000000000000001 /* DataRefreshMode.swift in Sources */,
|
||||
FBA000000000000000000002 /* FuelHistory.swift in Sources */,
|
||||
FBA000000000000000000003 /* FuelPriceProvider.swift in Sources */,
|
||||
FBA000000000000000000004 /* FuelStore.swift in Sources */,
|
||||
FBA000000000000000000005 /* MirrorFuelProvider.swift in Sources */,
|
||||
FBA000000000000000000006 /* SiriCheapestLookup.swift in Sources */,
|
||||
FBA000000000000000000007 /* WidgetLocationFetcher.swift in Sources */,
|
||||
FBA000000000000000000008 /* FuelBoardTests.swift in Sources */,
|
||||
FBA000000000000000000009 /* FuelHistoryTests.swift in Sources */,
|
||||
FBA00000000000000000000A /* LiveChainTests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
ADCE5B683033A32D008C131C /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = ADCE5B4E3033A329008C131C /* FuelBoardWatch Watch App Extension */;
|
||||
targetProxy = ADCE5B643033A32D008C131C /* PBXContainerItemProxy */;
|
||||
};
|
||||
ADCE5B703033A32F008C131C /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = ADCE5B673033A32D008C131C /* FuelBoardWatch Watch App */;
|
||||
targetProxy = ADCE5B6F3033A32F008C131C /* PBXContainerItemProxy */;
|
||||
};
|
||||
BB0000000000000000000013 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = BB0000000000000000000005 /* FuelBoardWidgets */;
|
||||
@@ -229,6 +503,266 @@
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
ADCE5B573033A32B008C131C /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Config/WatchApp.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 30;
|
||||
DEVELOPMENT_TEAM = F3BE6NE7U3;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
INFOPLIST_FILE = "Config/WatchExtension-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 1.1.0;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.apt.fuelboard.watchapp.watchkitextension;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = watchos;
|
||||
SKIP_INSTALL = YES;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = 4;
|
||||
WATCHOS_DEPLOYMENT_TARGET = 26.2;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
ADCE5B583033A32B008C131C /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Config/WatchApp.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 30;
|
||||
DEVELOPMENT_TEAM = F3BE6NE7U3;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
INFOPLIST_FILE = "Config/WatchExtension-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 1.1.0;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.apt.fuelboard.watchapp.watchkitextension;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = watchos;
|
||||
SKIP_INSTALL = YES;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = 4;
|
||||
WATCHOS_DEPLOYMENT_TARGET = 26.2;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
ADCE5B6B3033A32D008C131C /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 30;
|
||||
DEVELOPMENT_TEAM = F3BE6NE7U3;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
INFOPLIST_FILE = "Config/WatchWrapper-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 1.1.0;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.apt.fuelboard.watchapp;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = watchos;
|
||||
SKIP_INSTALL = YES;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = 4;
|
||||
WATCHOS_DEPLOYMENT_TARGET = 26.2;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
ADCE5B6C3033A32D008C131C /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 30;
|
||||
DEVELOPMENT_TEAM = F3BE6NE7U3;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
INFOPLIST_FILE = "Config/WatchWrapper-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 1.1.0;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.apt.fuelboard.watchapp;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = watchos;
|
||||
SKIP_INSTALL = YES;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = 4;
|
||||
WATCHOS_DEPLOYMENT_TARGET = 26.2;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
BB0000000000000000000018 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
@@ -290,17 +824,20 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_ENTITLEMENTS = Config/App.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
CURRENT_PROJECT_VERSION = 30;
|
||||
DEVELOPMENT_TEAM = F3BE6NE7U3;
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
INFOPLIST_FILE = Config/App-Info.plist;
|
||||
INFOPLIST_FILE = "Config/App-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
MARKETING_VERSION = 1.1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.apt.fuelboard;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = 1;
|
||||
@@ -312,17 +849,20 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_ENTITLEMENTS = Config/App.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
CURRENT_PROJECT_VERSION = 30;
|
||||
DEVELOPMENT_TEAM = F3BE6NE7U3;
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
INFOPLIST_FILE = Config/App-Info.plist;
|
||||
INFOPLIST_FILE = "Config/App-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
MARKETING_VERSION = 1.1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.apt.fuelboard;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = 1;
|
||||
@@ -335,15 +875,16 @@
|
||||
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Config/Widget.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
CURRENT_PROJECT_VERSION = 30;
|
||||
DEVELOPMENT_TEAM = F3BE6NE7U3;
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
INFOPLIST_FILE = Config/Widget-Info.plist;
|
||||
INFOPLIST_FILE = "Config/Widget-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
MARKETING_VERSION = 1.1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.apt.fuelboard.widgets;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
@@ -359,15 +900,16 @@
|
||||
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Config/Widget.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
CURRENT_PROJECT_VERSION = 30;
|
||||
DEVELOPMENT_TEAM = F3BE6NE7U3;
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
INFOPLIST_FILE = Config/Widget-Info.plist;
|
||||
INFOPLIST_FILE = "Config/Widget-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
MARKETING_VERSION = 1.1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.apt.fuelboard.widgets;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
@@ -377,14 +919,69 @@
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
FBA000000000000000000401 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 23;
|
||||
DEVELOPMENT_TEAM = F3BE6NE7U3;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = FuelBoardSharedTests;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.apt.fuelboard.sharedtests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = 1;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
FBA000000000000000000402 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 23;
|
||||
DEVELOPMENT_TEAM = F3BE6NE7U3;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = FuelBoardSharedTests;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.apt.fuelboard.sharedtests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = 1;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
BB0000000000000000000017 /* Build configuration list for PBXProject "FuelBoard" */ = {
|
||||
ADCE5B593033A32B008C131C /* Build configuration list for PBXNativeTarget "FuelBoardWatch Watch App Extension" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
BB0000000000000000000018 /* Debug */,
|
||||
BB0000000000000000000019 /* Release */,
|
||||
ADCE5B573033A32B008C131C /* Debug */,
|
||||
ADCE5B583033A32B008C131C /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
ADCE5B6A3033A32D008C131C /* Build configuration list for PBXNativeTarget "FuelBoardWatch Watch App" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
ADCE5B6B3033A32D008C131C /* Debug */,
|
||||
ADCE5B6C3033A32D008C131C /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
@@ -407,6 +1004,24 @@
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
BB0000000000000000000017 /* Build configuration list for PBXProject "FuelBoard" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
BB0000000000000000000018 /* Debug */,
|
||||
BB0000000000000000000019 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
FBA000000000000000000400 /* Build configuration list for PBXNativeTarget "FuelBoardSharedTests" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
FBA000000000000000000401 /* Debug */,
|
||||
FBA000000000000000000402 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = BB0000000000000000000004 /* Project object */;
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "2600"
|
||||
version = "1.8">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BB000000000000000000000F"
|
||||
BuildableName = "FuelBoard.app"
|
||||
BlueprintName = "FuelBoard"
|
||||
ReferencedContainer = "container:FuelBoard.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "FBA000000000000000000300"
|
||||
BuildableName = "FuelBoardSharedTests.xctest"
|
||||
BlueprintName = "FuelBoardSharedTests"
|
||||
ReferencedContainer = "container:FuelBoard.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
<StoreKitConfigurationFileReference
|
||||
identifier = "../../../FuelBoardTips.storekit">
|
||||
</StoreKitConfigurationFileReference>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
language = "en"
|
||||
region = "GB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersion = "1.0"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BB000000000000000000000F"
|
||||
BuildableName = "FuelBoard.app"
|
||||
BlueprintName = "FuelBoard"
|
||||
ReferencedContainer = "container:FuelBoard.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersion = "1.0"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BB000000000000000000000F"
|
||||
BuildableName = "FuelBoard.app"
|
||||
BlueprintName = "FuelBoard"
|
||||
ReferencedContainer = "container:FuelBoard.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
@@ -6,12 +6,14 @@ import SwiftUI
|
||||
/// versa). The Live Activity section also lives here — it mirrors this page's
|
||||
/// shape (toggle + fuel + distance) so the two "notify me while driving"
|
||||
/// surfaces sit together. Geofence plumbing (count, 18-limit) lives in
|
||||
/// Settings → Debug; the user-facing page keeps a plain-language explainer
|
||||
/// plus a 0-stations hint.
|
||||
/// Settings → Debug; the user-facing page keeps short descriptions of the
|
||||
/// cheapest-station rule and radius, plus a 0-stations hint.
|
||||
struct AlertsView: View {
|
||||
@Binding var enabled: Bool
|
||||
@Binding var radius: Double // stored in km (monitor + storage)
|
||||
@Binding var fuel: FuelType // the fuel alerts monitor for
|
||||
@Binding var favouriteDropEnabled: Bool
|
||||
@Binding var favouriteDropFuel: FuelType
|
||||
/// Whether alerts mirror the Stations-tab search distance (capped at 8 mi).
|
||||
@Binding var followsSearch: Bool
|
||||
/// Whether the "cheapest nearby" Live Activity is shown on the Lock Screen
|
||||
@@ -30,12 +32,13 @@ struct AlertsView: View {
|
||||
let distanceUnit: DistanceUnit
|
||||
let monitoredCount: Int
|
||||
let lastAlert: String?
|
||||
let onCheckFavouriteDropNow: () -> Void
|
||||
|
||||
/// The Stations-tab search distance shown in the user's unit, e.g.
|
||||
/// "5 miles" or "8 km" — for the "Follow search" rows.
|
||||
private var searchInUnitLabel: String {
|
||||
let shown = distanceUnit.displayMiles(stationLimit) // stationLimit is always miles
|
||||
return "\(shown) \(distanceUnit.label(for: Double(shown)))"
|
||||
return String(format: String(localized: "%lld %@", comment: "Distance label"), shown, distanceUnit.label(for: Double(shown)))
|
||||
}
|
||||
|
||||
/// A miles value (5/10/15) shown truthfully in the user's unit, e.g.
|
||||
@@ -43,7 +46,33 @@ struct AlertsView: View {
|
||||
/// what the option really means.
|
||||
private func distanceLabel(forMiles miles: Int) -> String {
|
||||
let shown = distanceUnit.displayMiles(miles)
|
||||
return "\(shown) \(distanceUnit.label(for: Double(shown)))"
|
||||
return String(format: String(localized: "%lld %@", comment: "Distance label"), shown, distanceUnit.label(for: Double(shown)))
|
||||
}
|
||||
|
||||
/// The distance the geofence actually caps "Follow search" at, in miles
|
||||
/// (FuelStore stores the cap in km so the monitor and storage are unit-free).
|
||||
private var followSearchCapMiles: Int {
|
||||
Int((FuelStore.alertFollowCapKM / FuelSavingsCalculator.kilometresPerMile).rounded())
|
||||
}
|
||||
|
||||
/// The radius the selected option really monitors, stated in the user's
|
||||
/// unit — e.g. "Get alerts when you're within 2 miles of a matching
|
||||
/// station." "Follow search" mirrors the Stations-tab distance, which the
|
||||
/// geofence caps; said plainly, and only when the search distance really
|
||||
/// exceeds the cap, so the sentence never contradicts the picker row.
|
||||
private var alertRadiusDescription: String {
|
||||
switch alertChoice.wrappedValue {
|
||||
case .followSearch:
|
||||
let capped = stationLimit > followSearchCapMiles
|
||||
let label = distanceLabel(forMiles: capped ? followSearchCapMiles : stationLimit)
|
||||
if capped {
|
||||
return String(format: String(localized: "Get alerts when you're within %@ of a matching station (your search distance is capped for reliable alerts).", comment: "Proximity alert radius description, follow search capped"), label)
|
||||
}
|
||||
return String(format: String(localized: "Get alerts when you're within %@ of a matching station.", comment: "Proximity alert radius description"), label)
|
||||
case .fixed(let value):
|
||||
let label = String(format: String(localized: "%lld %@", comment: "Distance label"), value, distanceUnit.label(for: Double(value)))
|
||||
return String(format: String(localized: "Get alerts when you're within %@ of a matching station.", comment: "Proximity alert radius description"), label)
|
||||
}
|
||||
}
|
||||
|
||||
private enum AlertRadiusChoice: Hashable {
|
||||
@@ -99,60 +128,156 @@ struct AlertsView: View {
|
||||
)
|
||||
}
|
||||
|
||||
/// The radius currently selected, in the user's unit — shown by the disabled
|
||||
/// row in place of the picker.
|
||||
private var alertRadiusValueLabel: String {
|
||||
switch alertChoice.wrappedValue {
|
||||
case .followSearch:
|
||||
return String(format: String(localized: "Follow search (%@)", comment: "Follow current station search distance option"), searchInUnitLabel)
|
||||
case .fixed(let value):
|
||||
return String(format: String(localized: "%lld %@", comment: "Distance label"), value, distanceUnit.label(for: Double(value)))
|
||||
}
|
||||
}
|
||||
|
||||
private var laDistanceValueLabel: String {
|
||||
switch laChoice.wrappedValue {
|
||||
case .followSearch:
|
||||
return String(format: String(localized: "Follow search (%@)", comment: "Follow current station search distance option"), searchInUnitLabel)
|
||||
case .fixed(let miles):
|
||||
return distanceLabel(forMiles: miles)
|
||||
}
|
||||
}
|
||||
|
||||
/// An option row that belongs to a toggle. When the toggle is off the control
|
||||
/// is replaced by a static value row. The enclosing dependent group applies
|
||||
/// FuelBoard's shared disabled interaction and appearance once.
|
||||
@ViewBuilder
|
||||
private func optionRow<Content: View>(_ title: String,
|
||||
value: String,
|
||||
isEnabled: Bool,
|
||||
@ViewBuilder picker: () -> Content) -> some View {
|
||||
if isEnabled {
|
||||
picker()
|
||||
} else {
|
||||
HStack {
|
||||
Text(title)
|
||||
Spacer()
|
||||
Text(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
List {
|
||||
Section {
|
||||
Toggle("Cheapest-station alerts", isOn: $enabled)
|
||||
Picker("Fuel", selection: $fuel) {
|
||||
ForEach(FuelType.allCases) { fuel in
|
||||
Text(fuel.displayName).tag(fuel)
|
||||
Toggle(String(localized: "Cheapest favourite", comment: "Favourite price-drop alert toggle"), isOn: $favouriteDropEnabled)
|
||||
Group {
|
||||
optionRow(String(localized: "Fuel", comment: "Fuel picker title"),
|
||||
value: favouriteDropFuel.displayName,
|
||||
isEnabled: favouriteDropEnabled) {
|
||||
Picker(String(localized: "Fuel", comment: "Fuel picker title"), selection: $favouriteDropFuel) {
|
||||
ForEach(FuelType.allCases) { fuel in
|
||||
Text(fuel.displayName).tag(fuel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Picker("Radius", selection: alertChoice) {
|
||||
Text("Follow search (\(searchInUnitLabel))").tag(AlertRadiusChoice.followSearch)
|
||||
ForEach(FuelStore.alertRadiusOptions, id: \.self) { value in
|
||||
Text("\(value) \(distanceUnit.label(for: Double(value)))").tag(AlertRadiusChoice.fixed(value))
|
||||
}
|
||||
}
|
||||
if enabled && monitoredCount == 0 {
|
||||
Text("No stations monitored yet — open the Stations tab to load prices first.")
|
||||
.font(.caption)
|
||||
Button(String(localized: "Check now", comment: "Manual favourite price-drop check button"), action: onCheckFavouriteDropNow)
|
||||
Text(String(localized: "Get notified when a different favourite becomes the cheapest, or your cheapest favourite drops in price.", comment: "Favourite price alerts section description"))
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.fuelBoardDisabledState(isEnabled: favouriteDropEnabled)
|
||||
} header: {
|
||||
Text("Cheapest-station alerts")
|
||||
} footer: {
|
||||
Text("When you approach a station that is the cheapest within the radius, FuelBoard sends a notification — even with the app closed. Nearby stations selling \(fuel.displayName.lowercased()) are watched — favourites get priority — and each station alerts at most once per hour. Follow search mirrors the Stations-tab distance, capped at 8 miles for reliable geofencing.")
|
||||
Text(String(localized: "Favourite price alerts", comment: "Favourite price alerts section title"))
|
||||
}
|
||||
|
||||
Section {
|
||||
Toggle(String(localized: "Cheapest station", comment: "Proximity alert toggle"), isOn: $enabled)
|
||||
Group {
|
||||
Text(String(localized: "Get notified when you're near one of the cheapest stations for your chosen fuel.", comment: "Cheapest station proximity alert description"))
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
optionRow(String(localized: "Fuel", comment: "Fuel picker title"),
|
||||
value: fuel.displayName,
|
||||
isEnabled: enabled) {
|
||||
Picker(String(localized: "Fuel", comment: "Fuel picker title"), selection: $fuel) {
|
||||
ForEach(FuelType.allCases) { fuel in
|
||||
Text(fuel.displayName).tag(fuel)
|
||||
}
|
||||
}
|
||||
}
|
||||
optionRow(String(localized: "Radius", comment: "Alert radius picker title"),
|
||||
value: alertRadiusValueLabel,
|
||||
isEnabled: enabled) {
|
||||
Picker(String(localized: "Radius", comment: "Alert radius picker title"), selection: alertChoice) {
|
||||
Text(String(format: String(localized: "Follow search (%@)", comment: "Follow current station search distance option"), searchInUnitLabel)).tag(AlertRadiusChoice.followSearch)
|
||||
ForEach(FuelStore.alertRadiusOptions, id: \.self) { value in
|
||||
Text(String(format: String(localized: "%lld %@", comment: "Distance label"), value, distanceUnit.label(for: Double(value)))).tag(AlertRadiusChoice.fixed(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
Text(alertRadiusDescription)
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
if enabled && monitoredCount == 0 {
|
||||
Text(String(localized: "No stations monitored yet — open the Stations tab to load prices first.", comment: "No monitored stations hint"))
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
Text(String(localized: "Background alerts depend on when your iPhone allows them; each station alerts at most once an hour.", comment: "Proximity alerts background limitation note"))
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.fuelBoardDisabledState(isEnabled: enabled)
|
||||
} header: {
|
||||
Text(String(localized: "Proximity alerts", comment: "Proximity alerts section title"))
|
||||
}
|
||||
|
||||
if enabled, let lastAlert {
|
||||
Section("Last alert") {
|
||||
Section(String(localized: "Last alert", comment: "Last alert section title")) {
|
||||
Label(lastAlert, systemImage: "bell.badge.fill")
|
||||
.font(.footnote)
|
||||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
Toggle("Live Activity", isOn: $liveActivityEnabled)
|
||||
Picker("Fuel", selection: $liveActivityFuel) {
|
||||
ForEach(FuelType.allCases) { fuel in
|
||||
Text(fuel.displayName).tag(fuel)
|
||||
Toggle(String(localized: "Cheapest station", comment: "Live Activity toggle"), isOn: $liveActivityEnabled)
|
||||
Group {
|
||||
optionRow(String(localized: "Fuel", comment: "Fuel picker title"),
|
||||
value: liveActivityFuel.displayName,
|
||||
isEnabled: liveActivityEnabled) {
|
||||
Picker(String(localized: "Fuel", comment: "Fuel picker title"), selection: $liveActivityFuel) {
|
||||
ForEach(FuelType.allCases) { fuel in
|
||||
Text(fuel.displayName).tag(fuel)
|
||||
}
|
||||
}
|
||||
}
|
||||
Picker("Distance", selection: laChoice) {
|
||||
Text("Follow search (\(searchInUnitLabel))").tag(LADistanceChoice.followSearch)
|
||||
ForEach(FuelStore.stationRadiusOptions, id: \.self) { miles in
|
||||
Text(distanceLabel(forMiles: miles)).tag(LADistanceChoice.fixed(miles))
|
||||
optionRow(String(localized: "Distance", comment: "Distance picker title"),
|
||||
value: laDistanceValueLabel,
|
||||
isEnabled: liveActivityEnabled) {
|
||||
Picker(String(localized: "Distance", comment: "Distance picker title"), selection: laChoice) {
|
||||
Text(String(format: String(localized: "Follow search (%@)", comment: "Follow current station search distance option"), searchInUnitLabel)).tag(LADistanceChoice.followSearch)
|
||||
ForEach(FuelStore.stationRadiusOptions, id: \.self) { miles in
|
||||
Text(distanceLabel(forMiles: miles)).tag(LADistanceChoice.fixed(miles))
|
||||
}
|
||||
}
|
||||
}
|
||||
if liveActivityFollowsSearch {
|
||||
Text(String(localized: "Uses the same distance as your station search.", comment: "Live Activity distance description, follow search"))
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
Text(String(localized: "See the cheapest nearby station on your Lock Screen and Dynamic Island, with updates as you travel.", comment: "Live Activity section description"))
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.fuelBoardDisabledState(isEnabled: liveActivityEnabled)
|
||||
} header: {
|
||||
Text("Live Activity")
|
||||
} footer: {
|
||||
Text("Shows the cheapest station for the fuel and distance you pick here on the Lock Screen and Dynamic Island — independent of the Stations tab (choose Follow search to mirror its distance instead). Updates as you drive; tap to open directions. Live Activities can be disabled in System Settings → Live Activities.")
|
||||
Text(String(localized: "Live Activity", comment: "Live Activity section title"))
|
||||
}
|
||||
}
|
||||
.navigationTitle("Alerts")
|
||||
.navigationTitle(String(localized: "Alerts", comment: "Alerts tab title"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 267 KiB After Width: | Height: | Size: 208 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 747 KiB |
@@ -1,38 +1,170 @@
|
||||
{
|
||||
"images" : [
|
||||
"images": [
|
||||
{
|
||||
"filename" : "AppIcon.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
"filename": "AppIcon.png",
|
||||
"idiom": "universal",
|
||||
"platform": "ios",
|
||||
"size": "1024x1024"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
"appearances": [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
"appearance": "luminosity",
|
||||
"value": "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "AppIcon-dark.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
"filename": "AppIcon-dark.png",
|
||||
"idiom": "universal",
|
||||
"platform": "ios",
|
||||
"size": "1024x1024"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
"appearances": [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "tinted"
|
||||
"appearance": "luminosity",
|
||||
"value": "tinted"
|
||||
}
|
||||
],
|
||||
"filename" : "AppIcon-tinted.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
"filename": "AppIcon-tinted.png",
|
||||
"idiom": "universal",
|
||||
"platform": "ios",
|
||||
"size": "1024x1024"
|
||||
},
|
||||
{
|
||||
"filename": "Watch-NotificationCenter-48.png",
|
||||
"idiom": "watch",
|
||||
"role": "notificationCenter",
|
||||
"scale": "2x",
|
||||
"size": "24x24",
|
||||
"subtype": "38mm"
|
||||
},
|
||||
{
|
||||
"filename": "Watch-NotificationCenter-55.png",
|
||||
"idiom": "watch",
|
||||
"role": "notificationCenter",
|
||||
"scale": "2x",
|
||||
"size": "27.5x27.5",
|
||||
"subtype": "42mm"
|
||||
},
|
||||
{
|
||||
"filename": "Watch-NotificationCenter-66.png",
|
||||
"idiom": "watch",
|
||||
"role": "notificationCenter",
|
||||
"scale": "2x",
|
||||
"size": "33x33",
|
||||
"subtype": "45mm"
|
||||
},
|
||||
{
|
||||
"filename": "Watch-CompanionSettings-58.png",
|
||||
"idiom": "watch",
|
||||
"role": "companionSettings",
|
||||
"scale": "2x",
|
||||
"size": "29x29"
|
||||
},
|
||||
{
|
||||
"filename": "Watch-CompanionSettings-87.png",
|
||||
"idiom": "watch",
|
||||
"role": "companionSettings",
|
||||
"scale": "3x",
|
||||
"size": "29x29"
|
||||
},
|
||||
{
|
||||
"filename": "Watch-AppLauncher-80.png",
|
||||
"idiom": "watch",
|
||||
"role": "appLauncher",
|
||||
"scale": "2x",
|
||||
"size": "40x40",
|
||||
"subtype": "38mm"
|
||||
},
|
||||
{
|
||||
"filename": "Watch-AppLauncher-88.png",
|
||||
"idiom": "watch",
|
||||
"role": "appLauncher",
|
||||
"scale": "2x",
|
||||
"size": "44x44",
|
||||
"subtype": "40mm"
|
||||
},
|
||||
{
|
||||
"filename": "Watch-AppLauncher-92.png",
|
||||
"idiom": "watch",
|
||||
"role": "appLauncher",
|
||||
"scale": "2x",
|
||||
"size": "46x46",
|
||||
"subtype": "41mm"
|
||||
},
|
||||
{
|
||||
"filename": "Watch-AppLauncher-100.png",
|
||||
"idiom": "watch",
|
||||
"role": "appLauncher",
|
||||
"scale": "2x",
|
||||
"size": "50x50",
|
||||
"subtype": "44mm"
|
||||
},
|
||||
{
|
||||
"filename": "Watch-AppLauncher-102.png",
|
||||
"idiom": "watch",
|
||||
"role": "appLauncher",
|
||||
"scale": "2x",
|
||||
"size": "51x51",
|
||||
"subtype": "45mm"
|
||||
},
|
||||
{
|
||||
"filename": "Watch-AppLauncher-108.png",
|
||||
"idiom": "watch",
|
||||
"role": "appLauncher",
|
||||
"scale": "2x",
|
||||
"size": "54x54",
|
||||
"subtype": "49mm"
|
||||
},
|
||||
{
|
||||
"filename": "Watch-QuickLook-172.png",
|
||||
"idiom": "watch",
|
||||
"role": "quickLook",
|
||||
"scale": "2x",
|
||||
"size": "86x86",
|
||||
"subtype": "38mm"
|
||||
},
|
||||
{
|
||||
"filename": "Watch-QuickLook-196.png",
|
||||
"idiom": "watch",
|
||||
"role": "quickLook",
|
||||
"scale": "2x",
|
||||
"size": "98x98",
|
||||
"subtype": "42mm"
|
||||
},
|
||||
{
|
||||
"filename": "Watch-QuickLook-216.png",
|
||||
"idiom": "watch",
|
||||
"role": "quickLook",
|
||||
"scale": "2x",
|
||||
"size": "108x108",
|
||||
"subtype": "44mm"
|
||||
},
|
||||
{
|
||||
"filename": "Watch-QuickLook-234.png",
|
||||
"idiom": "watch",
|
||||
"role": "quickLook",
|
||||
"scale": "2x",
|
||||
"size": "117x117",
|
||||
"subtype": "45mm"
|
||||
},
|
||||
{
|
||||
"filename": "Watch-QuickLook-258.png",
|
||||
"idiom": "watch",
|
||||
"role": "quickLook",
|
||||
"scale": "2x",
|
||||
"size": "129x129",
|
||||
"subtype": "49mm"
|
||||
},
|
||||
{
|
||||
"filename": "Watch-Marketing-1024.png",
|
||||
"idiom": "watch-marketing",
|
||||
"scale": "1x",
|
||||
"size": "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
"info": {
|
||||
"author": "xcode",
|
||||
"version": 1
|
||||
}
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 7.9 KiB |
|
After Width: | Height: | Size: 9.1 KiB |
|
After Width: | Height: | Size: 9.6 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 747 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 747 KiB |
@@ -4,6 +4,14 @@
|
||||
"filename" : "AppIconArtwork.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
|
||||
|
After Width: | Height: | Size: 747 KiB |
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "AppIconPreview.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 208 KiB |
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "AppIconPreviewDark.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"data" : [
|
||||
{
|
||||
"filename" : "fuelboard_dump.json",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "PreviewWallpaper.jpg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 177 KiB |
@@ -1,18 +1,22 @@
|
||||
{
|
||||
"images": [
|
||||
"images" : [
|
||||
{
|
||||
"filename": "brand_applegreen_2x.png",
|
||||
"idiom": "universal",
|
||||
"scale": "2x"
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename": "brand_applegreen_3x.png",
|
||||
"idiom": "universal",
|
||||
"scale": "3x"
|
||||
"filename" : "brand_applegreen_2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "brand_applegreen_3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"author": "xcode",
|
||||
"version": 1
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
{
|
||||
"images": [
|
||||
"images" : [
|
||||
{
|
||||
"filename": "brand_asda_2x.png",
|
||||
"idiom": "universal",
|
||||
"scale": "2x"
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename": "brand_asda_3x.png",
|
||||
"idiom": "universal",
|
||||
"scale": "3x"
|
||||
"filename" : "brand_asda_2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "brand_asda_3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"author": "xcode",
|
||||
"version": 1
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
{
|
||||
"images": [
|
||||
"images" : [
|
||||
{
|
||||
"filename": "brand_bp_2x.png",
|
||||
"idiom": "universal",
|
||||
"scale": "2x"
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename": "brand_bp_3x.png",
|
||||
"idiom": "universal",
|
||||
"scale": "3x"
|
||||
"filename" : "brand_bp_2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "brand_bp_3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"author": "xcode",
|
||||
"version": 1
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "brand_circle_k_2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "brand_circle_k_3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "brand_essar_2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "brand_essar_3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
@@ -1,18 +1,22 @@
|
||||
{
|
||||
"images": [
|
||||
"images" : [
|
||||
{
|
||||
"filename": "brand_esso_2x.png",
|
||||
"idiom": "universal",
|
||||
"scale": "2x"
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename": "brand_esso_3x.png",
|
||||
"idiom": "universal",
|
||||
"scale": "3x"
|
||||
"filename" : "brand_esso_2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "brand_esso_3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"author": "xcode",
|
||||
"version": 1
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "brand_gleaner_2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "brand_gleaner_3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
@@ -1,18 +1,22 @@
|
||||
{
|
||||
"images": [
|
||||
"images" : [
|
||||
{
|
||||
"filename": "brand_gulf_2x.png",
|
||||
"idiom": "universal",
|
||||
"scale": "2x"
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename": "brand_gulf_3x.png",
|
||||
"idiom": "universal",
|
||||
"scale": "3x"
|
||||
"filename" : "brand_gulf_2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "brand_gulf_3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"author": "xcode",
|
||||
"version": 1
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "brand_highland_fuels_2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "brand_highland_fuels_3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 636 B |
|
After Width: | Height: | Size: 970 B |
@@ -1,18 +1,22 @@
|
||||
{
|
||||
"images": [
|
||||
"images" : [
|
||||
{
|
||||
"filename": "brand_jet_2x.png",
|
||||
"idiom": "universal",
|
||||
"scale": "2x"
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename": "brand_jet_3x.png",
|
||||
"idiom": "universal",
|
||||
"scale": "3x"
|
||||
"filename" : "brand_jet_2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "brand_jet_3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"author": "xcode",
|
||||
"version": 1
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "brand_maxol_2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "brand_maxol_3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 7.8 KiB |
@@ -1,18 +1,22 @@
|
||||
{
|
||||
"images": [
|
||||
"images" : [
|
||||
{
|
||||
"filename": "brand_morrisons_2x.png",
|
||||
"idiom": "universal",
|
||||
"scale": "2x"
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename": "brand_morrisons_3x.png",
|
||||
"idiom": "universal",
|
||||
"scale": "3x"
|
||||
"filename" : "brand_morrisons_2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "brand_morrisons_3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"author": "xcode",
|
||||
"version": 1
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "brand_murco_2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "brand_murco_3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 12 KiB |
@@ -1,18 +1,22 @@
|
||||
{
|
||||
"images": [
|
||||
"images" : [
|
||||
{
|
||||
"filename": "brand_sainsburys_2x.png",
|
||||
"idiom": "universal",
|
||||
"scale": "2x"
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename": "brand_sainsburys_3x.png",
|
||||
"idiom": "universal",
|
||||
"scale": "3x"
|
||||
"filename" : "brand_sainsburys_2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "brand_sainsburys_3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"author": "xcode",
|
||||
"version": 1
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
{
|
||||
"images": [
|
||||
"images" : [
|
||||
{
|
||||
"filename": "brand_shell_2x.png",
|
||||
"idiom": "universal",
|
||||
"scale": "2x"
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename": "brand_shell_3x.png",
|
||||
"idiom": "universal",
|
||||
"scale": "3x"
|
||||
"filename" : "brand_shell_2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "brand_shell_3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"author": "xcode",
|
||||
"version": 1
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "brand_spar_2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "brand_spar_3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
@@ -1,18 +1,22 @@
|
||||
{
|
||||
"images": [
|
||||
"images" : [
|
||||
{
|
||||
"filename": "brand_tesco_2x.png",
|
||||
"idiom": "universal",
|
||||
"scale": "2x"
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename": "brand_tesco_3x.png",
|
||||
"idiom": "universal",
|
||||
"scale": "3x"
|
||||
"filename" : "brand_tesco_2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "brand_tesco_3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"author": "xcode",
|
||||
"version": 1
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
{
|
||||
"images": [
|
||||
"images" : [
|
||||
{
|
||||
"filename": "brand_texaco_2x.png",
|
||||
"idiom": "universal",
|
||||
"scale": "2x"
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename": "brand_texaco_3x.png",
|
||||
"idiom": "universal",
|
||||
"scale": "3x"
|
||||
"filename" : "brand_texaco_2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "brand_texaco_3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"author": "xcode",
|
||||
"version": 1
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "brand_the_co_operative_2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "brand_the_co_operative_3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "brand_valero_2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "brand_valero_3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 8.6 KiB |
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "brand_welcome_break_2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "brand_welcome_break_3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
@@ -0,0 +1,313 @@
|
||||
import SwiftUI
|
||||
|
||||
/// The Best Value page body: what the feature does, whether to switch it on, and
|
||||
/// — only when it is on — the two figures the ranking needs, entered with the
|
||||
/// same sliders Settings uses (one control, no text entry, so a first-run user
|
||||
/// can't stall on a keyboard).
|
||||
///
|
||||
/// Shared deliberately. First-run onboarding shows it as one of its pages, and
|
||||
/// existing users who updated into the feature get the same page as a
|
||||
/// once-per-announcement-version announcement (`BestValueIntroView`). Two copies
|
||||
/// would drift the first time either was edited, so the copy, the controls and
|
||||
/// the "declining is a decision" rule all live here.
|
||||
///
|
||||
/// Settings are written to the store as the controls move — the same
|
||||
/// immediate-persist rule as Settings — so a host dismissed mid-page cannot lose
|
||||
/// a choice the user has already made.
|
||||
struct BestValueIntroContent: View {
|
||||
@Binding var settings: FuelSavingsSettings
|
||||
|
||||
/// What the host's primary button says. ONE label for both answers: the
|
||||
/// toggle already shows whether Best Value is being switched on, so a
|
||||
/// separate "Not now" for the declined case was second-guessing a choice the
|
||||
/// user could see on screen.
|
||||
static var primaryTitle: String {
|
||||
String(localized: "Continue", comment: "Best Value continue button")
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
Spacer(minLength: 8)
|
||||
|
||||
AnimatedOnboardingIcon(tint: .green) {
|
||||
Image(systemName: "sterlingsign.circle.fill")
|
||||
.font(.system(size: 40, weight: .semibold))
|
||||
.foregroundStyle(.green)
|
||||
}
|
||||
.padding(.bottom, 20)
|
||||
|
||||
Text(String(localized: "Best Value", comment: "Best Value title"))
|
||||
.font(.largeTitle.bold())
|
||||
|
||||
Text(String(localized: "The cheapest pump isn't always the cheapest fill-up — driving further burns fuel. Best Value sorts stations by what you'd really save once the trip is counted.", comment: "Best Value explanation"))
|
||||
.font(.body)
|
||||
.foregroundStyle(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal, 28)
|
||||
.padding(.top, 10)
|
||||
|
||||
card
|
||||
.padding(.horizontal, 24)
|
||||
.padding(.top, 16)
|
||||
|
||||
Spacer(minLength: 8)
|
||||
}
|
||||
}
|
||||
|
||||
private var card: some View {
|
||||
VStack(spacing: 14) {
|
||||
Toggle(isOn: enabledBinding) {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(String(localized: "Turn on Best Value", comment: "Best Value toggle"))
|
||||
.font(.headline)
|
||||
Text(String(localized: "Sort by what a station saves you, travel included.", comment: "Best Value toggle detail"))
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
if settings.enabled {
|
||||
slider(
|
||||
title: String(localized: "Average fuel economy", comment: "Best Value MPG slider label"),
|
||||
readOut: mpgText,
|
||||
value: mpgBinding,
|
||||
range: FuelSavingsCalculator.mpgSliderRange,
|
||||
step: FuelSavingsCalculator.mpgSliderStep
|
||||
)
|
||||
|
||||
Picker("Fill-up unit", selection: unitBinding) {
|
||||
ForEach(FuelSavingsFillUnit.allCases) { unit in
|
||||
Text(unit.displayName).tag(unit)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
|
||||
slider(
|
||||
title: String(localized: "Fill-up amount", comment: "Best Value fill-up slider label"),
|
||||
readOut: fillUpText,
|
||||
value: amountBinding,
|
||||
range: FuelSavingsCalculator.fillAmountSliderRange(for: settings.preferredAmount.unit),
|
||||
step: FuelSavingsCalculator.fillAmountSliderStep(for: settings.preferredAmount.unit)
|
||||
)
|
||||
|
||||
Text(String(localized: "Best Value uses these, and can be changed any time in Settings.", comment: "Best Value configured note"))
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
.frame(maxWidth: .infinity)
|
||||
} else {
|
||||
Text(String(localized: "That's fine — you can turn Best Value on whenever you like in Settings, under Fuel Savings.", comment: "Best Value declined note"))
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
.padding(16)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 16, style: .continuous)
|
||||
.fill(Color(.secondarySystemBackground))
|
||||
)
|
||||
}
|
||||
|
||||
/// The sliders are the only controls for these two figures, exactly as in
|
||||
/// Settings: a labelled read-out above a slider, no text field.
|
||||
private func slider(title: String,
|
||||
readOut: String,
|
||||
value: Binding<Double>,
|
||||
range: ClosedRange<Double>,
|
||||
step: Double) -> some View {
|
||||
VStack(spacing: 2) {
|
||||
HStack {
|
||||
Text(title)
|
||||
.font(.subheadline)
|
||||
Spacer()
|
||||
Text(readOut)
|
||||
.font(.subheadline)
|
||||
.monospacedDigit()
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
Slider(value: value, in: range, step: step)
|
||||
.accessibilityLabel(title)
|
||||
.accessibilityValue(readOut)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Bindings (persist immediately, exactly like Settings)
|
||||
|
||||
private var enabledBinding: Binding<Bool> {
|
||||
Binding(
|
||||
get: { settings.enabled },
|
||||
set: { settings.enabled = $0; persist() }
|
||||
)
|
||||
}
|
||||
|
||||
private var mpgBinding: Binding<Double> {
|
||||
Binding(
|
||||
get: { settings.mpg },
|
||||
set: {
|
||||
settings.mpg = min(max($0, 0), FuelSavingsCalculator.maximumMPG)
|
||||
persist()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private var amountBinding: Binding<Double> {
|
||||
Binding(
|
||||
get: { settings.preferredAmount.value },
|
||||
set: {
|
||||
settings.preferredAmount.value = min(
|
||||
max($0, 0),
|
||||
FuelSavingsCalculator.maximumFillAmount(for: settings.preferredAmount.unit)
|
||||
)
|
||||
persist()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private var unitBinding: Binding<FuelSavingsFillUnit> {
|
||||
Binding(
|
||||
get: { settings.preferredAmount.unit },
|
||||
set: { newUnit in
|
||||
settings.preferredAmount.unit = newUnit
|
||||
// Same guard as Settings: a figure valid in one unit may be
|
||||
// valid in neither after the switch, so fall back to a sensible
|
||||
// default for the new unit rather than storing junk.
|
||||
if !settings.preferredAmount.isValid {
|
||||
settings.preferredAmount.value = newUnit == .pounds ? 40 : 30
|
||||
}
|
||||
persist()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private func persist() {
|
||||
FuelStore.saveFuelSavingsSettings(settings)
|
||||
}
|
||||
|
||||
/// "45 MPG" / "45.5 MPG" — the slider is the only control, so this read-out
|
||||
/// is the one place the figure is stated.
|
||||
private var mpgText: String {
|
||||
let mpg = settings.mpg
|
||||
return mpg.rounded() == mpg ? String(format: "%.0f MPG", mpg) : String(format: "%.1f MPG", mpg)
|
||||
}
|
||||
|
||||
/// "£40" / "40 L" — the fill-up slider's read-out.
|
||||
private var fillUpText: String {
|
||||
switch settings.preferredAmount.unit {
|
||||
case .pounds: return String(format: "£%.0f", settings.preferredAmount.value)
|
||||
case .litres: return String(format: "%.0f L", settings.preferredAmount.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The announcement existing users see once per announcement version: the Best
|
||||
/// Value page on its own. Deliberately NOT the onboarding pager — no page dots
|
||||
/// and no permission pages, because those users answered all of that long ago
|
||||
/// and the full flow is mandatory with no skip.
|
||||
struct BestValueIntroView: View {
|
||||
var onFinish: () -> Void
|
||||
|
||||
@State private var settings = FuelStore.loadFuelSavingsSettings()
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
BestValueIntroContent(settings: $settings)
|
||||
|
||||
OnboardingPrimaryButton(title: BestValueIntroContent.primaryTitle,
|
||||
action: finish)
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.bottom, 24)
|
||||
}
|
||||
.background(
|
||||
LinearGradient(
|
||||
colors: [Color(.systemBackground), Color.accentColor.opacity(0.06)],
|
||||
startPoint: .top, endPoint: .bottom
|
||||
)
|
||||
.ignoresSafeArea()
|
||||
)
|
||||
}
|
||||
|
||||
private func finish() {
|
||||
// Marked on the way OUT, so a crash or force-quit mid-page re-shows the
|
||||
// announcement instead of silently swallowing it.
|
||||
FuelStore.markBestValueIntroSeen()
|
||||
onFinish()
|
||||
}
|
||||
}
|
||||
|
||||
/// Presents the Best Value announcement over the app, exactly once per
|
||||
/// announcement version. It wraps whatever it is given, so the whole feature —
|
||||
/// state, gate and dismissal — lives in this file and ContentView only has to
|
||||
/// install it.
|
||||
///
|
||||
/// The gate, in order:
|
||||
/// - suppressed under `-skipOnboarding` (DEBUG), so screenshot runs can open a
|
||||
/// tab without a cover interrupting the capture;
|
||||
/// - forced by `-showBestValueIntro` (DEBUG);
|
||||
/// - never while first-run onboarding is due or on screen — that flow contains
|
||||
/// the same page, and `OnboardingView.finish` marks this seen;
|
||||
/// - never when the stored announcement version is current;
|
||||
/// - never for someone who has already configured Best Value themselves.
|
||||
struct BestValueAnnouncementHost<Content: View>: View {
|
||||
/// True while the first-run onboarding cover is up: the announcement must
|
||||
/// not fight it for the screen.
|
||||
var isSuppressed: Bool = false
|
||||
var onDismiss: () -> Void = {}
|
||||
@ViewBuilder var content: Content
|
||||
|
||||
@State private var presenting = false
|
||||
|
||||
var body: some View {
|
||||
content
|
||||
.fullScreenCover(isPresented: $presenting) {
|
||||
BestValueIntroView {
|
||||
presenting = false
|
||||
}
|
||||
}
|
||||
// `task`, not `onAppear`: runs once when the app's content appears,
|
||||
// after ContentView has made its onboarding decision.
|
||||
.task {
|
||||
guard !presenting, shouldAnnounce else { return }
|
||||
presenting = true
|
||||
}
|
||||
.onChange(of: presenting) { _, showing in
|
||||
guard !showing else { return }
|
||||
// The page writes straight to the store — it is its own cover
|
||||
// with its own copy of the settings — so the app's in-memory
|
||||
// copy has to be pulled back in, or an announcement choice
|
||||
// wouldn't apply until the next launch.
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
|
||||
private var shouldAnnounce: Bool {
|
||||
#if DEBUG
|
||||
let args = ProcessInfo.processInfo.arguments
|
||||
if args.contains("-skipOnboarding") { return false }
|
||||
if args.contains("-showBestValueIntro") { return true }
|
||||
#endif
|
||||
// The rule itself lives in the store so it can be unit-tested — see
|
||||
// `FuelStore.shouldAnnounceBestValue`.
|
||||
return FuelStore.shouldAnnounceBestValue(
|
||||
settings: FuelStore.loadFuelSavingsSettings(),
|
||||
isSuppressed: isSuppressed
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// The prominent button both Best Value hosts use, so they can't drift apart.
|
||||
struct OnboardingPrimaryButton: View {
|
||||
var title: String
|
||||
var action: () -> Void
|
||||
|
||||
var body: some View {
|
||||
Button(action: action) {
|
||||
Text(title)
|
||||
.font(.headline)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 14)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// BundledDumpProvider.swift — no-network last resort (P0 live chain).
|
||||
//
|
||||
// A REAL snapshot of the mirror dump is bundled into the app (FuelBoardDump
|
||||
// dataset, refreshed before builds by scripts/refresh_bundled_dump.sh) so the
|
||||
// app never demos empty and App Review always sees real data even fully
|
||||
// offline. Same relay envelope, so the shared decode applies unchanged.
|
||||
|
||||
import UIKit
|
||||
|
||||
enum BundledDumpProvider {
|
||||
/// The bundled real snapshot, decoded with the shared relay decode —
|
||||
/// nil only if the asset is missing or corrupt (callers then fall back
|
||||
/// to the England-wide sample set).
|
||||
static var stations: [FuelStation]? {
|
||||
guard let asset = NSDataAsset(name: "FuelBoardDump") else { return nil }
|
||||
return try? FuelPriceProvider.decodeStations(from: asset.data)
|
||||
}
|
||||
|
||||
/// The GOV.UK `data_updated` stamp from the bundled envelope (ISO 8601).
|
||||
/// Labels the offline-data banner honestly ("Offline data from 15 Aug")
|
||||
/// instead of presenting the build's snapshot as live.
|
||||
static var dataUpdatedStamp: String? {
|
||||
guard let asset = NSDataAsset(name: "FuelBoardDump"),
|
||||
let obj = try? JSONSerialization.jsonObject(with: asset.data)
|
||||
as? [String: Any] else { return nil }
|
||||
return obj["data_updated"] as? String
|
||||
}
|
||||
}
|
||||