Compare commits
@@ -1,5 +1,9 @@
|
|||||||
build/
|
build/
|
||||||
|
Builds/
|
||||||
|
build-*/
|
||||||
build-release/
|
build-release/
|
||||||
|
build-sim/
|
||||||
|
build-watch/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.xcuserstate
|
*.xcuserstate
|
||||||
xcuserdata/
|
xcuserdata/
|
||||||
|
|||||||
@@ -1,86 +1,227 @@
|
|||||||
# FuelBoard App Store Approval Review
|
# FuelBoard App Store Submission Plan
|
||||||
|
|
||||||
Review date: 2026-08-11 · Target: App Store submission readiness.
|
Updated: 2026-09-26 · Target: safe App Store submission.
|
||||||
This is the companion to SECURITY.md — focuses on App Store *review* gate
|
Companion to SECURITY.md (code security) — this file is the *review-gate*
|
||||||
(guideline compliance), not code security.
|
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
|
The two 2026-08-11 blockers are RESOLVED by P0 (live chain):
|
||||||
checks out for review.
|
- ~~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.
|
||||||
|
|
||||||
## Blockers
|
## StoreKit sandbox: verified working configuration and caveats
|
||||||
|
|
||||||
### 1. Data source — LAN relay (BLOCKER)
|
### Verified configuration
|
||||||
- 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.
|
|
||||||
|
|
||||||
### 2. Always-location justification (BLOCKER without review notes)
|
The FuelBoard consumable tip products are:
|
||||||
- `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).
|
|
||||||
|
|
||||||
## 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 —
|
The app requests these exact **Product IDs** with StoreKit 2
|
||||||
1024×1024 `AppIcon.appiconset` + `ASSETCATALOG_COMPILER_APPICON_NAME`.
|
`Product.products(for:)`; the UI names are display labels and are not used for
|
||||||
- [x] **Launch screen**: `UILaunchScreen` empty dict present (iOS 14+ style).
|
lookup. The app bundle ID, App Store Connect app, explicit App ID, development
|
||||||
- [x] **Device family**: iPhone-only (`TARGETED_DEVICE_FAMILY = 1`) — no iPad
|
team, and Sandbox Apple Account must belong to the same developer account.
|
||||||
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.
|
|
||||||
|
|
||||||
## Content / IP review
|
Two test environments must remain distinct:
|
||||||
|
|
||||||
- **Brand logos (Shell, BP, Tesco…)**: bundled from Simple Icons (CC0) +
|
- **Local StoreKit testing:** select `/Users/apt/workspace/fuelboard/FuelBoardTips.storekit`
|
||||||
Wikimedia Commons. Nominative use (identifying stations) is standard practice
|
in Xcode, under Product > Scheme > Edit Scheme > Run > Options. This bypasses
|
||||||
(cf. GasBuddy). Low risk, but keep the sources documented.
|
App Store Connect and confirms the app's StoreKit loading and purchase code.
|
||||||
- **Price data**: gov.uk Fuel Finder data is OGL-licensed — include attribution
|
- **Apple sandbox testing:** select **None** for StoreKit Configuration, then
|
||||||
in the app (About/credits) once live data ships. Sample fallback data must be
|
use Product > Run on a physical device with the development-signed build and
|
||||||
clearly demo-labelled (or removed for review) so reviewers don't think the
|
a Sandbox Apple Account signed in under Settings > Developer. This exercises
|
||||||
prices are fake — 2.3.1 accuracy risk.
|
the real App Store Connect catalog.
|
||||||
- **"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.
|
|
||||||
|
|
||||||
## 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 2026-08-16** (app + widget
|
||||||
|
manifests added; UserDefaults `1C8F.1`, Location + Diagnostics declared).
|
||||||
|
4. **[ME] Real Distribution signing** — Distribution certificate + App Store
|
||||||
|
provisioning profile (or Xcode automatic signing once the account is in
|
||||||
|
Xcode); then `xcodebuild archive` (no more `CODE_SIGNING_ALLOWED=NO`).
|
||||||
|
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 manifests added (2026-08-16):
|
||||||
|
UserDefaults reason `1C8F.1`; app also declares Location (linked, not
|
||||||
|
tracking) + Diagnostics (not linked, not tracking — the dev beacon)
|
||||||
|
- [ ] Distribution archive + TestFlight upload (needs Apple account)
|
||||||
|
- [x] **Version bump** → `1.0.1` (build 2), app + widget plists
|
||||||
|
- [x] **OGL attribution** — Settings → Data source footer now reads "Prices
|
||||||
|
use the UK Fuel Finder dataset (Open Government Licence) and refresh
|
||||||
|
up to twice a day." (2.3.1 hygiene)
|
||||||
|
- [x] **Out-of-UK empty state** — "No X stations within N miles" + UK-only
|
||||||
|
explainer + "Browse all UK stations" button (switches to Closest)
|
||||||
|
- [ ] ~~Gate dev surface behind #if DEBUG~~ **DECLINED — kept in Release on
|
||||||
|
purpose**: the user's home-testing installs the Release IPA and toggles
|
||||||
|
`fuelboard.relayFallback` via Settings → Debug (revealed only by tapping
|
||||||
|
About → Version ×5 — invisible to reviewers; no credentials involved).
|
||||||
|
Gating it out of Release would break that workflow for zero review gain.
|
||||||
|
- [ ] Final pass: `swift test` green (95 tests), archive smoke test on device
|
||||||
|
- [ ] ~~Capture screenshots from simulator~~ **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; diagnostics: yes — the widget-diag beacon, not
|
||||||
|
linked, not used for 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
|
> 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
|
> using the UK government Fuel Finder dataset (Open Government Licence).
|
||||||
> twice a day; the app filters 5/10/15-mile radii on-device. Location is used
|
> Prices are downloaded from the internet up to twice a day; the app filters
|
||||||
> to (a) rank stations by distance and (b) power the optional Alerts feature:
|
> 5/10/15-mile radii on-device and works offline from a bundled snapshot.
|
||||||
> when enabled, the app geofences the cheapest station in the user's chosen
|
> Location is used to (a) rank stations by distance and (b) power the
|
||||||
> radius and sends a notification when they approach it. Always-location is
|
> optional Alerts feature: when the user enables alerts, the app geofences
|
||||||
> only requested when the user enables alerts. The app also provides a
|
> the cheapest station in their radius and sends a notification when they
|
||||||
> home-screen widget showing the current cheapest station nearby.
|
> approach it. 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
|
- **Always-location (5.1.1)** — the #1 question. The feature is visible
|
||||||
2. [ ] Always-permission flow tied to explicit user opt-in
|
(Alerts tab) and opt-in; the review notes above say so explicitly.
|
||||||
3. [ ] Bump `CFBundleShortVersionString` / `CFBundleVersion`
|
- **Reviewer outside the UK** — empty radius; mitigated by the empty-state
|
||||||
4. [ ] Sample-data fallback labelled or removed in the release build
|
task above + review notes.
|
||||||
5. [ ] OGL attribution present in-app
|
- **"Cheapest" claims (2.3.1)** — mitigated by the OGL/footnote task.
|
||||||
6. [ ] `swift test` green (FuelBoardTests, 28 tests)
|
- **Live Activities** — reviewers may ask why; answer: price alerts at a
|
||||||
7. [ ] Security audit pass (see SECURITY.md recurring checks)
|
glance. It is declared in the plist already.
|
||||||
8. [ ] Icon + screenshots for all required sizes in App Store Connect
|
- **Brand logos (Shell, BP, Tesco…)** — Simple Icons (CC0) + Wikimedia,
|
||||||
|
nominative use (cf. GasBuddy); low risk, keep sources documented.
|
||||||
|
- **GitHub raw as data source** — public data, no credentials; 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 Backlog
|
||||||
|
|
||||||
FuelBoard — England-wide petrol-station price widget + app.
|
FuelBoard — UK-wide petrol-station price widget + app.
|
||||||
Relay mirrors the UK gov data (hourly CSV); app fetches the full-UK dump
|
GitHub mirror (`aptonline/fuelboard-data`, 2×/day 10:00+16:00) holds the
|
||||||
(~8,038 stations, all prices) once per 12 h and filters/sorts on-device.
|
full-UK dump (~8,022 stations, all prices); app fetches it once per 12 h and
|
||||||
|
filters/sorts on-device, with a bundled snapshot as the no-network last resort.
|
||||||
|
LAN relay is dev-only (hidden `fuelboard.relayFallback` flag).
|
||||||
|
|
||||||
Priority guide: P0 = next build, P1 = soon, P2 = later, P3 = maybe/idea.
|
Priority guide: P0 = next build, P1 = soon, P2 = later, P3 = maybe/idea.
|
||||||
Status: TODO / IN PROGRESS / DONE / BLOCKED.
|
Status: TODO / IN PROGRESS / DONE / BLOCKED.
|
||||||
@@ -11,18 +13,60 @@ Status: TODO / IN PROGRESS / DONE / BLOCKED.
|
|||||||
|
|
||||||
## P1 — Soon
|
## 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`
|
- [x] **Siri: "Cheapest [fuel] near me"** — `AppShortcutsProvider` + `CheapestFuelIntent`
|
||||||
(App Intents, iOS 16+), dialog + price-card snippet, cached full-UK dump first
|
(App Intents, iOS 16+), dialog + price-card snippet, cached full-UK dump first
|
||||||
with freshness label ("as of 6am"), last-known-location fallback. Constraint:
|
with freshness label ("as of 6am"), last-known-location fallback. Constraint:
|
||||||
fresh-data path needs a reachable source off-LAN — cached answers work anywhere
|
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
|
the app has previously fetched (the GitHub mirror serves this now). *DONE —
|
||||||
Tailscale (100.120.217.98:8789) is reachable off-LAN for testing; relay will
|
radius-scoped
|
||||||
move to a permanent box or the GitHub mirror long-term. *DONE — radius-scoped
|
|
||||||
lookup `037c241` (real-data regression: GULF HISTON 100.9p 132 mi away),
|
lookup `037c241` (real-data regression: GULF HISTON 100.9p 132 mi away),
|
||||||
per-fuel literal-word shortcuts `e55078c`, names = app words `a3b6f37`, plus
|
per-fuel literal-word shortcuts `e55078c`, names = app words `a3b6f37`, plus
|
||||||
Directions-to-cheapest `caadf18` + spoken hand-off `aed99e3`.*
|
Directions-to-cheapest `caadf18` + spoken hand-off `aed99e3`.*
|
||||||
- [ ] **Siri: "Closest [fuel] station"** — same plumbing as cheapest, sort by
|
- [ ] **Siri: "Closest [fuel] station"** — same plumbing as cheapest, sort by
|
||||||
distance instead of price. Free second phrase in the same `AppShortcutsProvider`.
|
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
|
## P2 — Later
|
||||||
|
|
||||||
@@ -33,8 +77,10 @@ Status: TODO / IN PROGRESS / DONE / BLOCKED.
|
|||||||
`FavouriteFuelPriceIntent`: TOP favourite per fuel (first in the manual
|
`FavouriteFuelPriceIntent`: TOP favourite per fuel (first in the manual
|
||||||
Favourites order — `SiriCheapestLookup.topFavourite`, refresh-vs-dump with
|
Favourites order — `SiriCheapestLookup.topFavourite`, refresh-vs-dump with
|
||||||
offline cached-snapshot fallback); no location required (distance shown only
|
offline cached-snapshot fallback); no location required (distance shown only
|
||||||
when the saved fix exists). Also `DirectionsToFavouriteFuelIntent` (Maps
|
when the saved fix exists). `DirectionsToFavouriteFuelIntent` (Maps
|
||||||
hand-off, station announced first). 4+4 App Shortcuts (generic + per-fuel).
|
hand-off, station announced first) stays in code but carries no voice
|
||||||
|
phrases — every favourite price card has the interactive Directions button
|
||||||
|
(`6d7254e`), and the voice slots went to Directions-to-Cheapest (`8a4c472`).
|
||||||
*DONE — favourites already carried station snapshots, so no `StationEntity`
|
*DONE — favourites already carried station snapshots, so no `StationEntity`
|
||||||
dependency.*
|
dependency.*
|
||||||
- [ ] **Siri: on-screen awareness (View Annotations + StationEntity)** — iOS
|
- [ ] **Siri: on-screen awareness (View Annotations + StationEntity)** — iOS
|
||||||
@@ -60,13 +106,30 @@ Status: TODO / IN PROGRESS / DONE / BLOCKED.
|
|||||||
scene (CPListTemplate) for navigation deep-links.
|
scene (CPListTemplate) for navigation deep-links.
|
||||||
- [ ] **Alert region tuning** — make alert trigger radius + dedup window
|
- [ ] **Alert region tuning** — make alert trigger radius + dedup window
|
||||||
user-configurable (currently fixed 300 m trigger / 1 per station per hour).
|
user-configurable (currently fixed 300 m trigger / 1 per station per hour).
|
||||||
|
- [ ] **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
|
- [ ] **Pull-to-refresh spinner state** — surface refresh in-flight state
|
||||||
(currently `refreshable` fires but no visible progress in the row list).
|
(currently `refreshable` fires but no visible progress in the row list).
|
||||||
- [ ] **Offline first-run** — first launch without network currently shows empty
|
- [x] **Offline first-run** — bundled REAL 8,022-station dump (`FuelBoardDump`,
|
||||||
states; bundle a sample dump so the app demos offline.
|
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.
|
- [ ] **Localization** — UK English only today; audit string catalog for l10n.
|
||||||
- [ ] **Dark mode polish** — verify brand logos (white-circle clips) and RAG dot
|
- [ ] **Dark mode polish** — verify brand logos (white-circle clips) and RAG dot
|
||||||
contrast under dark appearance on-device.
|
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
|
## P3 — Ideas / Research
|
||||||
|
|
||||||
@@ -76,19 +139,55 @@ Status: TODO / IN PROGRESS / DONE / BLOCKED.
|
|||||||
- [ ] **App Intents testing** — AppIntentsTesting (iOS 27) for intent coverage
|
- [ ] **App Intents testing** — AppIntentsTesting (iOS 27) for intent coverage
|
||||||
instead of XCUITest when the Siri features land.
|
instead of XCUITest when the Siri features land.
|
||||||
|
|
||||||
- [ ] **Production data mirror (GitHub)** — relay pushes the synced dump
|
- [ ] **Fuel-price prediction** — "price tends up/down" arrow derived from the
|
||||||
(`stations.json` + `latest.json` version pointer) to a PUBLIC GitHub repo; the
|
price-history archive (P0). Cheap once the archive + Trends graph exist; keep a
|
||||||
app fetches `raw.githubusercontent.com` with the LAN relay as fallback source.
|
separate phase. Backlog ancestry: this was the original "relay could keep a
|
||||||
Public repo required for App Review (reviewer must fetch the data themselves);
|
rolling archive" idea — the archive is now P0 and the Trends graph (P1) is its
|
||||||
HTTPS/ATS fine; plain JSON data is not "remote code" (2.5.2-safe); bundle a small
|
visible half.
|
||||||
fallback dump so the app never demos empty. Needs GitHub PAT (aptonline account).
|
- [ ] **Foresight Live Activity (heading-aware next station)** — investigate a
|
||||||
- [ ] **Fuel-price prediction** — trend from historical dumps (relay could keep a
|
driving-time surface that predicts the likely next station ahead of the user
|
||||||
rolling archive) to show "price tends up/down".
|
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 alerts for a specific station** — notify when a starred station's
|
||||||
price drops below a user threshold.
|
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.
|
- [ ] **Share sheet** — share "Cheapest Unleaded near me" as text/image card.
|
||||||
- [ ] **Watch app** — glanceable cheapest-price complication (WatchKit + shared
|
- [ ] **Watch app v1 — favourites + Check now** — small Apple Watch companion,
|
||||||
app-group store; watch target would need to be added to the project).
|
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.
|
- [ ] **App Clip** — 10 MB budget for "find cheapest fuel nearby" without install.
|
||||||
- [ ] **Widget picker: dual-unit distance labels** — the Edit-Widget Distance row
|
- [ ] **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
|
label is cached by the system at sheet-render time and only re-resolves on
|
||||||
@@ -107,6 +206,20 @@ Status: TODO / IN PROGRESS / DONE / BLOCKED.
|
|||||||
|
|
||||||
## Done (recent)
|
## 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 FIRST everywhere** (alert checks + widget too — the relay is
|
||||||
|
unreachable from any consumer phone and attempting it would fire the Local
|
||||||
|
Network prompt); LAN relay joins ONLY behind the hidden dev flag
|
||||||
|
`fuelboard.relayFallback` (Settings → Debug, off by default); bundled REAL
|
||||||
|
dump (`FuelBoardDump` dataset, refreshed by `scripts/refresh_bundled_dump.sh`)
|
||||||
|
is the app's no-network last resort. `FuelBeacon` fire-and-forget X-Client app
|
||||||
|
ping fires only with the dev flag on. **Local Network permission removed from
|
||||||
|
onboarding entirely** — data page is informational, replay on cellular can no
|
||||||
|
longer hang. 95 tests.
|
||||||
- [x] Switch dataset to govuk API data — official Fuel Finder OAuth API via
|
- [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
|
relay-api :8789 (source: api, ~8,012 stations); app baseURL → :8789; credentials
|
||||||
live in relay `.env` only, never in the IPA
|
live in relay `.env` only, never in the IPA
|
||||||
|
|||||||
@@ -16,6 +16,8 @@
|
|||||||
<string>$(PRODUCT_NAME)</string>
|
<string>$(PRODUCT_NAME)</string>
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>$(MARKETING_VERSION)</string>
|
||||||
<key>CFBundleURLTypes</key>
|
<key>CFBundleURLTypes</key>
|
||||||
<array>
|
<array>
|
||||||
<dict>
|
<dict>
|
||||||
@@ -27,22 +29,25 @@
|
|||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
<key>CFBundleShortVersionString</key>
|
|
||||||
<string>1.0</string>
|
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>1</string>
|
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>NSLocationWhenInUseUsageDescription</key>
|
<key>NSLocalNetworkUsageDescription</key>
|
||||||
<string>FuelBoard uses your location to find the cheapest nearby petrol stations.</string>
|
<string>FuelBoard can optionally connect to the FuelBoard Relay on your local network for faster alerts during development.</string>
|
||||||
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
|
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
|
||||||
<string>FuelBoard uses Always location to alert you when you approach the cheapest station nearby.</string>
|
<string>FuelBoard uses Always location to alert you when you approach the cheapest station nearby.</string>
|
||||||
<key>NSLocalNetworkUsageDescription</key>
|
<key>NSLocationWhenInUseUsageDescription</key>
|
||||||
<string>FuelBoard connects to the FuelBoard Relay on your local network to download the latest fuel prices.</string>
|
<string>FuelBoard uses your location to find the cheapest nearby petrol stations.</string>
|
||||||
<key>NSSupportsLiveActivities</key>
|
<key>NSSupportsLiveActivities</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>BGTaskSchedulerPermittedIdentifiers</key>
|
||||||
|
<array>
|
||||||
|
<string>com.apt.fuelboard.smart-refresh</string>
|
||||||
|
</array>
|
||||||
<key>UIBackgroundModes</key>
|
<key>UIBackgroundModes</key>
|
||||||
<array>
|
<array>
|
||||||
|
<string>fetch</string>
|
||||||
<string>location</string>
|
<string>location</string>
|
||||||
</array>
|
</array>
|
||||||
<key>UILaunchScreen</key>
|
<key>UILaunchScreen</key>
|
||||||
|
|||||||
@@ -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>
|
<key>CFBundlePackageType</key>
|
||||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.0</string>
|
<string>$(MARKETING_VERSION)</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>1</string>
|
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||||
<key>NSLocationWhenInUseUsageDescription</key>
|
|
||||||
<string>FuelBoard uses your location to show the cheapest nearby petrol stations.</string>
|
|
||||||
<key>NSExtension</key>
|
<key>NSExtension</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSExtensionPointIdentifier</key>
|
<key>NSExtensionPointIdentifier</key>
|
||||||
<string>com.apple.widgetkit-extension</string>
|
<string>com.apple.widgetkit-extension</string>
|
||||||
</dict>
|
</dict>
|
||||||
|
<key>NSLocationWhenInUseUsageDescription</key>
|
||||||
|
<string>FuelBoard uses your location to show the cheapest nearby petrol stations.</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
// !$*UTF8*$!
|
// !$*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;
|
archiveVersion = 1;
|
||||||
classes = {
|
classes = {
|
||||||
@@ -10,10 +7,37 @@
|
|||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
|
FBA000000000000000000001 /* DataRefreshMode.swift in FuelBoardShared */ = {isa = PBXBuildFile; fileRef = FBA000000000000000000101 /* DataRefreshMode.swift */; };
|
||||||
|
FBA000000000000000000002 /* FuelHistory.swift in FuelBoardShared */ = {isa = PBXBuildFile; fileRef = FBA000000000000000000102 /* FuelHistory.swift */; };
|
||||||
|
FBA000000000000000000003 /* FuelPriceProvider.swift in FuelBoardShared */ = {isa = PBXBuildFile; fileRef = FBA000000000000000000103 /* FuelPriceProvider.swift */; };
|
||||||
|
FBA000000000000000000004 /* FuelStore.swift in FuelBoardShared */ = {isa = PBXBuildFile; fileRef = FBA000000000000000000104 /* FuelStore.swift */; };
|
||||||
|
FBA000000000000000000005 /* MirrorFuelProvider.swift in FuelBoardShared */ = {isa = PBXBuildFile; fileRef = FBA000000000000000000105 /* MirrorFuelProvider.swift */; };
|
||||||
|
FBA000000000000000000006 /* SiriCheapestLookup.swift in FuelBoardShared */ = {isa = PBXBuildFile; fileRef = FBA000000000000000000106 /* SiriCheapestLookup.swift */; };
|
||||||
|
FBA000000000000000000007 /* WidgetLocationFetcher.swift in FuelBoardShared */ = {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 */; };
|
||||||
|
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, ); }; };
|
BB0000000000000000000001 /* FuelBoardWidgets.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = BB0000000000000000000002 /* FuelBoardWidgets.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy 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 */ = {
|
BB0000000000000000000003 /* PBXContainerItemProxy */ = {
|
||||||
isa = PBXContainerItemProxy;
|
isa = PBXContainerItemProxy;
|
||||||
containerPortal = BB0000000000000000000004 /* Project object */;
|
containerPortal = BB0000000000000000000004 /* Project object */;
|
||||||
@@ -24,6 +48,28 @@
|
|||||||
/* End PBXContainerItemProxy section */
|
/* End PBXContainerItemProxy section */
|
||||||
|
|
||||||
/* Begin PBXCopyFilesBuildPhase 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 */ = {
|
BB0000000000000000000006 /* Embed App Extensions */ = {
|
||||||
isa = PBXCopyFilesBuildPhase;
|
isa = PBXCopyFilesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@@ -38,11 +84,36 @@
|
|||||||
/* End PBXCopyFilesBuildPhase section */
|
/* End PBXCopyFilesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
|
FBA000000000000000000100 /* FuelBoardSharedTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FuelBoardSharedTests.xctest; 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; };
|
||||||
|
AD22FCFF30682512009859D4 /* FuelBoardTips.storekit */ = {isa = PBXFileReference; lastKnownFileType = text; path = FuelBoardTips.storekit; sourceTree = "<group>"; };
|
||||||
|
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; };
|
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; };
|
BB0000000000000000000007 /* FuelBoard.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FuelBoard.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFileSystemSynchronizedRootGroup 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 */ = {
|
BB0000000000000000000008 /* FuelBoard */ = {
|
||||||
isa = PBXFileSystemSynchronizedRootGroup;
|
isa = PBXFileSystemSynchronizedRootGroup;
|
||||||
path = FuelBoard;
|
path = FuelBoard;
|
||||||
@@ -61,6 +132,28 @@
|
|||||||
/* End PBXFileSystemSynchronizedRootGroup section */
|
/* End PBXFileSystemSynchronizedRootGroup section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
|
FBA000000000000000000302 /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
FBA00000000000000000000B /* XCTest.framework in Frameworks */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
ADCE5B4C3033A329008C131C /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
ADCE5B6D3033A32E008C131C /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
BB000000000000000000000B /* Frameworks */ = {
|
BB000000000000000000000B /* Frameworks */ = {
|
||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@@ -78,21 +171,63 @@
|
|||||||
/* End PBXFrameworksBuildPhase section */
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXGroup section */
|
/* Begin PBXGroup section */
|
||||||
|
FBA000000000000000000200 = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
FBA000000000000000000201 /* Sources */,
|
||||||
|
FBA000000000000000000202 /* Tests */,
|
||||||
|
);
|
||||||
|
name = FuelBoardTests;
|
||||||
|
path = FuelBoardTests;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
FBA000000000000000000201 = {
|
||||||
|
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 = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
FBA000000000000000000108 /* FuelBoardTests.swift */,
|
||||||
|
FBA000000000000000000109 /* FuelHistoryTests.swift */,
|
||||||
|
FBA00000000000000000010A /* LiveChainTests.swift */,
|
||||||
|
);
|
||||||
|
name = Tests;
|
||||||
|
path = Tests/FuelBoardSharedTests;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
BB000000000000000000000D = {
|
BB000000000000000000000D = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
FBA000000000000000000200 /* FuelBoardTests */,
|
||||||
|
AD22FCFF30682512009859D4 /* FuelBoardTips.storekit */,
|
||||||
BB0000000000000000000008 /* FuelBoard */,
|
BB0000000000000000000008 /* FuelBoard */,
|
||||||
BB0000000000000000000009 /* FuelBoardWidgets */,
|
BB0000000000000000000009 /* FuelBoardWidgets */,
|
||||||
BB000000000000000000000A /* Shared */,
|
BB000000000000000000000A /* Shared */,
|
||||||
|
ADCE5B663033A32D008C131C /* FuelBoardWatch Wrapper */,
|
||||||
|
ADCE5B503033A329008C131C /* FuelBoardWatch Watch App */,
|
||||||
BB000000000000000000000E /* Products */,
|
BB000000000000000000000E /* Products */,
|
||||||
);
|
);
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
BB000000000000000000000E /* Products */ = {
|
BB000000000000000000000E = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
BB0000000000000000000007 /* FuelBoard.app */,
|
BB0000000000000000000007 /* FuelBoard.app */,
|
||||||
BB0000000000000000000002 /* FuelBoardWidgets.appex */,
|
BB0000000000000000000002 /* FuelBoardWidgets.appex */,
|
||||||
|
ADCE5B4F3033A329008C131C /* FuelBoardWatch Watch App.app */,
|
||||||
|
ADCE5B633033A32D008C131C /* FuelBoardWatch Watch App Extension.appex */,
|
||||||
);
|
);
|
||||||
name = Products;
|
name = Products;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -100,30 +235,67 @@
|
|||||||
/* End PBXGroup section */
|
/* End PBXGroup section */
|
||||||
|
|
||||||
/* Begin PBXNativeTarget section */
|
/* Begin PBXNativeTarget section */
|
||||||
BB000000000000000000000F /* FuelBoard */ = {
|
FBA000000000000000000300 /* FuelBoardSharedTests */ = {
|
||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = BB0000000000000000000010 /* Build configuration list for PBXNativeTarget "FuelBoard" */;
|
buildConfigurationList = FBA000000000000000000400 /* Build configuration list for PBXNativeTarget "FuelBoardSharedTests" */;
|
||||||
buildPhases = (
|
buildPhases = (
|
||||||
BB0000000000000000000011 /* Sources */,
|
FBA000000000000000000301 /* Sources */,
|
||||||
BB000000000000000000000B /* Frameworks */,
|
FBA000000000000000000302 /* Frameworks */,
|
||||||
BB0000000000000000000012 /* Resources */,
|
FBA000000000000000000303 /* Resources */,
|
||||||
BB0000000000000000000006 /* Embed App Extensions */,
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
);
|
||||||
|
name = FuelBoardSharedTests;
|
||||||
|
productName = FuelBoardSharedTests;
|
||||||
|
productReference = FBA000000000000000000100 /* FuelBoardSharedTests.xctest */;
|
||||||
|
productType = "com.apple.product-type.bundle.unit-test";
|
||||||
|
};
|
||||||
|
ADCE5B4E3033A329008C131C /* FuelBoardWatch Watch App Extension */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = ADCE5B593033A32B008C131C /* Build configuration list for PBXNativeTarget "FuelBoardWatch Watch App Extension" */;
|
||||||
|
buildPhases = (
|
||||||
|
ADCE5B4B3033A329008C131C /* Sources */,
|
||||||
|
ADCE5B4C3033A329008C131C /* Frameworks */,
|
||||||
|
ADCE5B4D3033A329008C131C /* Resources */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
dependencies = (
|
dependencies = (
|
||||||
BB0000000000000000000013 /* PBXTargetDependency */,
|
|
||||||
);
|
);
|
||||||
fileSystemSynchronizedGroups = (
|
fileSystemSynchronizedGroups = (
|
||||||
BB0000000000000000000008 /* FuelBoard */,
|
ADCE5B503033A329008C131C /* FuelBoardWatch Watch App */,
|
||||||
BB000000000000000000000A /* Shared */,
|
|
||||||
);
|
);
|
||||||
name = FuelBoard;
|
name = "FuelBoardWatch Watch App Extension";
|
||||||
packageProductDependencies = (
|
packageProductDependencies = (
|
||||||
);
|
);
|
||||||
productName = FuelBoard;
|
productName = "FuelBoardWatch Watch App Extension";
|
||||||
productReference = BB0000000000000000000007 /* FuelBoard.app */;
|
productReference = ADCE5B633033A32D008C131C /* FuelBoardWatch Watch App Extension.appex */;
|
||||||
productType = "com.apple.product-type.application";
|
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 */ = {
|
BB0000000000000000000005 /* FuelBoardWidgets */ = {
|
||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
@@ -148,6 +320,33 @@
|
|||||||
productReference = BB0000000000000000000002 /* FuelBoardWidgets.appex */;
|
productReference = BB0000000000000000000002 /* FuelBoardWidgets.appex */;
|
||||||
productType = "com.apple.product-type.app-extension";
|
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";
|
||||||
|
};
|
||||||
/* End PBXNativeTarget section */
|
/* End PBXNativeTarget section */
|
||||||
|
|
||||||
/* Begin PBXProject section */
|
/* Begin PBXProject section */
|
||||||
@@ -155,15 +354,23 @@
|
|||||||
isa = PBXProject;
|
isa = PBXProject;
|
||||||
attributes = {
|
attributes = {
|
||||||
BuildIndependentTargetsInParallel = 1;
|
BuildIndependentTargetsInParallel = 1;
|
||||||
LastSwiftUpdateCheck = 1600;
|
LastSwiftUpdateCheck = 2630;
|
||||||
LastUpgradeCheck = 1600;
|
LastUpgradeCheck = 1600;
|
||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
BB000000000000000000000F = {
|
ADCE5B4E3033A329008C131C = {
|
||||||
CreatedOnToolsVersion = 16.0;
|
CreatedOnToolsVersion = 26.3;
|
||||||
};
|
};
|
||||||
BB0000000000000000000005 = {
|
BB0000000000000000000005 = {
|
||||||
CreatedOnToolsVersion = 16.0;
|
CreatedOnToolsVersion = 16.0;
|
||||||
};
|
};
|
||||||
|
BB000000000000000000000F = {
|
||||||
|
CreatedOnToolsVersion = 16.0;
|
||||||
|
SystemCapabilities = {
|
||||||
|
com.apple.InAppPurchase = {
|
||||||
|
enabled = 1;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
buildConfigurationList = BB0000000000000000000017 /* Build configuration list for PBXProject "FuelBoard" */;
|
buildConfigurationList = BB0000000000000000000017 /* Build configuration list for PBXProject "FuelBoard" */;
|
||||||
@@ -182,11 +389,35 @@
|
|||||||
targets = (
|
targets = (
|
||||||
BB000000000000000000000F /* FuelBoard */,
|
BB000000000000000000000F /* FuelBoard */,
|
||||||
BB0000000000000000000005 /* FuelBoardWidgets */,
|
BB0000000000000000000005 /* FuelBoardWidgets */,
|
||||||
|
ADCE5B673033A32D008C131C /* FuelBoardWatch Watch App */,
|
||||||
|
ADCE5B4E3033A329008C131C /* FuelBoardWatch Watch App Extension */,
|
||||||
|
FBA000000000000000000300 /* FuelBoardSharedTests */,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
/* End PBXProject section */
|
/* End PBXProject section */
|
||||||
|
|
||||||
/* Begin PBXResourcesBuildPhase section */
|
/* Begin PBXResourcesBuildPhase section */
|
||||||
|
FBA000000000000000000303 /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
ADCE5B4D3033A329008C131C /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
ADCE5B6E3033A32E008C131C /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
BB0000000000000000000012 /* Resources */ = {
|
BB0000000000000000000012 /* Resources */ = {
|
||||||
isa = PBXResourcesBuildPhase;
|
isa = PBXResourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@@ -204,6 +435,30 @@
|
|||||||
/* End PBXResourcesBuildPhase section */
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXSourcesBuildPhase section */
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
|
FBA000000000000000000301 /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
FBA000000000000000000001 /* DataRefreshMode.swift in FuelBoardShared */,
|
||||||
|
FBA000000000000000000002 /* FuelHistory.swift in FuelBoardShared */,
|
||||||
|
FBA000000000000000000003 /* FuelPriceProvider.swift in FuelBoardShared */,
|
||||||
|
FBA000000000000000000004 /* FuelStore.swift in FuelBoardShared */,
|
||||||
|
FBA000000000000000000005 /* MirrorFuelProvider.swift in FuelBoardShared */,
|
||||||
|
FBA000000000000000000006 /* SiriCheapestLookup.swift in FuelBoardShared */,
|
||||||
|
FBA000000000000000000007 /* WidgetLocationFetcher.swift in FuelBoardShared */,
|
||||||
|
FBA000000000000000000008 /* FuelBoardTests.swift in Sources */,
|
||||||
|
FBA000000000000000000009 /* FuelHistoryTests.swift in Sources */,
|
||||||
|
FBA00000000000000000000A /* LiveChainTests.swift in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
ADCE5B4B3033A329008C131C /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
BB0000000000000000000011 /* Sources */ = {
|
BB0000000000000000000011 /* Sources */ = {
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@@ -221,6 +476,16 @@
|
|||||||
/* End PBXSourcesBuildPhase section */
|
/* End PBXSourcesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXTargetDependency 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 */ = {
|
BB0000000000000000000013 /* PBXTargetDependency */ = {
|
||||||
isa = PBXTargetDependency;
|
isa = PBXTargetDependency;
|
||||||
target = BB0000000000000000000005 /* FuelBoardWidgets */;
|
target = BB0000000000000000000005 /* FuelBoardWidgets */;
|
||||||
@@ -229,6 +494,266 @@
|
|||||||
/* End PBXTargetDependency section */
|
/* End PBXTargetDependency section */
|
||||||
|
|
||||||
/* Begin XCBuildConfiguration 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 = 22;
|
||||||
|
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.0.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 = 22;
|
||||||
|
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.0.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 = 22;
|
||||||
|
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.0.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 = 22;
|
||||||
|
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.0.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 */ = {
|
BB0000000000000000000018 /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
@@ -290,17 +815,20 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CODE_SIGN_ENTITLEMENTS = Config/App.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Config/App.entitlements;
|
||||||
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 22;
|
||||||
|
DEVELOPMENT_TEAM = F3BE6NE7U3;
|
||||||
GENERATE_INFOPLIST_FILE = NO;
|
GENERATE_INFOPLIST_FILE = NO;
|
||||||
INFOPLIST_FILE = Config/App-Info.plist;
|
INFOPLIST_FILE = "Config/App-Info.plist";
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.apt.fuelboard;
|
PRODUCT_BUNDLE_IDENTIFIER = com.apt.fuelboard;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = 1;
|
TARGETED_DEVICE_FAMILY = 1;
|
||||||
@@ -312,17 +840,20 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CODE_SIGN_ENTITLEMENTS = Config/App.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Config/App.entitlements;
|
||||||
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 22;
|
||||||
|
DEVELOPMENT_TEAM = F3BE6NE7U3;
|
||||||
GENERATE_INFOPLIST_FILE = NO;
|
GENERATE_INFOPLIST_FILE = NO;
|
||||||
INFOPLIST_FILE = Config/App-Info.plist;
|
INFOPLIST_FILE = "Config/App-Info.plist";
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.apt.fuelboard;
|
PRODUCT_BUNDLE_IDENTIFIER = com.apt.fuelboard;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = 1;
|
TARGETED_DEVICE_FAMILY = 1;
|
||||||
@@ -335,15 +866,16 @@
|
|||||||
APPLICATION_EXTENSION_API_ONLY = YES;
|
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = Config/Widget.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Config/Widget.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 22;
|
||||||
|
DEVELOPMENT_TEAM = F3BE6NE7U3;
|
||||||
GENERATE_INFOPLIST_FILE = NO;
|
GENERATE_INFOPLIST_FILE = NO;
|
||||||
INFOPLIST_FILE = Config/Widget-Info.plist;
|
INFOPLIST_FILE = "Config/Widget-Info.plist";
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.apt.fuelboard.widgets;
|
PRODUCT_BUNDLE_IDENTIFIER = com.apt.fuelboard.widgets;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
@@ -359,15 +891,16 @@
|
|||||||
APPLICATION_EXTENSION_API_ONLY = YES;
|
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = Config/Widget.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Config/Widget.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 22;
|
||||||
|
DEVELOPMENT_TEAM = F3BE6NE7U3;
|
||||||
GENERATE_INFOPLIST_FILE = NO;
|
GENERATE_INFOPLIST_FILE = NO;
|
||||||
INFOPLIST_FILE = Config/Widget-Info.plist;
|
INFOPLIST_FILE = "Config/Widget-Info.plist";
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.apt.fuelboard.widgets;
|
PRODUCT_BUNDLE_IDENTIFIER = com.apt.fuelboard.widgets;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
@@ -377,14 +910,78 @@
|
|||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
};
|
};
|
||||||
|
FBA000000000000000000401 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 22;
|
||||||
|
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 = 22;
|
||||||
|
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 */
|
/* End XCBuildConfiguration section */
|
||||||
|
|
||||||
/* Begin XCConfigurationList section */
|
/* Begin XCConfigurationList section */
|
||||||
BB0000000000000000000017 /* Build configuration list for PBXProject "FuelBoard" */ = {
|
FBA000000000000000000400 /* Build configuration list for PBXNativeTarget "FuelBoardSharedTests" */ = {
|
||||||
isa = XCConfigurationList;
|
isa = XCConfigurationList;
|
||||||
buildConfigurations = (
|
buildConfigurations = (
|
||||||
BB0000000000000000000018 /* Debug */,
|
FBA000000000000000000401 /* Debug */,
|
||||||
BB0000000000000000000019 /* Release */,
|
FBA000000000000000000402 /* Release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
ADCE5B593033A32B008C131C /* Build configuration list for PBXNativeTarget "FuelBoardWatch Watch App Extension" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
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;
|
defaultConfigurationIsVisible = 0;
|
||||||
defaultConfigurationName = Release;
|
defaultConfigurationName = Release;
|
||||||
@@ -407,6 +1004,15 @@
|
|||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
defaultConfigurationName = Release;
|
defaultConfigurationName = Release;
|
||||||
};
|
};
|
||||||
|
BB0000000000000000000017 /* Build configuration list for PBXProject "FuelBoard" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
BB0000000000000000000018 /* Debug */,
|
||||||
|
BB0000000000000000000019 /* Release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
/* End XCConfigurationList section */
|
/* End XCConfigurationList section */
|
||||||
};
|
};
|
||||||
rootObject = BB0000000000000000000004 /* Project object */;
|
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>
|
||||||
@@ -12,6 +12,8 @@ struct AlertsView: View {
|
|||||||
@Binding var enabled: Bool
|
@Binding var enabled: Bool
|
||||||
@Binding var radius: Double // stored in km (monitor + storage)
|
@Binding var radius: Double // stored in km (monitor + storage)
|
||||||
@Binding var fuel: FuelType // the fuel alerts monitor for
|
@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).
|
/// Whether alerts mirror the Stations-tab search distance (capped at 8 mi).
|
||||||
@Binding var followsSearch: Bool
|
@Binding var followsSearch: Bool
|
||||||
/// Whether the "cheapest nearby" Live Activity is shown on the Lock Screen
|
/// Whether the "cheapest nearby" Live Activity is shown on the Lock Screen
|
||||||
@@ -30,6 +32,7 @@ struct AlertsView: View {
|
|||||||
let distanceUnit: DistanceUnit
|
let distanceUnit: DistanceUnit
|
||||||
let monitoredCount: Int
|
let monitoredCount: Int
|
||||||
let lastAlert: String?
|
let lastAlert: String?
|
||||||
|
let onCheckFavouriteDropNow: () -> Void
|
||||||
|
|
||||||
/// The Stations-tab search distance shown in the user's unit, e.g.
|
/// The Stations-tab search distance shown in the user's unit, e.g.
|
||||||
/// "5 miles" or "8 km" — for the "Follow search" rows.
|
/// "5 miles" or "8 km" — for the "Follow search" rows.
|
||||||
@@ -46,6 +49,34 @@ struct AlertsView: View {
|
|||||||
return "\(shown) \(distanceUnit.label(for: Double(shown)))"
|
return "\(shown) \(distanceUnit.label(for: Double(shown)))"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Situational guidance for the currently-selected alert radius, shown
|
||||||
|
/// under the picker so users know what each choice is best for. Labels
|
||||||
|
/// mirror the picker row text (unit-aware) and explain the trade-off:
|
||||||
|
/// smaller radii ping more often but later; larger radii ping rarely in
|
||||||
|
/// town but earlier from afar.
|
||||||
|
private var alertRadiusGuidance: String {
|
||||||
|
switch alertChoice.wrappedValue {
|
||||||
|
case .followSearch:
|
||||||
|
return "Set-and-forget: alerts behave exactly like the Stations-tab search distance (capped at 8 miles for reliable geofencing)."
|
||||||
|
case .fixed(let value):
|
||||||
|
let optionLabel = "\(value) \(distanceUnit.label(for: Double(value)))"
|
||||||
|
let body: String
|
||||||
|
switch value {
|
||||||
|
case 1:
|
||||||
|
body = "Town driving. Fires most often — you're rarely inside the circle for long, so pings are meaningful. Best for stop-start local trips."
|
||||||
|
case 2:
|
||||||
|
body = "City or suburb commute. Slightly earlier heads-up than the smallest option; still fires regularly around town."
|
||||||
|
case 3:
|
||||||
|
body = "Mixed driving. Middle ground: heads-up a few minutes out, still works in most towns."
|
||||||
|
case 5:
|
||||||
|
body = "A-roads and motorway exits. Wide early warning for longer trips — but in town it rarely fires, because you're usually already inside the circle."
|
||||||
|
default:
|
||||||
|
body = "Long-distance journeys. Maximum reach: a ping when a cheap station is coming up from far out. In town this essentially never fires."
|
||||||
|
}
|
||||||
|
return "\(optionLabel) — \(body)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private enum AlertRadiusChoice: Hashable {
|
private enum AlertRadiusChoice: Hashable {
|
||||||
case followSearch
|
case followSearch
|
||||||
case fixed(Int)
|
case fixed(Int)
|
||||||
@@ -103,7 +134,22 @@ struct AlertsView: View {
|
|||||||
NavigationStack {
|
NavigationStack {
|
||||||
List {
|
List {
|
||||||
Section {
|
Section {
|
||||||
Toggle("Cheapest-station alerts", isOn: $enabled)
|
Toggle("Cheapest favourite", isOn: $favouriteDropEnabled)
|
||||||
|
Picker("Fuel", selection: $favouriteDropFuel) {
|
||||||
|
ForEach(FuelType.allCases) { fuel in
|
||||||
|
Text(fuel.displayName).tag(fuel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Button("Check now", action: onCheckFavouriteDropNow)
|
||||||
|
.disabled(!favouriteDropEnabled)
|
||||||
|
} header: {
|
||||||
|
Text("Price-drop alerts")
|
||||||
|
} footer: {
|
||||||
|
Text("Watches your cheapest saved favourite for the fuel you pick here. FuelBoard alerts when a different favourite becomes the cheapest, or when your current cheapest favourite drops by at least 1.0p. This is separate from the nearby-geofence alert, so you can monitor favourites without extra location noise.")
|
||||||
|
}
|
||||||
|
|
||||||
|
Section {
|
||||||
|
Toggle("Cheapest station", isOn: $enabled)
|
||||||
Picker("Fuel", selection: $fuel) {
|
Picker("Fuel", selection: $fuel) {
|
||||||
ForEach(FuelType.allCases) { fuel in
|
ForEach(FuelType.allCases) { fuel in
|
||||||
Text(fuel.displayName).tag(fuel)
|
Text(fuel.displayName).tag(fuel)
|
||||||
@@ -115,15 +161,18 @@ struct AlertsView: View {
|
|||||||
Text("\(value) \(distanceUnit.label(for: Double(value)))").tag(AlertRadiusChoice.fixed(value))
|
Text("\(value) \(distanceUnit.label(for: Double(value)))").tag(AlertRadiusChoice.fixed(value))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Text(alertRadiusGuidance)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
if enabled && monitoredCount == 0 {
|
if enabled && monitoredCount == 0 {
|
||||||
Text("No stations monitored yet — open the Stations tab to load prices first.")
|
Text("No stations monitored yet — open the Stations tab to load prices first.")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
} header: {
|
} header: {
|
||||||
Text("Cheapest-station alerts")
|
Text("Proximity alerts")
|
||||||
} footer: {
|
} 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("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. Smaller radii ping more often but later; larger radii ping rarely in town but earlier from afar.")
|
||||||
}
|
}
|
||||||
|
|
||||||
if enabled, let lastAlert {
|
if enabled, let lastAlert {
|
||||||
@@ -134,7 +183,7 @@ struct AlertsView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
Toggle("Live Activity", isOn: $liveActivityEnabled)
|
Toggle("Cheapest station", isOn: $liveActivityEnabled)
|
||||||
Picker("Fuel", selection: $liveActivityFuel) {
|
Picker("Fuel", selection: $liveActivityFuel) {
|
||||||
ForEach(FuelType.allCases) { fuel in
|
ForEach(FuelType.allCases) { fuel in
|
||||||
Text(fuel.displayName).tag(fuel)
|
Text(fuel.displayName).tag(fuel)
|
||||||
|
|||||||
|
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",
|
"filename": "AppIcon.png",
|
||||||
"idiom" : "universal",
|
"idiom": "universal",
|
||||||
"platform" : "ios",
|
"platform": "ios",
|
||||||
"size" : "1024x1024"
|
"size": "1024x1024"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"appearances" : [
|
"appearances": [
|
||||||
{
|
{
|
||||||
"appearance" : "luminosity",
|
"appearance": "luminosity",
|
||||||
"value" : "dark"
|
"value": "dark"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"filename" : "AppIcon-dark.png",
|
"filename": "AppIcon-dark.png",
|
||||||
"idiom" : "universal",
|
"idiom": "universal",
|
||||||
"platform" : "ios",
|
"platform": "ios",
|
||||||
"size" : "1024x1024"
|
"size": "1024x1024"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"appearances" : [
|
"appearances": [
|
||||||
{
|
{
|
||||||
"appearance" : "luminosity",
|
"appearance": "luminosity",
|
||||||
"value" : "tinted"
|
"value": "tinted"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"filename" : "AppIcon-tinted.png",
|
"filename": "AppIcon-tinted.png",
|
||||||
"idiom" : "universal",
|
"idiom": "universal",
|
||||||
"platform" : "ios",
|
"platform": "ios",
|
||||||
"size" : "1024x1024"
|
"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" : {
|
"info": {
|
||||||
"author" : "xcode",
|
"author": "xcode",
|
||||||
"version" : 1
|
"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",
|
"filename" : "AppIconArtwork.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "1x"
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info" : {
|
"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",
|
||||||
"idiom": "universal",
|
"scale" : "1x"
|
||||||
"scale": "2x"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename": "brand_applegreen_3x.png",
|
"filename" : "brand_applegreen_2x.png",
|
||||||
"idiom": "universal",
|
"idiom" : "universal",
|
||||||
"scale": "3x"
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "brand_applegreen_3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info": {
|
"info" : {
|
||||||
"author": "xcode",
|
"author" : "xcode",
|
||||||
"version": 1
|
"version" : 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,22 @@
|
|||||||
{
|
{
|
||||||
"images": [
|
"images" : [
|
||||||
{
|
{
|
||||||
"filename": "brand_asda_2x.png",
|
"idiom" : "universal",
|
||||||
"idiom": "universal",
|
"scale" : "1x"
|
||||||
"scale": "2x"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename": "brand_asda_3x.png",
|
"filename" : "brand_asda_2x.png",
|
||||||
"idiom": "universal",
|
"idiom" : "universal",
|
||||||
"scale": "3x"
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "brand_asda_3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info": {
|
"info" : {
|
||||||
"author": "xcode",
|
"author" : "xcode",
|
||||||
"version": 1
|
"version" : 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,22 @@
|
|||||||
{
|
{
|
||||||
"images": [
|
"images" : [
|
||||||
{
|
{
|
||||||
"filename": "brand_bp_2x.png",
|
"idiom" : "universal",
|
||||||
"idiom": "universal",
|
"scale" : "1x"
|
||||||
"scale": "2x"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename": "brand_bp_3x.png",
|
"filename" : "brand_bp_2x.png",
|
||||||
"idiom": "universal",
|
"idiom" : "universal",
|
||||||
"scale": "3x"
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "brand_bp_3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info": {
|
"info" : {
|
||||||
"author": "xcode",
|
"author" : "xcode",
|
||||||
"version": 1
|
"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",
|
||||||
"idiom": "universal",
|
"scale" : "1x"
|
||||||
"scale": "2x"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename": "brand_esso_3x.png",
|
"filename" : "brand_esso_2x.png",
|
||||||
"idiom": "universal",
|
"idiom" : "universal",
|
||||||
"scale": "3x"
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "brand_esso_3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info": {
|
"info" : {
|
||||||
"author": "xcode",
|
"author" : "xcode",
|
||||||
"version": 1
|
"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",
|
||||||
"idiom": "universal",
|
"scale" : "1x"
|
||||||
"scale": "2x"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename": "brand_gulf_3x.png",
|
"filename" : "brand_gulf_2x.png",
|
||||||
"idiom": "universal",
|
"idiom" : "universal",
|
||||||
"scale": "3x"
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "brand_gulf_3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info": {
|
"info" : {
|
||||||
"author": "xcode",
|
"author" : "xcode",
|
||||||
"version": 1
|
"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",
|
||||||
"idiom": "universal",
|
"scale" : "1x"
|
||||||
"scale": "2x"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename": "brand_jet_3x.png",
|
"filename" : "brand_jet_2x.png",
|
||||||
"idiom": "universal",
|
"idiom" : "universal",
|
||||||
"scale": "3x"
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "brand_jet_3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info": {
|
"info" : {
|
||||||
"author": "xcode",
|
"author" : "xcode",
|
||||||
"version": 1
|
"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",
|
||||||
"idiom": "universal",
|
"scale" : "1x"
|
||||||
"scale": "2x"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename": "brand_morrisons_3x.png",
|
"filename" : "brand_morrisons_2x.png",
|
||||||
"idiom": "universal",
|
"idiom" : "universal",
|
||||||
"scale": "3x"
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "brand_morrisons_3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info": {
|
"info" : {
|
||||||
"author": "xcode",
|
"author" : "xcode",
|
||||||
"version": 1
|
"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",
|
||||||
"idiom": "universal",
|
"scale" : "1x"
|
||||||
"scale": "2x"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename": "brand_sainsburys_3x.png",
|
"filename" : "brand_sainsburys_2x.png",
|
||||||
"idiom": "universal",
|
"idiom" : "universal",
|
||||||
"scale": "3x"
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "brand_sainsburys_3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info": {
|
"info" : {
|
||||||
"author": "xcode",
|
"author" : "xcode",
|
||||||
"version": 1
|
"version" : 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,22 @@
|
|||||||
{
|
{
|
||||||
"images": [
|
"images" : [
|
||||||
{
|
{
|
||||||
"filename": "brand_shell_2x.png",
|
"idiom" : "universal",
|
||||||
"idiom": "universal",
|
"scale" : "1x"
|
||||||
"scale": "2x"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename": "brand_shell_3x.png",
|
"filename" : "brand_shell_2x.png",
|
||||||
"idiom": "universal",
|
"idiom" : "universal",
|
||||||
"scale": "3x"
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "brand_shell_3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info": {
|
"info" : {
|
||||||
"author": "xcode",
|
"author" : "xcode",
|
||||||
"version": 1
|
"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",
|
||||||
"idiom": "universal",
|
"scale" : "1x"
|
||||||
"scale": "2x"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename": "brand_tesco_3x.png",
|
"filename" : "brand_tesco_2x.png",
|
||||||
"idiom": "universal",
|
"idiom" : "universal",
|
||||||
"scale": "3x"
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "brand_tesco_3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info": {
|
"info" : {
|
||||||
"author": "xcode",
|
"author" : "xcode",
|
||||||
"version": 1
|
"version" : 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,22 @@
|
|||||||
{
|
{
|
||||||
"images": [
|
"images" : [
|
||||||
{
|
{
|
||||||
"filename": "brand_texaco_2x.png",
|
"idiom" : "universal",
|
||||||
"idiom": "universal",
|
"scale" : "1x"
|
||||||
"scale": "2x"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename": "brand_texaco_3x.png",
|
"filename" : "brand_texaco_2x.png",
|
||||||
"idiom": "universal",
|
"idiom" : "universal",
|
||||||
"scale": "3x"
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "brand_texaco_3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info": {
|
"info" : {
|
||||||
"author": "xcode",
|
"author" : "xcode",
|
||||||
"version": 1
|
"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,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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,18 +2,32 @@ import SwiftUI
|
|||||||
import CoreLocation
|
import CoreLocation
|
||||||
import WidgetKit
|
import WidgetKit
|
||||||
|
|
||||||
|
/// The single floating notification banner. Network/offline state and tip
|
||||||
|
/// outcomes all funnel through ONE style and render via the same chrome in
|
||||||
|
/// ContentView — never pushing layout, always overlaid.
|
||||||
|
enum AppBanner: Hashable {
|
||||||
|
case offlineDump(date: String)
|
||||||
|
case connectionProblem
|
||||||
|
case tip(TipStore.TipOutcome)
|
||||||
|
}
|
||||||
|
|
||||||
struct ContentView: View {
|
struct ContentView: View {
|
||||||
@Environment(\.scenePhase) private var scenePhase
|
@Environment(\.scenePhase) private var scenePhase
|
||||||
|
|
||||||
@State private var stations: [FuelStation] = FuelStore.loadStations()
|
@State private var stations: [FuelStation] = FuelStore.loadStations()
|
||||||
@State private var selectedFuel: FuelType = FuelStore.loadSelectedFuel()
|
@State private var selectedFuel: FuelType = FuelStore.loadSelectedFuel()
|
||||||
|
@State private var dataRefreshMode: DataRefreshMode = FuelStore.loadDataRefreshMode()
|
||||||
@State private var sortMode: SortMode = FuelStore.loadSortMode()
|
@State private var sortMode: SortMode = FuelStore.loadSortMode()
|
||||||
@State private var stationLimit: Int = FuelStore.loadStationLimit()
|
@State private var stationLimit: Int = FuelStore.loadStationLimit()
|
||||||
@State private var distanceUnit: DistanceUnit = FuelStore.loadDistanceUnit()
|
@State private var distanceUnit: DistanceUnit = FuelStore.loadDistanceUnit()
|
||||||
|
@State private var priceDisplayStyle: PriceDisplayStyle = FuelStore.loadPriceDisplayStyle()
|
||||||
|
@State private var stationTapAction: StationTapAction = FuelStore.loadStationTapAction()
|
||||||
@State private var favourites: [FavouriteEntry] = FuelStore.loadFavourites()
|
@State private var favourites: [FavouriteEntry] = FuelStore.loadFavourites()
|
||||||
@State private var alertsEnabled: Bool = FuelStore.loadAlertsEnabled()
|
@State private var alertsEnabled: Bool = FuelStore.loadAlertsEnabled()
|
||||||
@State private var alertsRadius: Double = FuelStore.loadAlertsRadius()
|
@State private var alertsRadius: Double = FuelStore.loadAlertsRadius()
|
||||||
@State private var alertsFuel: FuelType = FuelStore.loadAlertsFuel()
|
@State private var alertsFuel: FuelType = FuelStore.loadAlertsFuel()
|
||||||
|
@State private var favouriteDropAlertsEnabled: Bool = FuelStore.loadFavouriteDropAlertsEnabled()
|
||||||
|
@State private var favouriteDropAlertsFuel: FuelType = FuelStore.loadFavouriteDropAlertsFuel()
|
||||||
@State private var alertsFollowsSearch: Bool = FuelStore.loadAlertsFollowsSearch()
|
@State private var alertsFollowsSearch: Bool = FuelStore.loadAlertsFollowsSearch()
|
||||||
@State private var liveActivityEnabled: Bool = FuelStore.loadLiveActivityEnabled()
|
@State private var liveActivityEnabled: Bool = FuelStore.loadLiveActivityEnabled()
|
||||||
@State private var liveActivityFuel: FuelType = FuelStore.loadLiveActivityFuel()
|
@State private var liveActivityFuel: FuelType = FuelStore.loadLiveActivityFuel()
|
||||||
@@ -29,11 +43,75 @@ struct ContentView: View {
|
|||||||
private var effectiveAlertsRadiusKM: Double {
|
private var effectiveAlertsRadiusKM: Double {
|
||||||
FuelStore.effectiveAlertsRadiusKM(followsSearch: alertsFollowsSearch, manualKM: alertsRadius)
|
FuelStore.effectiveAlertsRadiusKM(followsSearch: alertsFollowsSearch, manualKM: alertsRadius)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func installLocationUpdateHook() {
|
||||||
|
locationManager.onLocationUpdate = { [weak monitor] in
|
||||||
|
monitor?.update(stations: stations, favourites: refreshedFavourites,
|
||||||
|
fuel: alertsFuel, radiusKM: effectiveAlertsRadiusKM)
|
||||||
|
// The Live Activity follows the same wake-ups — this hook
|
||||||
|
// fires on every fix incl. background significant-change
|
||||||
|
// wake-ups, so the Lock Screen pill stays live while driving.
|
||||||
|
updateLiveActivity()
|
||||||
|
refreshRoadDistancesIfNeeded()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Kicks off a (throttled) Apple-Maps road-distance recompute for the
|
||||||
|
/// stations around the current fix. The app owns routing — the widget and
|
||||||
|
/// Live Activity only read the cached result. Falls back to the last saved
|
||||||
|
/// location so it can run before the first fresh GPS fix arrives.
|
||||||
|
private func refreshRoadDistancesIfNeeded() {
|
||||||
|
let origin = location ?? FuelStore.loadLocation()
|
||||||
|
guard let origin else { return }
|
||||||
|
Task {
|
||||||
|
await RoadDistanceService.refreshIfNeeded(
|
||||||
|
stations: stations,
|
||||||
|
lat: origin.lat,
|
||||||
|
lng: origin.lng
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func ensureBackgroundMotionTrackingIfNeeded() {
|
||||||
|
if alertsEnabled || liveActivityEnabled {
|
||||||
|
locationManager.startBackgroundTracking()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@State private var isLoading = false
|
@State private var isLoading = false
|
||||||
@State private var statusMessage = ""
|
@State private var statusMessage = ""
|
||||||
|
/// What data is on screen, driving which (if any) status banner shows
|
||||||
|
/// above the tabs:
|
||||||
|
/// - `.live`: fetched or cached data — nothing to say.
|
||||||
|
/// - `.offlineDump(date)`: serving the BUNDLED no-network snapshot —
|
||||||
|
/// the banner labels it honestly with the snapshot's own date.
|
||||||
|
/// - `.connectionProblem`: fetch failed but a saved cache is showing —
|
||||||
|
/// the banner says to check connectivity (tap = retry).
|
||||||
|
enum DataSourceStatus: Equatable {
|
||||||
|
case live
|
||||||
|
case offlineDump(date: String)
|
||||||
|
case connectionProblem
|
||||||
|
}
|
||||||
|
@State private var dataStatus: DataSourceStatus = .live
|
||||||
@State private var showOnboarding = false
|
@State private var showOnboarding = false
|
||||||
|
@State private var showWidgetMock = false
|
||||||
|
@State private var selectedTab = 0
|
||||||
@State private var locationManager = LocationManager()
|
@State private var locationManager = LocationManager()
|
||||||
@StateObject private var monitor = ProximityMonitor()
|
@StateObject private var monitor = ProximityMonitor()
|
||||||
|
/// Owns tip purchases; the single instance is shared down to Settings.
|
||||||
|
/// Its transient outcome rides the SAME status-banner chrome as the
|
||||||
|
/// network/offline strip (ContentView renders it above the tabs).
|
||||||
|
@StateObject private var tipStore = TipStore()
|
||||||
|
|
||||||
|
// --- Explicit banner animation state (robust fade-out) ---
|
||||||
|
// SwiftUI's removal transition for the banner proved unreliable here
|
||||||
|
// (the tip faded to nothing only in the cross-nil case, and even then
|
||||||
|
// inconsistently). Drive opacity/offset explicitly instead so a
|
||||||
|
// dismissal ALWAYS fades + slides up on ANY banner change.
|
||||||
|
@State private var currentBanner: AppBanner?
|
||||||
|
@State private var bannerOpacity: Double = 0
|
||||||
|
@State private var bannerOffset: CGFloat = 0
|
||||||
|
@State private var bannerClearTask: Task<Void, Never>?
|
||||||
|
|
||||||
/// The pool the list draws from. In Cheapest mode the chosen miles radius
|
/// The pool the list draws from. In Cheapest mode the chosen miles radius
|
||||||
/// bounds it ("best price within X miles"); in Closest mode the radius is
|
/// bounds it ("best price within X miles"); in Closest mode the radius is
|
||||||
@@ -136,87 +214,169 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
TabView {
|
configuredContent
|
||||||
StationsView(
|
}
|
||||||
stations: displayedStations,
|
|
||||||
totalCount: sortedStations.count,
|
|
||||||
isLoading: isLoading,
|
|
||||||
selectedFuel: $selectedFuel,
|
|
||||||
sortMode: $sortMode,
|
|
||||||
stationLimit: $stationLimit,
|
|
||||||
distanceUnit: distanceUnit,
|
|
||||||
baselinePrice: baselinePrice,
|
|
||||||
topStationID: topStationID,
|
|
||||||
location: location,
|
|
||||||
favouriteIDs: favouriteIDs,
|
|
||||||
onToggleFavourite: toggleFavourite,
|
|
||||||
onRefresh: { await refresh(force: true) }
|
|
||||||
)
|
|
||||||
.tabItem { Label("Stations", systemImage: "fuelpump.fill") }
|
|
||||||
|
|
||||||
FavouritesView(
|
private var configuredContent: AnyView {
|
||||||
favourites: refreshedFavourites,
|
interactionObservedContent
|
||||||
selectedFuel: selectedFuel,
|
}
|
||||||
location: location,
|
|
||||||
distanceUnit: distanceUnit,
|
|
||||||
onToggleFavourite: toggleFavourite,
|
|
||||||
onReorder: reorderFavourites
|
|
||||||
)
|
|
||||||
.tabItem { Label("Favourites", systemImage: "star.fill") }
|
|
||||||
|
|
||||||
AlertsView(
|
private var presentedContent: AnyView {
|
||||||
enabled: $alertsEnabled,
|
AnyView(
|
||||||
radius: $alertsRadius,
|
mainContent
|
||||||
fuel: $alertsFuel,
|
.fullScreenCover(isPresented: $showOnboarding) {
|
||||||
followsSearch: $alertsFollowsSearch,
|
OnboardingView {
|
||||||
liveActivityEnabled: $liveActivityEnabled,
|
showOnboarding = false
|
||||||
liveActivityFuel: $liveActivityFuel,
|
}
|
||||||
liveActivityRadiusMiles: $liveActivityRadiusMiles,
|
}
|
||||||
liveActivityFollowsSearch: $liveActivityFollowsSearch,
|
.fullScreenCover(isPresented: $showWidgetMock) {
|
||||||
stationLimit: stationLimit,
|
WidgetMockScreen()
|
||||||
distanceUnit: distanceUnit,
|
}
|
||||||
monitoredCount: monitor.monitoredStationIDs.count,
|
.sheet(item: $monitor.pendingStationMap) { request in
|
||||||
lastAlert: monitor.lastAlert
|
StationMapView(request: request)
|
||||||
)
|
}
|
||||||
.tabItem { Label("Alerts", systemImage: "bell.fill") }
|
.sheet(item: $monitor.pendingDetailStation) { station in
|
||||||
|
StationDetailView(
|
||||||
|
station: station,
|
||||||
|
location: location,
|
||||||
|
distanceUnit: distanceUnit
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.onReceive(NotificationCenter.default.publisher(for: .fuelBoardShowStationDetail)) { note in
|
||||||
|
let info = note.userInfo ?? [:]
|
||||||
|
if let id = info["stationID"] as? String,
|
||||||
|
let station = stations.first(where: { $0.id == id }) {
|
||||||
|
monitor.pendingDetailStation = station
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Station left the cache — fall back to the map sheet so
|
||||||
|
// the tap still shows something.
|
||||||
|
if let lat = info["stationLat"] as? Double,
|
||||||
|
let lng = info["stationLng"] as? Double {
|
||||||
|
let name = info["stationName"] as? String ?? "Station"
|
||||||
|
monitor.pendingStationMap = StationMapRequest(
|
||||||
|
name: name, latitude: lat, longitude: lng
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
settingsTab
|
private var lifecycleObservedContent: AnyView {
|
||||||
}
|
AnyView(
|
||||||
.fullScreenCover(isPresented: $showOnboarding) {
|
presentedContent
|
||||||
OnboardingView {
|
.onAppear {
|
||||||
showOnboarding = false
|
// Launch-arg hooks for the screenshot/UI-test harness (same
|
||||||
|
// pattern as StationsView's `-showKeySheet`): `-tab <name>`
|
||||||
|
// opens the given tab; `-skipOnboarding` skips onboarding
|
||||||
|
// without touching the stored flag.
|
||||||
|
let args = ProcessInfo.processInfo.arguments
|
||||||
|
#if DEBUG
|
||||||
|
showWidgetMock = args.contains("-widgets")
|
||||||
|
// `-seedFavourite <substring>` pins the first matching station
|
||||||
|
// for Unleaded via the normal save path (keychain + app group)
|
||||||
|
// so screenshot captures can show a populated Favourites tab.
|
||||||
|
// Repeatable: each occurrence adds another favourite (QA temp).
|
||||||
|
// Optional ":fuel" suffix (e10|e5|diesel) picks the
|
||||||
|
// fuel, so the fuel-aware Trends open can be verified (QA temp).
|
||||||
|
let seedQueries = args.enumerated()
|
||||||
|
.filter { $0.element == "-seedFavourite" }
|
||||||
|
.compactMap { i, _ in
|
||||||
|
i + 1 < args.count ? args[i + 1] : nil
|
||||||
|
}
|
||||||
|
if !seedQueries.isEmpty {
|
||||||
|
let stations = FuelStore.loadStations()
|
||||||
|
var favs = seedQueries.compactMap { query in
|
||||||
|
let parts = query.split(separator: ":", maxSplits: 1)
|
||||||
|
let name = String(parts[0])
|
||||||
|
let fuel = parts.count > 1
|
||||||
|
? FuelType(rawValue: String(parts[1])) ?? .e10
|
||||||
|
: FuelType.e10
|
||||||
|
return stations.first { $0.name.localizedCaseInsensitiveContains(name) }
|
||||||
|
.map { FavouriteEntry(station: $0, fuel: fuel) }
|
||||||
|
}
|
||||||
|
if favs.isEmpty, let q = seedQueries.first,
|
||||||
|
let s = stations.first(where: { $0.name.localizedCaseInsensitiveContains(q.split(separator: ":").first.map(String.init) ?? q) }) {
|
||||||
|
favs = [FavouriteEntry(station: s, fuel: .e10)]
|
||||||
|
}
|
||||||
|
if !favs.isEmpty {
|
||||||
|
FuelStore.saveFavourites(favs)
|
||||||
|
WatchSyncManager.shared.pushSnapshot()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
if let i = args.firstIndex(of: "-tab"), i + 1 < args.count {
|
||||||
.sheet(item: $monitor.pendingStationMap) { request in
|
switch args[i + 1] {
|
||||||
StationMapView(request: request)
|
case "favourites": selectedTab = 1
|
||||||
}
|
case "alerts": selectedTab = 2
|
||||||
.onAppear {
|
case "settings": selectedTab = 3
|
||||||
|
default: selectedTab = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// QA temp: `-fuel <e10|e5|diesel>` sets the app-wide selected
|
||||||
|
// fuel so the fuel-aware Trends open can be verified (simulates
|
||||||
|
// the user having switched to the diesel tab).
|
||||||
|
if let i = args.firstIndex(of: "-fuel"), i + 1 < args.count,
|
||||||
|
let f = FuelType(rawValue: args[i + 1]) {
|
||||||
|
selectedFuel = f
|
||||||
|
FuelStore.saveSelectedFuel(f)
|
||||||
|
}
|
||||||
|
// `-forceOfflineDump` / `-forceConnectionProblem` simulate the two
|
||||||
|
// failure legs for the screenshot harness. The auto-refresh below
|
||||||
|
// is skipped so the banner stays up (a live fetch would clear it).
|
||||||
|
if args.contains("-forceOfflineDump") {
|
||||||
|
stations = BundledDumpProvider.stations ?? SampleFuelProvider.sampleStations
|
||||||
|
dataStatus = .offlineDump(date: FuelStore.offlineDataLabel(from: BundledDumpProvider.dataUpdatedStamp) ?? "")
|
||||||
|
}
|
||||||
|
if args.contains("-forceConnectionProblem") {
|
||||||
|
stations = FuelStore.loadStations().isEmpty
|
||||||
|
? (BundledDumpProvider.stations ?? SampleFuelProvider.sampleStations)
|
||||||
|
: FuelStore.loadStations()
|
||||||
|
dataStatus = .connectionProblem
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
showWidgetMock = false
|
||||||
|
#endif
|
||||||
// Onboarding runs first on a fresh install — it owns the initial
|
// Onboarding runs first on a fresh install — it owns the initial
|
||||||
// permission prompts (location, notifications, and the data/local
|
// permission prompts (location, notifications, and the data/local
|
||||||
// network probe on the Data page). Location tracking and the first
|
// network probe on the Data page). Location tracking and the first
|
||||||
// network fetch start once it's done.
|
// network fetch start once it's done.
|
||||||
if FuelStore.loadHasCompletedOnboarding() {
|
// Launch-arg hook (UI-testing/screenshot harness, same pattern as
|
||||||
|
// StationsView's `-showKeySheet`): skip onboarding without
|
||||||
|
// touching the stored flag.
|
||||||
|
#if DEBUG
|
||||||
|
let shouldSkipOnboarding = args.contains("-skipOnboarding")
|
||||||
|
#else
|
||||||
|
let shouldSkipOnboarding = false
|
||||||
|
#endif
|
||||||
|
if !FuelStore.shouldShowOnboarding()
|
||||||
|
|| shouldSkipOnboarding {
|
||||||
locationManager.startForegroundTracking()
|
locationManager.startForegroundTracking()
|
||||||
// Geofences must follow the user even in the background:
|
// Geofences and the Live Activity must follow the user even in
|
||||||
// wire the delegate hook (fires on every fix incl. background
|
// the background: wire the delegate hook (fires on every fix
|
||||||
// significant-change wake-ups) to re-register the region
|
// incl. background significant-change wake-ups) so SwiftUI's
|
||||||
// window around the new position. SwiftUI onChange alone
|
// foreground-only onChange isn't the only update path.
|
||||||
// never runs while suspended, so alerts would otherwise stay
|
installLocationUpdateHook()
|
||||||
// frozen around the last foreground fix.
|
ensureBackgroundMotionTrackingIfNeeded()
|
||||||
locationManager.onLocationUpdate = { [weak monitor] in
|
|
||||||
monitor?.update(stations: stations, favourites: refreshedFavourites,
|
|
||||||
fuel: alertsFuel, radiusKM: effectiveAlertsRadiusKM)
|
|
||||||
// The Live Activity follows the same wake-ups — this hook
|
|
||||||
// fires on every fix incl. background significant-change
|
|
||||||
// wake-ups, so the Lock Screen pill stays live while driving.
|
|
||||||
updateLiveActivity()
|
|
||||||
}
|
|
||||||
monitor.update(stations: stations, favourites: refreshedFavourites,
|
monitor.update(stations: stations, favourites: refreshedFavourites,
|
||||||
fuel: alertsFuel, radiusKM: effectiveAlertsRadiusKM)
|
fuel: alertsFuel, radiusKM: effectiveAlertsRadiusKM)
|
||||||
monitor.setEnabled(alertsEnabled)
|
monitor.setEnabled(alertsEnabled)
|
||||||
updateLiveActivity()
|
updateLiveActivity()
|
||||||
|
// Compute road distances early (throttled; falls back to the
|
||||||
|
// last saved location) so distance surfaces are road-matched
|
||||||
|
// as soon as stations are available.
|
||||||
|
refreshRoadDistancesIfNeeded()
|
||||||
// Refresh only when the cache is stale (twice-a-day policy).
|
// Refresh only when the cache is stale (twice-a-day policy).
|
||||||
Task { await refresh() }
|
// Skipped under the force-* hooks so the banner stays up.
|
||||||
|
#if DEBUG
|
||||||
|
let shouldSkipInitialRefresh = args.contains("-forceOfflineDump")
|
||||||
|
|| args.contains("-forceConnectionProblem")
|
||||||
|
|| args.contains("-forceHistoryFailure")
|
||||||
|
#else
|
||||||
|
let shouldSkipInitialRefresh = false
|
||||||
|
#endif
|
||||||
|
if !shouldSkipInitialRefresh {
|
||||||
|
Task { await refresh() }
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
showOnboarding = true
|
showOnboarding = true
|
||||||
}
|
}
|
||||||
@@ -231,6 +391,8 @@ struct ContentView: View {
|
|||||||
// empty on a brand-new install.
|
// empty on a brand-new install.
|
||||||
if !showing, FuelStore.loadHasCompletedOnboarding() {
|
if !showing, FuelStore.loadHasCompletedOnboarding() {
|
||||||
locationManager.startForegroundTracking()
|
locationManager.startForegroundTracking()
|
||||||
|
installLocationUpdateHook()
|
||||||
|
ensureBackgroundMotionTrackingIfNeeded()
|
||||||
monitor.update(stations: stations, favourites: refreshedFavourites,
|
monitor.update(stations: stations, favourites: refreshedFavourites,
|
||||||
fuel: alertsFuel, radiusKM: effectiveAlertsRadiusKM)
|
fuel: alertsFuel, radiusKM: effectiveAlertsRadiusKM)
|
||||||
updateLiveActivity()
|
updateLiveActivity()
|
||||||
@@ -248,7 +410,11 @@ struct ContentView: View {
|
|||||||
monitor.update(stations: stations, favourites: refreshedFavourites,
|
monitor.update(stations: stations, favourites: refreshedFavourites,
|
||||||
fuel: alertsFuel, radiusKM: effectiveAlertsRadiusKM)
|
fuel: alertsFuel, radiusKM: effectiveAlertsRadiusKM)
|
||||||
updateLiveActivity()
|
updateLiveActivity()
|
||||||
// No network fetch on foreground — pull-to-refresh is the override.
|
refreshRoadDistancesIfNeeded()
|
||||||
|
if FuelStore.hasPendingWatchRefreshRequest {
|
||||||
|
Task { await handlePendingWatchRefreshRequest() }
|
||||||
|
}
|
||||||
|
// No network fetch on foreground unless the watch explicitly requests one.
|
||||||
} else {
|
} else {
|
||||||
locationManager.stopForegroundTracking()
|
locationManager.stopForegroundTracking()
|
||||||
}
|
}
|
||||||
@@ -257,17 +423,26 @@ struct ContentView: View {
|
|||||||
if let newLocation {
|
if let newLocation {
|
||||||
location = newLocation
|
location = newLocation
|
||||||
FuelStore.saveLocation(lat: newLocation.lat, lng: newLocation.lng)
|
FuelStore.saveLocation(lat: newLocation.lat, lng: newLocation.lng)
|
||||||
|
WatchSyncManager.shared.pushSnapshot()
|
||||||
WidgetCenter.shared.reloadAllTimelines()
|
WidgetCenter.shared.reloadAllTimelines()
|
||||||
// Geofences follow the user's position, but the station list is
|
// Geofences follow the user's position, but the station list is
|
||||||
// NOT re-fetched on every movement (cached, twice-a-day policy).
|
// NOT re-fetched on every movement (cached, twice-a-day policy).
|
||||||
monitor.update(stations: stations, favourites: refreshedFavourites,
|
monitor.update(stations: stations, favourites: refreshedFavourites,
|
||||||
fuel: alertsFuel, radiusKM: effectiveAlertsRadiusKM)
|
fuel: alertsFuel, radiusKM: effectiveAlertsRadiusKM)
|
||||||
updateLiveActivity()
|
updateLiveActivity()
|
||||||
|
refreshRoadDistancesIfNeeded()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onChange(of: selectedFuel) { _, _ in
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var interactionObservedContent: AnyView {
|
||||||
|
AnyView(
|
||||||
|
lifecycleObservedContent
|
||||||
|
.onChange(of: selectedFuel) { _, _ in
|
||||||
// No re-fetch needed — one response carries E5/E10/DIESEL prices.
|
// No re-fetch needed — one response carries E5/E10/DIESEL prices.
|
||||||
WidgetCenter.shared.reloadAllTimelines()
|
WidgetCenter.shared.reloadAllTimelines()
|
||||||
|
WatchSyncManager.shared.pushSnapshot()
|
||||||
}
|
}
|
||||||
.onChange(of: stationLimit) { _, newValue in
|
.onChange(of: stationLimit) { _, newValue in
|
||||||
// Distance filter is LOCAL math now — the cache holds the full-UK
|
// Distance filter is LOCAL math now — the cache holds the full-UK
|
||||||
@@ -288,9 +463,7 @@ struct ContentView: View {
|
|||||||
monitor.setEnabled(newValue)
|
monitor.setEnabled(newValue)
|
||||||
monitor.update(stations: stations, favourites: refreshedFavourites,
|
monitor.update(stations: stations, favourites: refreshedFavourites,
|
||||||
fuel: alertsFuel, radiusKM: effectiveAlertsRadiusKM)
|
fuel: alertsFuel, radiusKM: effectiveAlertsRadiusKM)
|
||||||
if newValue {
|
ensureBackgroundMotionTrackingIfNeeded()
|
||||||
locationManager.startBackgroundTracking()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.onChange(of: alertsRadius) { _, newValue in
|
.onChange(of: alertsRadius) { _, newValue in
|
||||||
FuelStore.saveAlertsRadius(newValue)
|
FuelStore.saveAlertsRadius(newValue)
|
||||||
@@ -311,10 +484,17 @@ struct ContentView: View {
|
|||||||
monitor.update(stations: stations, favourites: refreshedFavourites,
|
monitor.update(stations: stations, favourites: refreshedFavourites,
|
||||||
fuel: alertsFuel, radiusKM: effectiveAlertsRadiusKM)
|
fuel: alertsFuel, radiusKM: effectiveAlertsRadiusKM)
|
||||||
}
|
}
|
||||||
|
.onChange(of: favouriteDropAlertsEnabled) { _, newValue in
|
||||||
|
FuelStore.saveFavouriteDropAlertsEnabled(newValue)
|
||||||
|
}
|
||||||
|
.onChange(of: favouriteDropAlertsFuel) { _, newValue in
|
||||||
|
FuelStore.saveFavouriteDropAlertsFuel(newValue)
|
||||||
|
}
|
||||||
.onChange(of: liveActivityEnabled) { _, newValue in
|
.onChange(of: liveActivityEnabled) { _, newValue in
|
||||||
// Toggling the Live Activity on starts it with the current best
|
// Toggling the Live Activity on starts it with the current best
|
||||||
// station; toggling off ends any running activity.
|
// station; toggling off ends any running activity.
|
||||||
FuelStore.saveLiveActivityEnabled(newValue)
|
FuelStore.saveLiveActivityEnabled(newValue)
|
||||||
|
ensureBackgroundMotionTrackingIfNeeded()
|
||||||
updateLiveActivity()
|
updateLiveActivity()
|
||||||
}
|
}
|
||||||
.onChange(of: liveActivityFuel) { _, newValue in
|
.onChange(of: liveActivityFuel) { _, newValue in
|
||||||
@@ -342,13 +522,220 @@ struct ContentView: View {
|
|||||||
fuel: alertsFuel, radiusKM: effectiveAlertsRadiusKM)
|
fuel: alertsFuel, radiusKM: effectiveAlertsRadiusKM)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.onChange(of: priceDisplayStyle) { _, newValue in
|
||||||
|
// The pill carries its own display style in content state — push
|
||||||
|
// an update so the running activity re-renders in the new style
|
||||||
|
// immediately. Pass the style explicitly: SettingsView saves it
|
||||||
|
// in its own onChange, whose ordering vs this one isn't guaranteed,
|
||||||
|
// so reading storage here could push the OLD style.
|
||||||
|
updateLiveActivity(priceDisplayStyleOverride: newValue)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The one banner to show right now. Network/offline and tip outcomes all
|
||||||
|
/// funnel here and render through the SAME floating chrome. A transient
|
||||||
|
/// tip (auto-dismissed by TipStore) takes precedence over the persistent
|
||||||
|
/// network state for its ~3.5 s.
|
||||||
|
private var activeBanner: AppBanner? {
|
||||||
|
if let outcome = tipStore.outcome { return .tip(outcome) }
|
||||||
|
switch dataStatus {
|
||||||
|
case .live: return nil
|
||||||
|
case .offlineDump(let date): return .offlineDump(date: date)
|
||||||
|
case .connectionProblem: return .connectionProblem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var mainContent: some View {
|
||||||
|
GeometryReader { geo in
|
||||||
|
ZStack(alignment: .top) {
|
||||||
|
rootTabView
|
||||||
|
if let banner = currentBanner {
|
||||||
|
// Floating near the top of the screen, over the nav area —
|
||||||
|
// overlays content (never pushes it) and sits above the
|
||||||
|
// main content so it doesn't cover or block the list/pill
|
||||||
|
// beneath it. Opacity/offset are driven explicitly by
|
||||||
|
// reflectBanner so the fade-out reliably animates.
|
||||||
|
floatingBanner(banner)
|
||||||
|
.padding(.top, geo.safeAreaInsets.top + 10)
|
||||||
|
.opacity(bannerOpacity)
|
||||||
|
.offset(y: bannerOffset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.task(id: activeBanner) {
|
||||||
|
// Fires on launch with the current banner AND whenever it changes —
|
||||||
|
// unlike onChange(computed) which can miss the first non-nil value
|
||||||
|
// when dataStatus is set slightly after the view appears.
|
||||||
|
reflectBanner(activeBanner)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Explicitly animates the banner in/out — a robust replacement for the
|
||||||
|
/// SwiftUI removal transition (which was silently not firing). Handles:
|
||||||
|
/// - first appearance → fade + settle down from slightly above
|
||||||
|
/// - any content change (tip→network, new tip) → fade the old out, then
|
||||||
|
/// fade the new in — so a tip ALWAYS visibly fades away
|
||||||
|
/// - dismissal → fade out + slide up, then clear after the fade
|
||||||
|
private func reflectBanner(_ newBanner: AppBanner?) {
|
||||||
|
guard newBanner != currentBanner else { return }
|
||||||
|
bannerClearTask?.cancel()
|
||||||
|
if let newBanner {
|
||||||
|
if currentBanner == nil {
|
||||||
|
present(newBanner)
|
||||||
|
} else {
|
||||||
|
// Crossfade: slide+fade the current out, then present the new.
|
||||||
|
withAnimation(.easeOut(duration: 0.2)) {
|
||||||
|
bannerOpacity = 0
|
||||||
|
bannerOffset = -28
|
||||||
|
}
|
||||||
|
bannerClearTask = Task { @MainActor in
|
||||||
|
try? await Task.sleep(nanoseconds: 250_000_000)
|
||||||
|
guard !Task.isCancelled else { return }
|
||||||
|
present(newBanner)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
withAnimation(.easeOut(duration: 0.35)) {
|
||||||
|
bannerOpacity = 0
|
||||||
|
bannerOffset = -28
|
||||||
|
}
|
||||||
|
bannerClearTask = Task { @MainActor in
|
||||||
|
try? await Task.sleep(nanoseconds: 350_000_000)
|
||||||
|
guard !Task.isCancelled else { return }
|
||||||
|
currentBanner = nil
|
||||||
|
bannerOpacity = 0
|
||||||
|
bannerOffset = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fade a banner in from slightly above and settle it into place.
|
||||||
|
private func present(_ banner: AppBanner) {
|
||||||
|
currentBanner = banner
|
||||||
|
bannerOpacity = 0
|
||||||
|
bannerOffset = -16
|
||||||
|
withAnimation(.easeOut(duration: 0.3)) {
|
||||||
|
bannerOpacity = 1
|
||||||
|
bannerOffset = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var rootTabView: some View {
|
||||||
|
TabView(selection: $selectedTab) {
|
||||||
|
stationsTab
|
||||||
|
.tabItem { Label("Stations", systemImage: "fuelpump.fill") }
|
||||||
|
.tag(0)
|
||||||
|
|
||||||
|
favouritesTab
|
||||||
|
.tabItem { Label("Favourites", systemImage: "star.fill") }
|
||||||
|
.tag(1)
|
||||||
|
|
||||||
|
alertsTab
|
||||||
|
.tabItem { Label("Alerts", systemImage: "bell.fill") }
|
||||||
|
.tag(2)
|
||||||
|
|
||||||
|
settingsTab
|
||||||
|
.tag(3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The banner title for the bundled-snapshot case: date when the stamp
|
||||||
|
/// parsed, plain "Offline data" otherwise.
|
||||||
|
private func offlineTitle(date: String) -> String {
|
||||||
|
date.isEmpty
|
||||||
|
? NSLocalizedString("Offline data", comment: "")
|
||||||
|
: String(format: NSLocalizedString("Offline data from %@", comment: ""), date)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ONE floating banner style for all notifications. Network/offline and
|
||||||
|
/// tip outcomes share the same chrome; only the tap action differs —
|
||||||
|
/// network → retry the fetch, tip → dismiss (TipStore auto-dismisses too).
|
||||||
|
@ViewBuilder
|
||||||
|
private func floatingBanner(_ banner: AppBanner) -> some View {
|
||||||
|
switch banner {
|
||||||
|
case .offlineDump(let date):
|
||||||
|
let title = offlineTitle(date: date)
|
||||||
|
statusBannerCard(
|
||||||
|
icon: "wifi.slash", tint: .orange, title: title,
|
||||||
|
subtitle: NSLocalizedString("Pull to refresh on the Stations tab", comment: ""),
|
||||||
|
trailingIcon: "arrow.clockwise",
|
||||||
|
accessibilityLabel: date.isEmpty
|
||||||
|
? NSLocalizedString("Offline data. Pull to refresh on the Stations tab", comment: "")
|
||||||
|
: String(format: NSLocalizedString("Offline data from %@. Pull to refresh on the Stations tab", comment: ""), date)
|
||||||
|
) {
|
||||||
|
Task { await refresh(force: true) }
|
||||||
|
}
|
||||||
|
case .connectionProblem:
|
||||||
|
statusBannerCard(
|
||||||
|
icon: "wifi.exclamationmark", tint: .red,
|
||||||
|
title: NSLocalizedString("Check your internet connection", comment: ""),
|
||||||
|
subtitle: NSLocalizedString("Tap to try again", comment: ""),
|
||||||
|
trailingIcon: "arrow.clockwise",
|
||||||
|
accessibilityLabel: NSLocalizedString("Check your internet connection. Tap to try again", comment: "")
|
||||||
|
) {
|
||||||
|
Task { await refresh(force: true) }
|
||||||
|
}
|
||||||
|
case .tip(let outcome):
|
||||||
|
statusBannerCard(
|
||||||
|
icon: outcome.icon, tint: outcome.tint, title: outcome.message, subtitle: nil,
|
||||||
|
trailingIcon: "xmark", accessibilityLabel: outcome.message
|
||||||
|
) {
|
||||||
|
withAnimation(.easeOut(duration: 0.2)) { tipStore.dismissOutcome() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The single floating-banner chrome shared by every notification
|
||||||
|
/// (network/offline + tip). Overlaid — never in flow — so it never moves
|
||||||
|
/// the content beneath it.
|
||||||
|
private func statusBannerCard(icon: String, tint: Color, title: String, subtitle: String?,
|
||||||
|
trailingIcon: String, accessibilityLabel: String,
|
||||||
|
action: @escaping () -> Void) -> some View {
|
||||||
|
Button(action: action) {
|
||||||
|
HStack(spacing: 10) {
|
||||||
|
Image(systemName: icon)
|
||||||
|
.font(.system(size: 17, weight: .semibold))
|
||||||
|
.foregroundStyle(tint)
|
||||||
|
.frame(width: 30)
|
||||||
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
|
Text(title)
|
||||||
|
.font(.subheadline.weight(.semibold))
|
||||||
|
.foregroundStyle(.primary)
|
||||||
|
if let subtitle {
|
||||||
|
Text(subtitle)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
Image(systemName: trailingIcon)
|
||||||
|
.font(.system(size: 14, weight: .semibold))
|
||||||
|
.foregroundStyle(tint)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 14)
|
||||||
|
.padding(.vertical, 10)
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
||||||
|
.fill(Color(.secondarySystemGroupedBackground))
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
||||||
|
.stroke(tint.opacity(0.35), lineWidth: 1)
|
||||||
|
)
|
||||||
|
.shadow(color: .black.opacity(0.08), radius: 8, y: 3)
|
||||||
|
)
|
||||||
|
.padding(.horizontal, 12)
|
||||||
|
}
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.accessibilityLabel(accessibilityLabel)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Pushes the current best-in-radius station into the Live Activity.
|
/// Pushes the current best-in-radius station into the Live Activity.
|
||||||
/// No-op (or ends the activity) when the toggle is off or there's no
|
/// No-op (or ends the activity) when the toggle is off or there's no
|
||||||
/// location/data yet. Uses the Live Activity's OWN fuel + radius (set in
|
/// location/data yet. Uses the Live Activity's OWN fuel + radius (set in
|
||||||
/// Settings) — independent of the Stations-tab fuel/distance.
|
/// Settings) — independent of the Stations-tab fuel/distance.
|
||||||
private func updateLiveActivity() {
|
private func updateLiveActivity(priceDisplayStyleOverride: PriceDisplayStyle? = nil) {
|
||||||
// "Follow search" mirrors the Stations-tab distance exactly (no cap —
|
// "Follow search" mirrors the Stations-tab distance exactly (no cap —
|
||||||
// the pill only displays, it doesn't geofence).
|
// the pill only displays, it doesn't geofence).
|
||||||
let radiusKM = liveActivityFollowsSearch
|
let radiusKM = liveActivityFollowsSearch
|
||||||
@@ -359,7 +746,71 @@ struct ContentView: View {
|
|||||||
fuel: liveActivityFuel,
|
fuel: liveActivityFuel,
|
||||||
radiusKM: radiusKM,
|
radiusKM: radiusKM,
|
||||||
location: location,
|
location: location,
|
||||||
enabled: liveActivityEnabled
|
enabled: liveActivityEnabled,
|
||||||
|
priceDisplayStyle: priceDisplayStyleOverride
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The Stations tab, extracted from `body` so the TabView expression stays
|
||||||
|
/// within the compiler's type-check budget.
|
||||||
|
private var stationsTab: some View {
|
||||||
|
StationsView(
|
||||||
|
stations: displayedStations,
|
||||||
|
totalCount: sortedStations.count,
|
||||||
|
isLoading: isLoading,
|
||||||
|
selectedFuel: $selectedFuel,
|
||||||
|
sortMode: $sortMode,
|
||||||
|
stationLimit: $stationLimit,
|
||||||
|
distanceUnit: distanceUnit,
|
||||||
|
priceDisplayStyle: priceDisplayStyle,
|
||||||
|
baselinePrice: baselinePrice,
|
||||||
|
topStationID: topStationID,
|
||||||
|
location: location,
|
||||||
|
favouriteIDs: favouriteIDs,
|
||||||
|
stationTapAction: stationTapAction,
|
||||||
|
onToggleFavourite: toggleFavourite,
|
||||||
|
onRefresh: { await refresh(force: true) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The Favourites tab, extracted from `body` for the same type-check
|
||||||
|
/// budget reason.
|
||||||
|
private var favouritesTab: some View {
|
||||||
|
FavouritesView(
|
||||||
|
favourites: refreshedFavourites,
|
||||||
|
selectedFuel: selectedFuel,
|
||||||
|
location: location,
|
||||||
|
distanceUnit: distanceUnit,
|
||||||
|
priceDisplayStyle: priceDisplayStyle,
|
||||||
|
stationTapAction: stationTapAction,
|
||||||
|
onToggleFavourite: toggleFavourite,
|
||||||
|
onReorder: reorderFavourites,
|
||||||
|
onHistoryUnavailable: { if dataStatus == .live { dataStatus = .connectionProblem } },
|
||||||
|
onHistoryRecovered: { if dataStatus == .connectionProblem { dataStatus = .live } }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The Alerts tab, extracted from `body` so the TabView expression stays
|
||||||
|
/// within the compiler's type-check budget.
|
||||||
|
private var alertsTab: some View {
|
||||||
|
AlertsView(
|
||||||
|
enabled: $alertsEnabled,
|
||||||
|
radius: $alertsRadius,
|
||||||
|
fuel: $alertsFuel,
|
||||||
|
favouriteDropEnabled: $favouriteDropAlertsEnabled,
|
||||||
|
favouriteDropFuel: $favouriteDropAlertsFuel,
|
||||||
|
followsSearch: $alertsFollowsSearch,
|
||||||
|
liveActivityEnabled: $liveActivityEnabled,
|
||||||
|
liveActivityFuel: $liveActivityFuel,
|
||||||
|
liveActivityRadiusMiles: $liveActivityRadiusMiles,
|
||||||
|
liveActivityFollowsSearch: $liveActivityFollowsSearch,
|
||||||
|
stationLimit: stationLimit,
|
||||||
|
distanceUnit: distanceUnit,
|
||||||
|
monitoredCount: monitor.monitoredStationIDs.count,
|
||||||
|
lastAlert: monitor.lastAlert,
|
||||||
|
onCheckFavouriteDropNow: {
|
||||||
|
Task { await refresh(force: true) }
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -367,7 +818,11 @@ struct ContentView: View {
|
|||||||
/// within the compiler's type-check budget.
|
/// within the compiler's type-check budget.
|
||||||
private var settingsTab: some View {
|
private var settingsTab: some View {
|
||||||
SettingsView(
|
SettingsView(
|
||||||
|
tipStore: tipStore,
|
||||||
distanceUnit: $distanceUnit,
|
distanceUnit: $distanceUnit,
|
||||||
|
priceDisplayStyle: $priceDisplayStyle,
|
||||||
|
stationTapAction: $stationTapAction,
|
||||||
|
dataRefreshMode: $dataRefreshMode,
|
||||||
alertsFuel: alertsFuel,
|
alertsFuel: alertsFuel,
|
||||||
alertsRadiusKM: alertsRadius,
|
alertsRadiusKM: alertsRadius,
|
||||||
testAlertResult: monitor.lastTestResult,
|
testAlertResult: monitor.lastTestResult,
|
||||||
@@ -378,7 +833,12 @@ struct ContentView: View {
|
|||||||
debugStatus: monitor.debugStatus,
|
debugStatus: monitor.debugStatus,
|
||||||
appCheapest: appCheapestStatus,
|
appCheapest: appCheapestStatus,
|
||||||
regionError: monitor.lastRegionError,
|
regionError: monitor.lastRegionError,
|
||||||
monitoredCount: monitor.monitoredStationIDs.count
|
monitoredCount: monitor.monitoredStationIDs.count,
|
||||||
|
alertLog: monitor.alertLog,
|
||||||
|
regionEventCount: monitor.regionEventCount,
|
||||||
|
debugFenceIdentifier: monitor.debugFenceIdentifier,
|
||||||
|
onDebugFence: { monitor.registerDebugFenceAroundMe() },
|
||||||
|
onClearDebugFence: { monitor.clearDebugFence() }
|
||||||
)
|
)
|
||||||
.tabItem { Label("Settings", systemImage: "gearshape.fill") }
|
.tabItem { Label("Settings", systemImage: "gearshape.fill") }
|
||||||
}
|
}
|
||||||
@@ -391,6 +851,7 @@ struct ContentView: View {
|
|||||||
favourites.append(key)
|
favourites.append(key)
|
||||||
}
|
}
|
||||||
FuelStore.saveFavourites(favourites)
|
FuelStore.saveFavourites(favourites)
|
||||||
|
WatchSyncManager.shared.pushSnapshot()
|
||||||
WidgetCenter.shared.reloadAllTimelines()
|
WidgetCenter.shared.reloadAllTimelines()
|
||||||
monitor.update(stations: stations, favourites: refreshedFavourites,
|
monitor.update(stations: stations, favourites: refreshedFavourites,
|
||||||
fuel: alertsFuel, radiusKM: effectiveAlertsRadiusKM)
|
fuel: alertsFuel, radiusKM: effectiveAlertsRadiusKM)
|
||||||
@@ -402,11 +863,17 @@ struct ContentView: View {
|
|||||||
private func reorderFavourites(_ newOrder: [FavouriteEntry]) {
|
private func reorderFavourites(_ newOrder: [FavouriteEntry]) {
|
||||||
favourites = newOrder
|
favourites = newOrder
|
||||||
FuelStore.saveFavourites(favourites)
|
FuelStore.saveFavourites(favourites)
|
||||||
|
WatchSyncManager.shared.pushSnapshot()
|
||||||
WidgetCenter.shared.reloadAllTimelines()
|
WidgetCenter.shared.reloadAllTimelines()
|
||||||
monitor.update(stations: stations, favourites: refreshedFavourites,
|
monitor.update(stations: stations, favourites: refreshedFavourites,
|
||||||
fuel: alertsFuel, radiusKM: effectiveAlertsRadiusKM)
|
fuel: alertsFuel, radiusKM: effectiveAlertsRadiusKM)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func handlePendingWatchRefreshRequest() async {
|
||||||
|
guard FuelStore.hasPendingWatchRefreshRequest else { return }
|
||||||
|
await refresh(force: true)
|
||||||
|
}
|
||||||
|
|
||||||
/// Fetches fresh prices, but only when the cache is stale — unless
|
/// Fetches fresh prices, but only when the cache is stale — unless
|
||||||
/// `force` is true (pull-to-refresh is the manual override).
|
/// `force` is true (pull-to-refresh is the manual override).
|
||||||
private func refresh(force: Bool = false) async {
|
private func refresh(force: Bool = false) async {
|
||||||
@@ -416,6 +883,7 @@ struct ContentView: View {
|
|||||||
isLoading = true
|
isLoading = true
|
||||||
defer { isLoading = false }
|
defer { isLoading = false }
|
||||||
do {
|
do {
|
||||||
|
let previousFavouriteSnapshots = FuelStore.loadFavouriteAlertSnapshots()
|
||||||
let fetched = try await FuelPriceProvider.active.fetchStations(
|
let fetched = try await FuelPriceProvider.active.fetchStations(
|
||||||
near: location?.lat, lng: location?.lng,
|
near: location?.lat, lng: location?.lng,
|
||||||
fuel: selectedFuel,
|
fuel: selectedFuel,
|
||||||
@@ -424,9 +892,14 @@ struct ContentView: View {
|
|||||||
stations = fetched
|
stations = fetched
|
||||||
FuelStore.saveStations(fetched)
|
FuelStore.saveStations(fetched)
|
||||||
FuelStore.saveLastRefresh()
|
FuelStore.saveLastRefresh()
|
||||||
// Persist relay envelope metadata (source, station count, GOV.UK
|
if force, FuelStore.hasPendingWatchRefreshRequest {
|
||||||
// dataset update time) for the Settings → About section.
|
FuelStore.markWatchRefreshHandled()
|
||||||
if let meta = RelayFuelProvider.latestMeta {
|
}
|
||||||
|
WatchSyncManager.shared.pushSnapshot()
|
||||||
|
// Persist envelope metadata (source, station count, GOV.UK
|
||||||
|
// dataset update time) for the Settings → About section — the
|
||||||
|
// live chain records whichever leg served the fetch.
|
||||||
|
if let meta = LiveChainProvider.latestMeta {
|
||||||
FuelStore.saveRelayMeta(meta)
|
FuelStore.saveRelayMeta(meta)
|
||||||
}
|
}
|
||||||
// Keep the keychain favourites fresh with the new prices — the
|
// Keep the keychain favourites fresh with the new prices — the
|
||||||
@@ -434,11 +907,38 @@ struct ContentView: View {
|
|||||||
// channel shared on SideStore free), so stale star-time snapshots
|
// channel shared on SideStore free), so stale star-time snapshots
|
||||||
// would otherwise show old prices.
|
// would otherwise show old prices.
|
||||||
FuelStore.saveFavourites(refreshedFavourites)
|
FuelStore.saveFavourites(refreshedFavourites)
|
||||||
|
monitor.evaluateFavouritePriceDropAlert(
|
||||||
|
favourites: refreshedFavourites,
|
||||||
|
previousSnapshots: previousFavouriteSnapshots,
|
||||||
|
monitoredFuel: favouriteDropAlertsFuel,
|
||||||
|
enabled: favouriteDropAlertsEnabled
|
||||||
|
)
|
||||||
WidgetCenter.shared.reloadAllTimelines()
|
WidgetCenter.shared.reloadAllTimelines()
|
||||||
statusMessage = "Loaded \(fetched.count) stations · \(Date().formatted(date: .omitted, time: .shortened))"
|
statusMessage = "Loaded \(fetched.count) stations · \(Date().formatted(date: .omitted, time: .shortened))"
|
||||||
|
// Live data restored — any status banner no longer applies.
|
||||||
|
dataStatus = .live
|
||||||
} catch {
|
} catch {
|
||||||
statusMessage = "Live fetch failed: \(error.localizedDescription). Showing cached/sample data."
|
if force, FuelStore.hasPendingWatchRefreshRequest {
|
||||||
stations = FuelStore.loadStations().isEmpty ? SampleFuelProvider.sampleStations : FuelStore.loadStations()
|
FuelStore.markWatchRefreshHandled()
|
||||||
|
}
|
||||||
|
statusMessage = "Live fetch failed: \(error.localizedDescription). Showing cached data."
|
||||||
|
if FuelStore.loadStations().isEmpty {
|
||||||
|
// No cached prices — last resort is the bundled REAL dump
|
||||||
|
// (stale but genuine), then the demo sample set. The banner
|
||||||
|
// labels the bundled snapshot honestly with its own date.
|
||||||
|
if let dump = BundledDumpProvider.stations {
|
||||||
|
stations = dump
|
||||||
|
dataStatus = .offlineDump(date: FuelStore.offlineDataLabel(from: BundledDumpProvider.dataUpdatedStamp) ?? "")
|
||||||
|
} else {
|
||||||
|
stations = SampleFuelProvider.sampleStations
|
||||||
|
dataStatus = .live
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Saved prices are still on screen — but the fetch failed, so
|
||||||
|
// say so: a stale cache must not look like a live app.
|
||||||
|
stations = FuelStore.loadStations()
|
||||||
|
dataStatus = .connectionProblem
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Keep monitor geofences in sync with the freshest data.
|
// Keep monitor geofences in sync with the freshest data.
|
||||||
monitor.update(stations: stations, favourites: refreshedFavourites,
|
monitor.update(stations: stations, favourites: refreshedFavourites,
|
||||||
@@ -455,10 +955,13 @@ struct StationRow: View {
|
|||||||
let fuel: FuelType
|
let fuel: FuelType
|
||||||
let location: Coordinate?
|
let location: Coordinate?
|
||||||
let distanceUnit: DistanceUnit
|
let distanceUnit: DistanceUnit
|
||||||
|
let priceDisplayStyle: PriceDisplayStyle
|
||||||
let baselinePrice: Double?
|
let baselinePrice: Double?
|
||||||
let isTopResult: Bool
|
let isTopResult: Bool
|
||||||
let isFavourite: Bool
|
let isFavourite: Bool
|
||||||
|
var tapAction: StationTapAction = .openMap
|
||||||
var onToggleFavourite: () -> Void = {}
|
var onToggleFavourite: () -> Void = {}
|
||||||
|
var onShowDetails: (FuelStation) -> Void = { _ in }
|
||||||
|
|
||||||
private var ragColor: Color {
|
private var ragColor: Color {
|
||||||
guard let price = station.prices[fuel], let baselinePrice else { return .gray }
|
guard let price = station.prices[fuel], let baselinePrice else { return .gray }
|
||||||
@@ -478,6 +981,21 @@ struct StationRow: View {
|
|||||||
return String(format: "%+.1fp", delta)
|
return String(format: "%+.1fp", delta)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func handleTap() {
|
||||||
|
switch tapAction {
|
||||||
|
case .openMap:
|
||||||
|
if let url = station.mapsDirectionsURL {
|
||||||
|
UIApplication.shared.open(url)
|
||||||
|
}
|
||||||
|
case .showDetails:
|
||||||
|
showDetails()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func showDetails() {
|
||||||
|
onShowDetails(station)
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(spacing: 12) {
|
HStack(spacing: 12) {
|
||||||
// LEFT — round brand logo (or generic fuel pump fallback)
|
// LEFT — round brand logo (or generic fuel pump fallback)
|
||||||
@@ -489,14 +1007,14 @@ struct StationRow: View {
|
|||||||
.padding(5)
|
.padding(5)
|
||||||
} else {
|
} else {
|
||||||
Image(systemName: "fuelpump.fill")
|
Image(systemName: "fuelpump.fill")
|
||||||
.font(.system(size: 22))
|
.font(.system(size: 22, weight: .semibold))
|
||||||
.foregroundStyle(.white)
|
.foregroundStyle(Color.black.opacity(0.82))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(width: 46, height: 46)
|
.frame(width: 46, height: 46)
|
||||||
.background(Circle().fill(.white))
|
.background(Circle().fill(Color.white))
|
||||||
.clipShape(Circle())
|
.clipShape(Circle())
|
||||||
.overlay(Circle().stroke(Color.primary.opacity(0.08), lineWidth: 1))
|
.overlay(Circle().stroke(Color.primary.opacity(0.10), lineWidth: 1))
|
||||||
.shadow(color: .black.opacity(0.08), radius: 2, y: 1)
|
.shadow(color: .black.opacity(0.08), radius: 2, y: 1)
|
||||||
|
|
||||||
// MIDDLE — name, full address, distance
|
// MIDDLE — name, full address, distance
|
||||||
@@ -514,6 +1032,12 @@ struct StationRow: View {
|
|||||||
.background(Capsule().fill(.blue.opacity(0.15)))
|
.background(Capsule().fill(.blue.opacity(0.15)))
|
||||||
.foregroundStyle(.blue)
|
.foregroundStyle(.blue)
|
||||||
}
|
}
|
||||||
|
ForEach(StationNotice.notices(for: station)) { notice in
|
||||||
|
Image(systemName: notice.symbolName)
|
||||||
|
.font(.caption2.weight(.semibold))
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.accessibilityLabel(notice.title)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Text("\(station.address), \(station.postcode)")
|
Text("\(station.address), \(station.postcode)")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
@@ -521,7 +1045,7 @@ struct StationRow: View {
|
|||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
.truncationMode(.tail)
|
.truncationMode(.tail)
|
||||||
if let location {
|
if let location {
|
||||||
Text(distanceUnit.format(station.distanceKM(to: location.lat, lng2: location.lng)))
|
Text(distanceUnit.format(FuelStore.displayDistanceKM(station: station, userLat: location.lat, userLng: location.lng)))
|
||||||
.font(.caption2)
|
.font(.caption2)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
.monospacedDigit()
|
.monospacedDigit()
|
||||||
@@ -536,9 +1060,7 @@ struct StationRow: View {
|
|||||||
Circle()
|
Circle()
|
||||||
.fill(ragColor)
|
.fill(ragColor)
|
||||||
.frame(width: 8, height: 8)
|
.frame(width: 8, height: 8)
|
||||||
Text(String(format: "£%.3f", price / 100))
|
FuelStore.priceTextAttributed(price, style: priceDisplayStyle, size: 20, weight: .bold)
|
||||||
.font(.title3.bold().monospaced())
|
|
||||||
.monospacedDigit()
|
|
||||||
}
|
}
|
||||||
if let deltaText {
|
if let deltaText {
|
||||||
Text(deltaText)
|
Text(deltaText)
|
||||||
@@ -558,11 +1080,8 @@ struct StationRow: View {
|
|||||||
.buttonStyle(.borderless)
|
.buttonStyle(.borderless)
|
||||||
}
|
}
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
.onTapGesture {
|
.onTapGesture(perform: handleTap)
|
||||||
if let url = station.mapsDirectionsURL {
|
.onLongPressGesture(perform: showDetails)
|
||||||
UIApplication.shared.open(url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||