Compare commits

..
Author SHA1 Message Date
FuelBoard Contributor 7633a8df90 Add FuelBoard device and IPA helpers 2026-09-26 20:03:40 +01:00
FuelBoard Contributor b8f267814c Update FuelBoard assets and bundled data 2026-09-26 20:03:35 +01:00
FuelBoard Contributor c3a975eb10 Harden watch sync and App Group storage 2026-09-26 20:03:29 +01:00
FuelBoard Contributor 004fe2f787 Configure FuelBoard XCTest target 2026-09-26 20:03:20 +01:00
FuelBoard Contributor 6900345aab Ignore generated build directories 2026-09-26 19:34:50 +01:00
FuelBoard Contributor d0ee8edac3 Document and support StoreKit sandbox testing 2026-09-26 19:34:41 +01:00
FuelBoard Contributor 20f53b9290 Improve StoreKit tip diagnostics and pricing 2026-09-26 14:38:32 +01:00
FuelBoard Contributor 2921271be7 chore: update build settings and screenshots 2026-09-20 14:43:01 +01:00
FuelBoard Contributor 7cb23e09c2 fix: retry StoreKit tip product lookup 2026-09-20 14:33:28 +01:00
FuelBoard Contributor 7dbd48786f docs: add FuelBoard privacy policy 2026-09-19 14:04:23 +01:00
FuelBoard Contributor 3a1ef0bd59 feat: indicate Costco membership requirement 2026-09-19 08:10:15 +01:00
FuelBoard Contributor 551a40ddf0 feat: show station info on long press 2026-09-19 07:41:52 +01:00
FuelBoard Contributor 3c83c9b518 fix: shorten report confirmation action 2026-09-17 22:30:25 +01:00
FuelBoard Contributor 6c8fd8b0de feat: add GOV.UK report issue action 2026-09-17 22:18:27 +01:00
FuelBoard Contributor 44cbd3b1e4 feat: route all Live Activity island taps through Tap station action
Expanded leading/trailing, compact and minimal island views had
no Link, so tapping them just opened the app with no destination.
Every island region now links to the same tap destination as the
Lock Screen card (Maps vs in-app detail).

Tap hint captions follow the setting too: Tap for details vs
Tap for directions.
2026-09-16 16:30:27 +01:00
FuelBoard Contributor 20fe251b8e feat: honour Tap station action in widgets
Small widgetURL and medium list Links route through
StationTapAction via FuelStation.tapDestinationURL: Open map
keeps the Maps URL, More info uses a fuelboard://station
deep link (stationID plus coordinate/name fallback).

Deep link carries fallback coords; ContentView presents the
detail sheet when the station is cached, else the map sheet.
Live Activity detailURL upgraded to the same rich link.
2026-09-16 16:15:07 +01:00
FuelBoard Contributor 465e6cd4d4 feat: honour Tap station action on notification and Live Activity taps
Notification body/action taps route through StationTapAction:
Open map opens Apple Maps (existing); More info presents the
in-app detail sheet (falls back to the map sheet when the
station is not in cache).

Live Activity Lock Screen card and island station row pick
their Link destination the same way: Maps URL for Open map,
fuelboard://station deep link for More info, handled in
FuelBoardApp and observed by ContentView.

Also adds the small non-interactive map preview above the
Directions button in the More info sheet.
2026-09-16 13:11:36 +01:00
FuelBoard Contributor 1299b4a004 feat: split Tap action into its own Settings section
Moves the Tap station picker out of Units into a dedicated Tap
action section, with the tap descriptor text as its footer. Units
footer reverts to distances/prices only.
2026-09-14 22:11:23 +01:00
FuelBoard Contributor cc948dac3a feat: station tap action toggle (open map vs more info)
Settings → Units gains a Tap station picker: Open map (default,
historic behaviour — tap opens Apple Maps directions) or More info
(tap shows a detail sheet with name, address, distance from current
location, and a Directions button). Applies to Stations and
Favourites tabs; persisted keychain-first as
fuelboard.stationTapAction. Stations footer hint follows the setting.
2026-09-14 22:03:20 +01:00
FuelBoard Contributor ac44cb4ce7 Merge feature/watch-app into main 2026-09-14 21:39:18 +01:00
FuelBoard Contributor abc737eb2d test: symlink DataRefreshMode into test target
FuelStore references DataRefreshMode; without the symlink the
SPM test target fails to compile. 107 tests pass.
2026-09-14 21:39:14 +01:00
FuelBoard Contributor 3d5c999dc2 build: align embedded bundle versions for archive 2026-08-31 11:35:22 +01:00
FuelBoard Contributor d899409b78 build: align all target build numbers to 3 2026-08-31 11:18:05 +01:00
FuelBoard Contributor 8bbcb0b4dc watch: polish refresh status and compact live activity 2026-08-31 10:40:16 +01:00
FuelBoard Contributor 767080a983 watch: use MapKit directions handoff 2026-08-31 08:23:07 +01:00
FuelBoard Contributor 7a84364238 watch: fix snapshot sync and polish favourites UX 2026-08-31 07:48:20 +01:00
FuelBoard Contributor 486b8cff2e Fix watch fuel icons and snapshot sync 2026-08-30 23:35:59 +01:00
FuelBoard Contributor d40bcaeca0 Fix watch icon pixel sizes for archive validation 2026-08-30 23:11:27 +01:00
FuelBoard Contributor f2fb075f2b Fix watch app name and unify watch icon assets 2026-08-30 22:58:05 +01:00
FuelBoard Contributor 09740fca18 Fix watch build, status sync, and icon assets 2026-08-30 22:26:30 +01:00
FuelBoard Contributor ebda21436d Restore smart settings and sync watch favourites 2026-08-30 18:57:57 +01:00
FuelBoard Contributor 0997dcea45 Set watch app team for signing 2026-08-30 13:35:16 +01:00
FuelBoard Contributor a59591d6ca Merge main into feature/watch-app 2026-08-30 13:10:59 +01:00
FuelBoard Contributor e586e5ecbf Merge main into feature/watch-app
Bring watch branch up to date with main (banner fade, onboarding timing,
Live Activity full-width layout). Resolved the bundled fuel dump data-refresh
conflict by taking main's newer snapshot (data_updated 2026-08-17T15:01, 8028
stations).
2026-08-19 18:11:13 +01:00
FuelBoard Contributor e004f25a31 feat: add real watch companion and dual IPA build workflow 2026-08-17 22:41:12 +01:00
131 changed files with 3775 additions and 408 deletions
+2
View File
@@ -1,4 +1,6 @@
build/ build/
Builds/
build-*/
build-release/ build-release/
build-sim/ build-sim/
build-watch/ build-watch/
+100 -2
View File
@@ -1,6 +1,6 @@
# FuelBoard App Store Submission Plan # FuelBoard App Store Submission Plan
Updated: 2026-08-16 · Target: safe App Store submission. Updated: 2026-09-26 · Target: safe App Store submission.
Companion to SECURITY.md (code security) — this file is the *review-gate* Companion to SECURITY.md (code security) — this file is the *review-gate*
checklist: what must happen, who owns it, and the timeline. checklist: what must happen, who owns it, and the timeline.
@@ -15,8 +15,104 @@ The two 2026-08-11 blockers are RESOLVED by P0 (live chain):
Alerts** (ProximityMonitor escalates WhenInUse → Always on alert opt-in). Alerts** (ProximityMonitor escalates WhenInUse → Always on alert opt-in).
Onboarding requests WhenInUse only. Onboarding requests WhenInUse only.
## StoreKit sandbox: verified working configuration and caveats
### Verified configuration
The FuelBoard consumable tip products are:
- `com.apt.fuelboard.tip099`
- `com.apt.fuelboard.tip299`
- `com.apt.fuelboard.tip499`
The app requests these exact **Product IDs** with StoreKit 2
`Product.products(for:)`; the UI names are display labels and are not used for
lookup. The app bundle ID, App Store Connect app, explicit App ID, development
team, and Sandbox Apple Account must belong to the same developer account.
Two test environments must remain distinct:
- **Local StoreKit testing:** select `/Users/apt/workspace/fuelboard/FuelBoardTips.storekit`
in Xcode, under Product > Scheme > Edit Scheme > Run > Options. This bypasses
App Store Connect and confirms the app's StoreKit loading and purchase code.
- **Apple sandbox testing:** select **None** for StoreKit Configuration, then
use Product > Run on a physical device with the development-signed build and
a Sandbox Apple Account signed in under Settings > Developer. This exercises
the real App Store Connect catalog.
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) ## 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 1. **[USER] Apple Developer Program membership** — the app has never been
signed (all builds are `CODE_SIGNING_ALLOWED=NO` + ad-hoc re-sign for 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 SideStore). Requires a paid membership ($99/yr, up to 48 h to approve) and
@@ -31,7 +127,9 @@ The two 2026-08-11 blockers are RESOLVED by P0 (live chain):
provisioning profile (or Xcode automatic signing once the account is in provisioning profile (or Xcode automatic signing once the account is in
Xcode); then `xcodebuild archive` (no more `CODE_SIGNING_ALLOWED=NO`). Xcode); then `xcodebuild archive` (no more `CODE_SIGNING_ALLOWED=NO`).
5. **[USER] Privacy policy URL** in App Store Connect — mandatory; the app 5. **[USER] Privacy policy URL** in App Store Connect — mandatory; the app
collects location. A one-page GitHub Pages privacy policy is fine. 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) ## Required tasks — owner [ME] (technical, do after account exists)
Binary file not shown.

After

Width:  |  Height:  |  Size: 724 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 686 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 303 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 349 KiB

+6 -13
View File
@@ -113,19 +113,12 @@ Status: TODO / IN PROGRESS / DONE / BLOCKED.
action in the Alerts tab that forces a fetch and immediately re-evaluates the action in the Alerts tab that forces a fetch and immediately re-evaluates the
cheapest-favourite alert. Copy should set expectation honestly: standard cheapest-favourite alert. Copy should set expectation honestly: standard
mode checks when FuelBoard refreshes prices (up to twice daily). mode checks when FuelBoard refreshes prices (up to twice daily).
- [ ] **Data checking mode: Standard vs Smart** — keep the current cache-first - [ ] **Price-drop alerts: optional frequent-check mode** — if the app ever
/ 12 h policy as `Standard`, and add an optional `Smart` mode in Settings. relaxes the current "twice daily max" data policy, expose this as a clearly
`Smart` should try lightweight background checks of the mirror pointer separate mode rather than a freeform frequency slider. Example framing:
(`latest.json` / `data_updated`) more often, and only download the full price `Standard` (current behaviour) vs `Frequent` (best-effort extra checks),
dataset when a newer snapshot is available. Design goal: better freshness with conservative caps (e.g. every 4–6 h, not hourly) and copy that does not
without pretending to do real-time refreshes or re-downloading the full dump promise exact timing under iOS background scheduling.
unnecessarily. User-facing copy must be explicit and honest:
- `Standard` — Saves battery. Refreshes price data up to twice daily.
- `Smart` — Checks for newer data more often in the background and refreshes
full prices only when an update is available.
Add a footnote/subcopy that background checks are best-effort and happen only
when iOS allows, so timing is not exact. Prefer this 2-mode setting over a
freeform frequency slider.
- [ ] **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).
- [x] **Offline first-run** — bundled REAL 8,022-station dump (`FuelBoardDump`, - [x] **Offline first-run** — bundled REAL 8,022-station dump (`FuelBoardDump`,
+2 -2
View File
@@ -17,7 +17,7 @@
<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.1</string> <string>$(MARKETING_VERSION)</string>
<key>CFBundleURLTypes</key> <key>CFBundleURLTypes</key>
<array> <array>
<dict> <dict>
@@ -30,7 +30,7 @@
</dict> </dict>
</array> </array>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>2</string> <string>$(CURRENT_PROJECT_VERSION)</string>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <true/>
<key>NSLocalNetworkUsageDescription</key> <key>NSLocalNetworkUsageDescription</key>
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.apt.fuelboard</string>
</array>
</dict>
</plist>
+34
View File
@@ -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>
+33
View File
@@ -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>
+2 -2
View File
@@ -17,9 +17,9 @@
<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.1</string> <string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>2</string> <string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSExtension</key> <key>NSExtension</key>
<dict> <dict>
<key>NSExtensionPointIdentifier</key> <key>NSExtensionPointIdentifier</key>
+637 -35
View File
@@ -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,18 +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; 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;
@@ -313,18 +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; 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;
@@ -337,16 +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; 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;
@@ -362,16 +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; 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;
@@ -381,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;
@@ -411,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>
@@ -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
} }
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 747 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 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" : {
@@ -4,6 +4,14 @@
"filename" : "AppIconPreview.png", "filename" : "AppIconPreview.png",
"idiom" : "universal", "idiom" : "universal",
"scale" : "1x" "scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
} }
], ],
"info" : { "info" : {
@@ -4,6 +4,14 @@
"filename" : "AppIconPreviewDark.png", "filename" : "AppIconPreviewDark.png",
"idiom" : "universal", "idiom" : "universal",
"scale" : "1x" "scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
} }
], ],
"info" : { "info" : {
File diff suppressed because one or more lines are too long
@@ -4,6 +4,14 @@
"filename" : "PreviewWallpaper.jpg", "filename" : "PreviewWallpaper.jpg",
"idiom" : "universal", "idiom" : "universal",
"scale" : "1x" "scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
} }
], ],
"info" : { "info" : {
@@ -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
} }
} }
+14 -10
View File
@@ -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
} }
} }
+14 -10
View File
@@ -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
} }
} }
@@ -1,18 +1,22 @@
{ {
"images": [ "images" : [
{ {
"filename": "brand_circle_k_2x.png", "idiom" : "universal",
"idiom": "universal", "scale" : "1x"
"scale": "2x"
}, },
{ {
"filename": "brand_circle_k_3x.png", "filename" : "brand_circle_k_2x.png",
"idiom": "universal", "idiom" : "universal",
"scale": "3x" "scale" : "2x"
},
{
"filename" : "brand_circle_k_3x.png",
"idiom" : "universal",
"scale" : "3x"
} }
], ],
"info": { "info" : {
"author": "xcode", "author" : "xcode",
"version": 1 "version" : 1
} }
} }
+14 -10
View File
@@ -1,18 +1,22 @@
{ {
"images": [ "images" : [
{ {
"filename": "brand_essar_2x.png", "idiom" : "universal",
"idiom": "universal", "scale" : "1x"
"scale": "2x"
}, },
{ {
"filename": "brand_essar_3x.png", "filename" : "brand_essar_2x.png",
"idiom": "universal", "idiom" : "universal",
"scale": "3x" "scale" : "2x"
},
{
"filename" : "brand_essar_3x.png",
"idiom" : "universal",
"scale" : "3x"
} }
], ],
"info": { "info" : {
"author": "xcode", "author" : "xcode",
"version": 1 "version" : 1
} }
} }
+14 -10
View File
@@ -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
} }
} }
@@ -1,18 +1,22 @@
{ {
"images": [ "images" : [
{ {
"filename": "brand_gleaner_2x.png", "idiom" : "universal",
"idiom": "universal", "scale" : "1x"
"scale": "2x"
}, },
{ {
"filename": "brand_gleaner_3x.png", "filename" : "brand_gleaner_2x.png",
"idiom": "universal", "idiom" : "universal",
"scale": "3x" "scale" : "2x"
},
{
"filename" : "brand_gleaner_3x.png",
"idiom" : "universal",
"scale" : "3x"
} }
], ],
"info": { "info" : {
"author": "xcode", "author" : "xcode",
"version": 1 "version" : 1
} }
} }
+14 -10
View File
@@ -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
} }
} }
@@ -1,18 +1,22 @@
{ {
"images": [ "images" : [
{ {
"filename": "brand_highland_fuels_2x.png", "idiom" : "universal",
"idiom": "universal", "scale" : "1x"
"scale": "2x"
}, },
{ {
"filename": "brand_highland_fuels_3x.png", "filename" : "brand_highland_fuels_2x.png",
"idiom": "universal", "idiom" : "universal",
"scale": "3x" "scale" : "2x"
},
{
"filename" : "brand_highland_fuels_3x.png",
"idiom" : "universal",
"scale" : "3x"
} }
], ],
"info": { "info" : {
"author": "xcode", "author" : "xcode",
"version": 1 "version" : 1
} }
} }
+14 -10
View File
@@ -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
} }
} }
+14 -10
View File
@@ -1,18 +1,22 @@
{ {
"images": [ "images" : [
{ {
"filename": "brand_maxol_2x.png", "idiom" : "universal",
"idiom": "universal", "scale" : "1x"
"scale": "2x"
}, },
{ {
"filename": "brand_maxol_3x.png", "filename" : "brand_maxol_2x.png",
"idiom": "universal", "idiom" : "universal",
"scale": "3x" "scale" : "2x"
},
{
"filename" : "brand_maxol_3x.png",
"idiom" : "universal",
"scale" : "3x"
} }
], ],
"info": { "info" : {
"author": "xcode", "author" : "xcode",
"version": 1 "version" : 1
} }
} }
@@ -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
} }
} }
+14 -10
View File
@@ -1,18 +1,22 @@
{ {
"images": [ "images" : [
{ {
"filename": "brand_murco_2x.png", "idiom" : "universal",
"idiom": "universal", "scale" : "1x"
"scale": "2x"
}, },
{ {
"filename": "brand_murco_3x.png", "filename" : "brand_murco_2x.png",
"idiom": "universal", "idiom" : "universal",
"scale": "3x" "scale" : "2x"
},
{
"filename" : "brand_murco_3x.png",
"idiom" : "universal",
"scale" : "3x"
} }
], ],
"info": { "info" : {
"author": "xcode", "author" : "xcode",
"version": 1 "version" : 1
} }
} }
@@ -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
} }
} }
+14 -10
View File
@@ -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
} }
} }
+14 -10
View File
@@ -1,18 +1,22 @@
{ {
"images": [ "images" : [
{ {
"filename": "brand_spar_2x.png", "idiom" : "universal",
"idiom": "universal", "scale" : "1x"
"scale": "2x"
}, },
{ {
"filename": "brand_spar_3x.png", "filename" : "brand_spar_2x.png",
"idiom": "universal", "idiom" : "universal",
"scale": "3x" "scale" : "2x"
},
{
"filename" : "brand_spar_3x.png",
"idiom" : "universal",
"scale" : "3x"
} }
], ],
"info": { "info" : {
"author": "xcode", "author" : "xcode",
"version": 1 "version" : 1
} }
} }
+14 -10
View File
@@ -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
} }
} }
+14 -10
View File
@@ -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
} }
} }
@@ -1,18 +1,22 @@
{ {
"images": [ "images" : [
{ {
"filename": "brand_the_co_operative_2x.png", "idiom" : "universal",
"idiom": "universal", "scale" : "1x"
"scale": "2x"
}, },
{ {
"filename": "brand_the_co_operative_3x.png", "filename" : "brand_the_co_operative_2x.png",
"idiom": "universal", "idiom" : "universal",
"scale": "3x" "scale" : "2x"
},
{
"filename" : "brand_the_co_operative_3x.png",
"idiom" : "universal",
"scale" : "3x"
} }
], ],
"info": { "info" : {
"author": "xcode", "author" : "xcode",
"version": 1 "version" : 1
} }
} }
+14 -10
View File
@@ -1,18 +1,22 @@
{ {
"images": [ "images" : [
{ {
"filename": "brand_valero_2x.png", "idiom" : "universal",
"idiom": "universal", "scale" : "1x"
"scale": "2x"
}, },
{ {
"filename": "brand_valero_3x.png", "filename" : "brand_valero_2x.png",
"idiom": "universal", "idiom" : "universal",
"scale": "3x" "scale" : "2x"
},
{
"filename" : "brand_valero_3x.png",
"idiom" : "universal",
"scale" : "3x"
} }
], ],
"info": { "info" : {
"author": "xcode", "author" : "xcode",
"version": 1 "version" : 1
} }
} }
@@ -1,18 +1,22 @@
{ {
"images": [ "images" : [
{ {
"filename": "brand_welcome_break_2x.png", "idiom" : "universal",
"idiom": "universal", "scale" : "1x"
"scale": "2x"
}, },
{ {
"filename": "brand_welcome_break_3x.png", "filename" : "brand_welcome_break_2x.png",
"idiom": "universal", "idiom" : "universal",
"scale": "3x" "scale" : "2x"
},
{
"filename" : "brand_welcome_break_3x.png",
"idiom" : "universal",
"scale" : "3x"
} }
], ],
"info": { "info" : {
"author": "xcode", "author" : "xcode",
"version": 1 "version" : 1
} }
} }
+74 -9
View File
@@ -21,6 +21,7 @@ struct ContentView: View {
@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 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()
@@ -234,6 +235,30 @@ struct ContentView: View {
.sheet(item: $monitor.pendingStationMap) { request in .sheet(item: $monitor.pendingStationMap) { request in
StationMapView(request: request) StationMapView(request: request)
} }
.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
)
}
}
) )
} }
@@ -276,6 +301,7 @@ struct ContentView: View {
} }
if !favs.isEmpty { if !favs.isEmpty {
FuelStore.saveFavourites(favs) FuelStore.saveFavourites(favs)
WatchSyncManager.shared.pushSnapshot()
} }
} }
if let i = args.firstIndex(of: "-tab"), i + 1 < args.count { if let i = args.firstIndex(of: "-tab"), i + 1 < args.count {
@@ -366,7 +392,6 @@ struct ContentView: View {
if !showing, FuelStore.loadHasCompletedOnboarding() { if !showing, FuelStore.loadHasCompletedOnboarding() {
locationManager.startForegroundTracking() locationManager.startForegroundTracking()
installLocationUpdateHook() installLocationUpdateHook()
SmartDataRefreshScheduler.scheduleNextIfNeeded()
ensureBackgroundMotionTrackingIfNeeded() ensureBackgroundMotionTrackingIfNeeded()
monitor.update(stations: stations, favourites: refreshedFavourites, monitor.update(stations: stations, favourites: refreshedFavourites,
fuel: alertsFuel, radiusKM: effectiveAlertsRadiusKM) fuel: alertsFuel, radiusKM: effectiveAlertsRadiusKM)
@@ -382,12 +407,14 @@ struct ContentView: View {
if !showOnboarding, FuelStore.loadHasCompletedOnboarding() { if !showOnboarding, FuelStore.loadHasCompletedOnboarding() {
locationManager.startForegroundTracking() locationManager.startForegroundTracking()
} }
SmartDataRefreshScheduler.scheduleNextIfNeeded()
monitor.update(stations: stations, favourites: refreshedFavourites, monitor.update(stations: stations, favourites: refreshedFavourites,
fuel: alertsFuel, radiusKM: effectiveAlertsRadiusKM) fuel: alertsFuel, radiusKM: effectiveAlertsRadiusKM)
updateLiveActivity() updateLiveActivity()
refreshRoadDistancesIfNeeded() refreshRoadDistancesIfNeeded()
// No network fetch on foreground — pull-to-refresh is the override. if FuelStore.hasPendingWatchRefreshRequest {
Task { await handlePendingWatchRefreshRequest() }
}
// No network fetch on foreground unless the watch explicitly requests one.
} else { } else {
locationManager.stopForegroundTracking() locationManager.stopForegroundTracking()
} }
@@ -396,6 +423,7 @@ 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).
@@ -414,6 +442,7 @@ struct ContentView: View {
.onChange(of: selectedFuel) { _, _ in .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
@@ -738,6 +767,7 @@ struct ContentView: View {
topStationID: topStationID, topStationID: topStationID,
location: location, location: location,
favouriteIDs: favouriteIDs, favouriteIDs: favouriteIDs,
stationTapAction: stationTapAction,
onToggleFavourite: toggleFavourite, onToggleFavourite: toggleFavourite,
onRefresh: { await refresh(force: true) } onRefresh: { await refresh(force: true) }
) )
@@ -752,6 +782,7 @@ struct ContentView: View {
location: location, location: location,
distanceUnit: distanceUnit, distanceUnit: distanceUnit,
priceDisplayStyle: priceDisplayStyle, priceDisplayStyle: priceDisplayStyle,
stationTapAction: stationTapAction,
onToggleFavourite: toggleFavourite, onToggleFavourite: toggleFavourite,
onReorder: reorderFavourites, onReorder: reorderFavourites,
onHistoryUnavailable: { if dataStatus == .live { dataStatus = .connectionProblem } }, onHistoryUnavailable: { if dataStatus == .live { dataStatus = .connectionProblem } },
@@ -790,6 +821,7 @@ struct ContentView: View {
tipStore: tipStore, tipStore: tipStore,
distanceUnit: $distanceUnit, distanceUnit: $distanceUnit,
priceDisplayStyle: $priceDisplayStyle, priceDisplayStyle: $priceDisplayStyle,
stationTapAction: $stationTapAction,
dataRefreshMode: $dataRefreshMode, dataRefreshMode: $dataRefreshMode,
alertsFuel: alertsFuel, alertsFuel: alertsFuel,
alertsRadiusKM: alertsRadius, alertsRadiusKM: alertsRadius,
@@ -819,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)
@@ -830,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 {
@@ -853,7 +892,10 @@ struct ContentView: View {
stations = fetched stations = fetched
FuelStore.saveStations(fetched) FuelStore.saveStations(fetched)
FuelStore.saveLastRefresh() FuelStore.saveLastRefresh()
FuelStore.saveLastSmartProbe() if force, FuelStore.hasPendingWatchRefreshRequest {
FuelStore.markWatchRefreshHandled()
}
WatchSyncManager.shared.pushSnapshot()
// Persist envelope metadata (source, station count, GOV.UK // Persist envelope metadata (source, station count, GOV.UK
// dataset update time) for the Settings → About section — the // dataset update time) for the Settings → About section — the
// live chain records whichever leg served the fetch. // live chain records whichever leg served the fetch.
@@ -876,6 +918,9 @@ struct ContentView: View {
// Live data restored — any status banner no longer applies. // Live data restored — any status banner no longer applies.
dataStatus = .live dataStatus = .live
} catch { } catch {
if force, FuelStore.hasPendingWatchRefreshRequest {
FuelStore.markWatchRefreshHandled()
}
statusMessage = "Live fetch failed: \(error.localizedDescription). Showing cached data." statusMessage = "Live fetch failed: \(error.localizedDescription). Showing cached data."
if FuelStore.loadStations().isEmpty { if FuelStore.loadStations().isEmpty {
// No cached prices — last resort is the bundled REAL dump // No cached prices — last resort is the bundled REAL dump
@@ -914,7 +959,9 @@ struct StationRow: View {
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 }
@@ -934,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)
@@ -970,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)
@@ -1012,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)
}
}
} }
} }
+16 -1
View File
@@ -14,6 +14,7 @@ struct FavouritesView: View {
let location: Coordinate? let location: Coordinate?
let distanceUnit: DistanceUnit let distanceUnit: DistanceUnit
let priceDisplayStyle: PriceDisplayStyle let priceDisplayStyle: PriceDisplayStyle
let stationTapAction: StationTapAction
var onToggleFavourite: (FuelStation, FuelType) -> Void = { _, _ in } var onToggleFavourite: (FuelStation, FuelType) -> Void = { _, _ in }
/// Persists a reordered favourites array (after drag-and-drop). /// Persists a reordered favourites array (after drag-and-drop).
var onReorder: ([FavouriteEntry]) -> Void = { _ in } var onReorder: ([FavouriteEntry]) -> Void = { _ in }
@@ -37,6 +38,9 @@ struct FavouritesView: View {
/// Trends sheet (price history chart) presentation state. /// Trends sheet (price history chart) presentation state.
@State private var showTrends = false @State private var showTrends = false
/// Station picked for the More-info sheet (tap action = showDetails).
@State private var detailStation: FuelStation?
private var activeFuel: FuelType { private var activeFuel: FuelType {
availableFuels.contains(fuel) ? fuel : (availableFuels.first ?? .e10) availableFuels.contains(fuel) ? fuel : (availableFuels.first ?? .e10)
} }
@@ -65,6 +69,7 @@ struct FavouritesView: View {
location: Coordinate?, location: Coordinate?,
distanceUnit: DistanceUnit, distanceUnit: DistanceUnit,
priceDisplayStyle: PriceDisplayStyle, priceDisplayStyle: PriceDisplayStyle,
stationTapAction: StationTapAction,
onToggleFavourite: @escaping (FuelStation, FuelType) -> Void = { _, _ in }, onToggleFavourite: @escaping (FuelStation, FuelType) -> Void = { _, _ in },
onReorder: @escaping ([FavouriteEntry]) -> Void = { _ in }, onReorder: @escaping ([FavouriteEntry]) -> Void = { _ in },
onHistoryUnavailable: (() -> Void)? = nil, onHistoryUnavailable: (() -> Void)? = nil,
@@ -74,6 +79,7 @@ struct FavouritesView: View {
self.location = location self.location = location
self.distanceUnit = distanceUnit self.distanceUnit = distanceUnit
self.priceDisplayStyle = priceDisplayStyle self.priceDisplayStyle = priceDisplayStyle
self.stationTapAction = stationTapAction
self.onToggleFavourite = onToggleFavourite self.onToggleFavourite = onToggleFavourite
self.onReorder = onReorder self.onReorder = onReorder
self.onHistoryUnavailable = onHistoryUnavailable self.onHistoryUnavailable = onHistoryUnavailable
@@ -129,7 +135,9 @@ struct FavouritesView: View {
baselinePrice: cheapestPrice, baselinePrice: cheapestPrice,
isTopResult: index == 0, isTopResult: index == 0,
isFavourite: activeFuelFavouriteIDs.contains(station.id), isFavourite: activeFuelFavouriteIDs.contains(station.id),
onToggleFavourite: { onToggleFavourite(station, activeFuel) } tapAction: stationTapAction,
onToggleFavourite: { onToggleFavourite(station, activeFuel) },
onShowDetails: { detailStation = $0 }
) )
} }
.onMove(perform: moveFavourite) .onMove(perform: moveFavourite)
@@ -171,6 +179,13 @@ struct FavouritesView: View {
onHistoryRecovered: onHistoryRecovered onHistoryRecovered: onHistoryRecovered
) )
} }
.sheet(item: $detailStation) { station in
StationDetailView(
station: station,
location: location,
distanceUnit: distanceUnit
)
}
.onAppear { .onAppear {
#if DEBUG #if DEBUG
// QA hook: launch with `-showTrends` to open the sheet // QA hook: launch with `-showTrends` to open the sheet
+37 -2
View File
@@ -5,6 +5,7 @@ import SwiftUI
@main @main
struct FuelBoardApp: App { struct FuelBoardApp: App {
init() { init() {
WatchSyncManager.shared.activate()
#if DEBUG #if DEBUG
// QA hook (Debug builds only): `-qaLiveActivity e10|e5|diesel` starts a // QA hook (Debug builds only): `-qaLiveActivity e10|e5|diesel` starts a
// Live Activity with a long station name so the Lock Screen / island // Live Activity with a long station name so the Lock Screen / island
@@ -22,6 +23,9 @@ struct FuelBoardApp: App {
WindowGroup { WindowGroup {
ContentView() ContentView()
.onOpenURL(perform: handleOpenURL) .onOpenURL(perform: handleOpenURL)
.onReceive(NotificationCenter.default.publisher(for: .fuelBoardWatchRefreshRequested)) { _ in
WatchSyncManager.shared.pushSnapshot()
}
} }
.backgroundTask(.appRefresh(SmartDataRefreshScheduler.taskIdentifier)) { .backgroundTask(.appRefresh(SmartDataRefreshScheduler.taskIdentifier)) {
await SmartDataRefreshCoordinator.runBackgroundProbe() await SmartDataRefreshCoordinator.runBackgroundProbe()
@@ -65,9 +69,40 @@ struct FuelBoardApp: App {
/// `http://maps.apple.com` form is also handled, from very old timelines. /// `http://maps.apple.com` form is also handled, from very old timelines.
/// In CarPlay the widget uses the same `maps://` URL, which routes to /// In CarPlay the widget uses the same `maps://` URL, which routes to
/// Apple Maps without ever launching this app. /// Apple Maps without ever launching this app.
/// A `fuelboard://station?stationID=…&lat=…&lng=…&name=…` link (from a
/// widget or Live Activity tap when Settings → Tap station = More info)
/// posts a notification that ContentView observes to present the detail
/// sheet (falling back to the map sheet when the station has left
/// the cache).
private func handleOpenURL(_ url: URL) { private func handleOpenURL(_ url: URL) {
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false), guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
let items = components.queryItems else { return }
if components.scheme == "fuelboard",
components.host == "station" {
let items = components.queryItems ?? []
var userInfo: [String: Any] = [:]
if let id = items.first(where: { $0.name == "stationID" })?.value {
userInfo["stationID"] = id
}
if let lat = items.first(where: { $0.name == "lat" })?.value.flatMap(Double.init) {
userInfo["stationLat"] = lat
}
if let lng = items.first(where: { $0.name == "lng" })?.value.flatMap(Double.init) {
userInfo["stationLng"] = lng
}
if let name = items.first(where: { $0.name == "name" })?.value {
userInfo["stationName"] = name
}
NotificationCenter.default.post(
name: .fuelBoardShowStationDetail,
object: nil,
userInfo: userInfo
)
return
}
guard let items = components.queryItems
else { return } else { return }
// Preferred: maps://?daddr=lat,lng&t=d (also covers http://maps.apple.com). // Preferred: maps://?daddr=lat,lng&t=d (also covers http://maps.apple.com).
+1 -3
View File
@@ -73,9 +73,7 @@ enum LiveActivityManager {
currentStartDate = nil currentStartDate = nil
start(attributes: attributes, state: state) start(attributes: attributes, state: state)
} else { } else {
Task { Task { await current.update(using: state) }
await current.update(.init(state: state, staleDate: nil))
}
} }
} else { } else {
start(attributes: attributes, state: state) start(attributes: attributes, state: state)
+29 -5
View File
@@ -84,6 +84,9 @@ final class ProximityMonitor: NSObject, ObservableObject, @preconcurrency CLLoca
@Published var lastAlert: String? @Published var lastAlert: String?
@Published private(set) var lastTestResult: String? @Published private(set) var lastTestResult: String?
@Published var pendingStationMap: StationMapRequest? @Published var pendingStationMap: StationMapRequest?
/// Station tapped via notification when Settings → Tap station = More info.
/// ContentView presents this as a StationDetailView sheet.
@Published var pendingDetailStation: FuelStation?
/// Debug-only snapshot for the Settings → Debug section. Recomputed on /// Debug-only snapshot for the Settings → Debug section. Recomputed on
/// every location/stations change via `refreshDebugStatus()`. /// every location/stations change via `refreshDebugStatus()`.
@Published private(set) var debugStatus: DebugLocationStatus? @Published private(set) var debugStatus: DebugLocationStatus?
@@ -791,9 +794,11 @@ final class ProximityMonitor: NSObject, ObservableObject, @preconcurrency CLLoca
} }
} }
/// Tapping a notification opens Apple Maps directions to the station from /// Tapping a notification honours Settings → Tap station: Open map opens
/// the user's current location. If Apple Maps can't be opened (e.g. inside /// Apple Maps directions to the station from the user's current location;
/// LiveContainer), an in-app map sheet with directions is shown instead. /// More info opens the station's detail sheet in the app instead. If Apple
/// Maps can't be opened (e.g. inside LiveContainer), an in-app map sheet
/// with directions is shown instead.
/// ///
/// When the notification carried tie-choice action buttons, the tapped /// When the notification carried tie-choice action buttons, the tapped
/// button's identifier ("tie_0", "tie_1", …) selects the corresponding /// button's identifier ("tie_0", "tie_1", …) selects the corresponding
@@ -818,9 +823,9 @@ final class ProximityMonitor: NSObject, ObservableObject, @preconcurrency CLLoca
let tieName = ties[index]["name"] as? String, let tieName = ties[index]["name"] as? String,
let tieLat = ties[index]["lat"] as? Double, let tieLat = ties[index]["lat"] as? Double,
let tieLng = ties[index]["lng"] as? Double { let tieLng = ties[index]["lng"] as? Double {
openDirections(to: tieName, latitude: tieLat, longitude: tieLng) routeNotificationTap(name: tieName, latitude: tieLat, longitude: tieLng)
} else if !isTie, let name, let lat, let lng { } else if !isTie, let name, let lat, let lng {
openDirections(to: name, latitude: lat, longitude: lng) routeNotificationTap(name: name, latitude: lat, longitude: lng)
} }
// Tie body tap: intentionally no directions — the user must pick // Tie body tap: intentionally no directions — the user must pick
// an option button to choose a station. // an option button to choose a station.
@@ -828,6 +833,25 @@ final class ProximityMonitor: NSObject, ObservableObject, @preconcurrency CLLoca
} }
} }
/// Routes a resolved notification tap through the user's Tap station
/// preference: Open map → Apple Maps; More info → in-app detail sheet
/// (falling back to the map sheet when the station isn't in cache).
@MainActor
private func routeNotificationTap(name: String, latitude: Double, longitude: Double) {
guard FuelStore.loadStationTapAction() == .showDetails else {
openDirections(to: name, latitude: latitude, longitude: longitude)
return
}
if let station = stations.first(where: { $0.lat == latitude && $0.lng == longitude })
?? stations.first(where: { $0.name == name }) {
pendingDetailStation = station
} else {
// Station not in cache (e.g. data refreshed since the alert fired)
// — fall back to the map sheet so the tap still shows something.
pendingStationMap = StationMapRequest(name: name, latitude: latitude, longitude: longitude)
}
}
/// Opens Apple Maps with driving directions to the station; falls back to /// Opens Apple Maps with driving directions to the station; falls back to
/// the in-app map sheet when the hand-off fails. /// the in-app map sheet when the hand-off fails.
@MainActor @MainActor
+2 -6
View File
@@ -73,12 +73,8 @@ enum RoadDistanceService {
/// Driving distance (metres) between two coordinates via Apple Maps routing. /// Driving distance (metres) between two coordinates via Apple Maps routing.
private static func roadMeters(from: CLLocationCoordinate2D, to: CLLocationCoordinate2D) async -> Double? { private static func roadMeters(from: CLLocationCoordinate2D, to: CLLocationCoordinate2D) async -> Double? {
let request = MKDirections.Request() let request = MKDirections.Request()
request.source = MKMapItem(location: CLLocation(latitude: from.latitude, request.source = MKMapItem(placemark: MKPlacemark(coordinate: from))
longitude: from.longitude), request.destination = MKMapItem(placemark: MKPlacemark(coordinate: to))
address: nil)
request.destination = MKMapItem(location: CLLocation(latitude: to.latitude,
longitude: to.longitude),
address: nil)
request.transportType = .automobile request.transportType = .automobile
request.requestsAlternateRoutes = false request.requestsAlternateRoutes = false
do { do {
+197 -17
View File
@@ -18,6 +18,7 @@ struct SettingsView: View {
@ObservedObject var tipStore: TipStore @ObservedObject var tipStore: TipStore
@Binding var distanceUnit: DistanceUnit @Binding var distanceUnit: DistanceUnit
@Binding var priceDisplayStyle: PriceDisplayStyle @Binding var priceDisplayStyle: PriceDisplayStyle
@Binding var stationTapAction: StationTapAction
@Binding var dataRefreshMode: DataRefreshMode @Binding var dataRefreshMode: DataRefreshMode
/// The fuel + radius currently configured for alerts (mirrors the Alerts /// The fuel + radius currently configured for alerts (mirrors the Alerts
/// tab) so the test notification matches what real alerts will say. /// tab) so the test notification matches what real alerts will say.
@@ -104,6 +105,7 @@ struct SettingsView: View {
.onChange(of: distanceUnit) { _, newValue in .onChange(of: distanceUnit) { _, newValue in
FuelStore.saveDistanceUnit(newValue) FuelStore.saveDistanceUnit(newValue)
WidgetCenter.shared.reloadAllTimelines() WidgetCenter.shared.reloadAllTimelines()
WatchSyncManager.shared.pushSnapshot()
} }
Picker("Price display", selection: $priceDisplayStyle) { Picker("Price display", selection: $priceDisplayStyle) {
ForEach(PriceDisplayStyle.allCases) { style in ForEach(PriceDisplayStyle.allCases) { style in
@@ -114,6 +116,7 @@ struct SettingsView: View {
.onChange(of: priceDisplayStyle) { _, newValue in .onChange(of: priceDisplayStyle) { _, newValue in
FuelStore.savePriceDisplayStyle(newValue) FuelStore.savePriceDisplayStyle(newValue)
WidgetCenter.shared.reloadAllTimelines() WidgetCenter.shared.reloadAllTimelines()
WatchSyncManager.shared.pushSnapshot()
} }
} header: { } header: {
Text("Units") Text("Units")
@@ -121,6 +124,22 @@ struct SettingsView: View {
Text("Distances and search radii across the app, widget and alerts are shown in this unit. Prices can be shown as on a station sign (129.9) or in pounds and pence (£1.29⁹/L).") Text("Distances and search radii across the app, widget and alerts are shown in this unit. Prices can be shown as on a station sign (129.9) or in pounds and pence (£1.29⁹/L).")
} }
Section {
Picker("Tap station", selection: $stationTapAction) {
ForEach(StationTapAction.allCases) { action in
Text(action.displayName).tag(action)
}
}
.pickerStyle(.segmented)
.onChange(of: stationTapAction) { _, newValue in
FuelStore.saveStationTapAction(newValue)
}
} header: {
Text("Tap action")
} footer: {
Text("Tapping a station either opens directions straight away or shows its details first.")
}
Section { Section {
Picker("Data checking", selection: $dataRefreshMode) { Picker("Data checking", selection: $dataRefreshMode) {
ForEach(DataRefreshMode.allCases) { mode in ForEach(DataRefreshMode.allCases) { mode in
@@ -131,6 +150,7 @@ struct SettingsView: View {
.onChange(of: dataRefreshMode) { _, newValue in .onChange(of: dataRefreshMode) { _, newValue in
FuelStore.saveDataRefreshMode(newValue) FuelStore.saveDataRefreshMode(newValue)
SmartDataRefreshScheduler.scheduleNextIfNeeded() SmartDataRefreshScheduler.scheduleNextIfNeeded()
WatchSyncManager.shared.pushSnapshot()
} }
VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading, spacing: 8) {
Text(dataRefreshMode.summary) Text(dataRefreshMode.summary)
@@ -229,6 +249,55 @@ struct SettingsView: View {
} }
.onAppear { refreshWidgetDiag() } .onAppear { refreshWidgetDiag() }
Section {
Button {
Task { await tipStore.load() }
} label: {
Label("Probe App Store products", systemImage: "arrow.clockwise")
}
LabeledContent("Probe", value: tipStore.productLoadStatus)
if let loadedAt = tipStore.lastProductLoadDate {
LabeledContent("Last probe", value: loadedAt.formatted(date: .abbreviated, time: .standard))
}
LabeledContent("Requested", value: "\(TipStore.tiers.count) IDs")
LabeledContent("Returned", value: "\(tipStore.debugProducts.count) products")
if !tipStore.debugProducts.isEmpty {
ForEach(tipStore.debugProducts) { product in
VStack(alignment: .leading, spacing: 3) {
Text(product.id)
.font(.caption.monospaced())
.textSelection(.enabled)
Text(product.displayName)
.font(.footnote.weight(.medium))
Text("\(product.type) · \(product.displayPrice) · raw \(product.price)")
.font(.caption2)
.foregroundStyle(.secondary)
.textSelection(.enabled)
}
}
} else {
Text("No products were returned by StoreKit.")
.font(.caption)
.foregroundStyle(.secondary)
}
if !tipStore.missingProductIDs.isEmpty {
Text("Missing IDs: \(tipStore.missingProductIDs.joined(separator: ", "))")
.font(.caption2)
.foregroundStyle(.orange)
.textSelection(.enabled)
}
if let error = tipStore.lastProductLoadError {
Text("Error: \(error)")
.font(.caption2)
.foregroundStyle(.red)
.textSelection(.enabled)
}
} header: {
Text("IAP Diagnostics")
} footer: {
Text("Shows the exact product identifiers requested and the StoreKit products returned. Product names and prices are read from StoreKit; missing IDs were not returned by the current App Store environment.")
}
Section { Section {
HStack { HStack {
Text("Connection") Text("Connection")
@@ -611,19 +680,18 @@ final class TipStore: ObservableObject {
let id: String // product ID let id: String // product ID
let name: String let name: String
let blurb: String let blurb: String
let fallbackPrice: String
let accent: Color // card/price accent (matches the fuel palette) let accent: Color // card/price accent (matches the fuel palette)
} }
static let tiers: [TipTier] = [ static let tiers: [TipTier] = [
TipTier(id: "com.apt.fuelboard.tip099", name: "Splash & Dash", TipTier(id: "com.apt.fuelboard.tip099", name: "Splash & Dash",
blurb: "Just enough to keep things moving.", fallbackPrice: "£0.99", blurb: "Just enough to keep things moving.",
accent: Color(red: 0.39, green: 0.82, blue: 1.0)), // #64D2FF accent: Color(red: 0.39, green: 0.82, blue: 1.0)), // #64D2FF
TipTier(id: "com.apt.fuelboard.tip299", name: "Half a Tank", TipTier(id: "com.apt.fuelboard.tip299", name: "Half a Tank",
blurb: "A generous top-up for development.", fallbackPrice: "£2.99", blurb: "A generous top-up for development.",
accent: Color(red: 0.19, green: 0.82, blue: 0.35)), // #30D158 accent: Color(red: 0.19, green: 0.82, blue: 0.35)), // #30D158
TipTier(id: "com.apt.fuelboard.tip499", name: "Fill 'Er Up", TipTier(id: "com.apt.fuelboard.tip499", name: "Fill 'Er Up",
blurb: "Keeping the app on the road.", fallbackPrice: "£4.99", blurb: "Keeping the app on the road.",
accent: Color(red: 1.0, green: 0.84, blue: 0.04)), // #FFD60A accent: Color(red: 1.0, green: 0.84, blue: 0.04)), // #FFD60A
] ]
@@ -637,6 +705,22 @@ final class TipStore: ObservableObject {
@Published private(set) var products: [String: Product] = [:] @Published private(set) var products: [String: Product] = [:]
@Published private(set) var purchaseInProgress = false @Published private(set) var purchaseInProgress = false
@Published private(set) var outcome: TipOutcome? @Published private(set) var outcome: TipOutcome?
@Published private(set) var productLoadStatus = "Not probed"
@Published private(set) var lastProductLoadDate: Date?
@Published private(set) var lastProductLoadError: String?
@Published private(set) var debugProducts: [DebugProduct] = []
struct DebugProduct: Identifiable {
let id: String
let displayName: String
let type: String
let displayPrice: String
let price: String
}
var missingProductIDs: [String] {
Self.tiers.map(\.id).filter { products[$0] == nil }
}
/// Auto-dismiss timer for the outcome banner (replaced on each new outcome). /// Auto-dismiss timer for the outcome banner (replaced on each new outcome).
private var dismissTask: Task<Void, Never>? private var dismissTask: Task<Void, Never>?
@@ -647,9 +731,31 @@ final class TipStore: ObservableObject {
/// the queue but are never finished or acknowledged. /// the queue but are never finished or acknowledged.
private var updatesTask: Task<Void, Never>? private var updatesTask: Task<Void, Never>?
/// Drains verified FuelBoard transactions that were already unfinished
/// before this store was created (for example after termination during a
/// sandbox purchase). `Transaction.updates` only covers updates delivered
/// while the listener is active, so both sequences are required.
private var unfinishedTask: Task<Void, Never>?
/// Uses the existing hidden developer switch in Settings. StoreKit
/// diagnostics must stay silent unless the developer explicitly enables
/// Debug mode with the five-tap Version gesture.
private static func debugLog(_ message: @autoclosure () -> String) {
guard FuelStore.loadDebugMode() else { return }
print(message())
}
init() { init() {
Self.debugLog("[FuelBoard StoreKit] TipStore initialized; expected IDs=\(Self.tiers.map(\.id).joined(separator: ","))")
updatesTask = Task { [weak self] in updatesTask = Task { [weak self] in
for await update in Transaction.updates { for await update in Transaction.updates {
Self.debugLog("[FuelBoard StoreKit] Transaction.updates received")
await self?.handle(update)
}
}
unfinishedTask = Task { [weak self] in
for await update in Transaction.unfinished {
Self.debugLog("[FuelBoard StoreKit] Transaction.unfinished received")
await self?.handle(update) await self?.handle(update)
} }
} }
@@ -657,14 +763,27 @@ final class TipStore: ObservableObject {
deinit { deinit {
updatesTask?.cancel() updatesTask?.cancel()
unfinishedTask?.cancel()
dismissTask?.cancel() dismissTask?.cancel()
} }
private func handle(_ update: VerificationResult<StoreKit.Transaction>) async { private func handle(_ update: VerificationResult<StoreKit.Transaction>) async {
Self.debugLog("[FuelBoard StoreKit] Handling transaction update")
// Never deliver or finish an untrusted purchase. // Never deliver or finish an untrusted purchase.
guard case .verified(let transaction) = update else { return } guard case .verified(let transaction) = update else {
if case .unverified(_, let error) = update {
Self.debugLog("[FuelBoard StoreKit] Ignoring unverified transaction error=\(error.localizedDescription)")
} else {
Self.debugLog("[FuelBoard StoreKit] Ignoring unverified transaction update")
}
return
}
// Only acknowledge our own consumables (future products get their own). // Only acknowledge our own consumables (future products get their own).
guard Self.tiers.contains(where: { $0.id == transaction.productID }) else { return } guard Self.tiers.contains(where: { $0.id == transaction.productID }) else {
Self.debugLog("[FuelBoard StoreKit] Ignoring unrelated product ID=\(transaction.productID)")
return
}
Self.debugLog("[FuelBoard StoreKit] Finishing verified transaction productID=\(transaction.productID)")
await transaction.finish() await transaction.finish()
showOutcome( showOutcome(
NSLocalizedString("Thank you! Your tip has been received. ⛽", comment: ""), NSLocalizedString("Thank you! Your tip has been received. ⛽", comment: ""),
@@ -693,45 +812,101 @@ final class TipStore: ObservableObject {
} }
func displayPrice(for tier: TipTier) -> String { func displayPrice(for tier: TipTier) -> String {
products[tier.id]?.displayPrice ?? tier.fallbackPrice products[tier.id]?.displayPrice
?? NSLocalizedString("Unavailable", comment: "Tip product has not been returned by StoreKit")
} }
func load() async { func load() async {
// Refreshes product state on every visit so newly-approved products // Refreshes product state on every visit so newly-approved products
// (or restored transactions) are picked up. // (or restored transactions) are picked up.
let ids = Self.tiers.map(\.id)
productLoadStatus = "Loading…"
lastProductLoadError = nil
Self.debugLog("[FuelBoard StoreKit] Loading products IDs=\(ids.joined(separator: ","))")
do { do {
let fetched = try await Product.products(for: Self.tiers.map(\.id)) let fetched = try await Product.products(for: ids)
lastProductLoadDate = Date()
Self.debugLog("[FuelBoard StoreKit] Product.products returned count=\(fetched.count) IDs=\(fetched.map(\.id).joined(separator: ","))")
for product in fetched {
Self.debugLog("[FuelBoard StoreKit] Product id=\(product.id) type=\(String(describing: product.type)) displayPrice=\(product.displayPrice) price=\(product.price)")
}
products = Dictionary(uniqueKeysWithValues: fetched.map { ($0.id, $0) }) products = Dictionary(uniqueKeysWithValues: fetched.map { ($0.id, $0) })
debugProducts = fetched.map {
DebugProduct(
id: $0.id,
displayName: $0.displayName,
type: String(describing: $0.type),
displayPrice: $0.displayPrice,
price: String(describing: $0.price)
)
}
productLoadStatus = fetched.isEmpty ? "Completed — zero returned" : "Completed"
let matched = Self.tiers.filter { products[$0.id] != nil }.map(\.id)
Self.debugLog("[FuelBoard StoreKit] Matched products after load=\(matched.joined(separator: ","))")
} catch { } catch {
// No products yet (sideloaded build) — the buttons still show the lastProductLoadDate = Date()
// intended prices and report purchase attempts gracefully. productLoadStatus = "Failed"
lastProductLoadError = "\(String(describing: type(of: error))): \(error.localizedDescription)"
debugProducts = []
Self.debugLog("[FuelBoard StoreKit] Product load failed type=\(String(describing: type(of: error))) error=\(error.localizedDescription)")
// Keep the cards visible so the user can retry while App Store
// Connect metadata propagates; displayPrice() shows "Unavailable"
// until StoreKit returns a real product.
products = [:] products = [:]
} }
} }
private func merge(_ fetched: [Product]) {
products.merge(Dictionary(uniqueKeysWithValues: fetched.map { ($0.id, $0) })) { _, new in new }
}
private func product(for tier: TipTier) async throws -> Product? {
if let product = products[tier.id] {
Self.debugLog("[FuelBoard StoreKit] Using cached product ID=\(tier.id)")
return product
}
// TestFlight/App Store Connect product availability can lag the Settings
// onAppear load. Re-query the tapped tier before falling back so a newly
// approved IAP opens the system purchase sheet instead of showing stale
// "not available" copy.
Self.debugLog("[FuelBoard StoreKit] Re-querying tapped product ID=\(tier.id)")
let fetched = try await Product.products(for: [tier.id])
Self.debugLog("[FuelBoard StoreKit] Re-query returned count=\(fetched.count) IDs=\(fetched.map(\.id).joined(separator: ","))")
merge(fetched)
let match = fetched.first { $0.id == tier.id }
Self.debugLog("[FuelBoard StoreKit] Tapped product match=\(match != nil) ID=\(tier.id)")
return match
}
func purchase(_ tier: TipTier) async { func purchase(_ tier: TipTier) async {
guard !purchaseInProgress else { return } Self.debugLog("[FuelBoard StoreKit] Purchase requested ID=\(tier.id) name=\(tier.name)")
guard !purchaseInProgress else {
Self.debugLog("[FuelBoard StoreKit] Purchase ignored because another purchase is in progress")
return
}
purchaseInProgress = true purchaseInProgress = true
defer { purchaseInProgress = false } defer { purchaseInProgress = false }
// If the product hasn't loaded (e.g. not configured in App Store do {
// Connect yet), still allow the attempt so the user sees a clear guard let product = try await product(for: tier) else {
// outcome rather than a dead button. Self.debugLog("[FuelBoard StoreKit] Purchase stopped: no StoreKit product for ID=\(tier.id)")
guard let product = products[tier.id] else {
showOutcome( showOutcome(
NSLocalizedString("The tip isn't available in this build yet — check back after an App Store release.", comment: ""), NSLocalizedString("This tip isn't available from the App Store yet. Please try again shortly.", comment: ""),
icon: "info.circle.fill", icon: "info.circle.fill",
tint: Color(.secondaryLabel) tint: Color(.secondaryLabel)
) )
return return
} }
do { Self.debugLog("[FuelBoard StoreKit] Calling product.purchase() ID=\(product.id) displayPrice=\(product.displayPrice)")
let result = try await product.purchase() let result = try await product.purchase()
switch result { switch result {
case .success(let verification): case .success(let verification):
Self.debugLog("[FuelBoard StoreKit] Purchase result=success")
switch verification { switch verification {
case .verified(let transaction): case .verified(let transaction):
Self.debugLog("[FuelBoard StoreKit] Purchase verified productID=\(transaction.productID); finishing")
// Consume the consumable — otherwise StoreKit re-delivers // Consume the consumable — otherwise StoreKit re-delivers
// it through Transaction.updates on every launch. // it through Transaction.updates on every launch.
await transaction.finish() await transaction.finish()
@@ -741,6 +916,7 @@ final class TipStore: ObservableObject {
tint: Color(red: 0.19, green: 0.82, blue: 0.35) // #30D158 tint: Color(red: 0.19, green: 0.82, blue: 0.35) // #30D158
) )
case .unverified: case .unverified:
Self.debugLog("[FuelBoard StoreKit] Purchase result=unverified")
showOutcome( showOutcome(
NSLocalizedString("The purchase couldn't be verified. Please try again.", comment: ""), NSLocalizedString("The purchase couldn't be verified. Please try again.", comment: ""),
icon: "exclamationmark.triangle.fill", icon: "exclamationmark.triangle.fill",
@@ -748,17 +924,21 @@ final class TipStore: ObservableObject {
) )
} }
case .userCancelled: case .userCancelled:
Self.debugLog("[FuelBoard StoreKit] Purchase result=userCancelled")
dismissOutcome() // silent — the user just closed the sheet dismissOutcome() // silent — the user just closed the sheet
case .pending: case .pending:
Self.debugLog("[FuelBoard StoreKit] Purchase result=pending")
showOutcome( showOutcome(
NSLocalizedString("Your tip is pending approval. It'll finish automatically.", comment: ""), NSLocalizedString("Your tip is pending approval. It'll finish automatically.", comment: ""),
icon: "hourglass", icon: "hourglass",
tint: Color(red: 0.39, green: 0.82, blue: 1.0) // #64D2FF tint: Color(red: 0.39, green: 0.82, blue: 1.0) // #64D2FF
) )
@unknown default: @unknown default:
Self.debugLog("[FuelBoard StoreKit] Purchase result=unknown")
dismissOutcome() dismissOutcome()
} }
} catch { } catch {
Self.debugLog("[FuelBoard StoreKit] Purchase threw type=\(String(describing: type(of: error))) error=\(error.localizedDescription)")
showOutcome( showOutcome(
String(format: NSLocalizedString("The tip couldn't be completed: %@", comment: ""), error.localizedDescription), String(format: NSLocalizedString("The tip couldn't be completed: %@", comment: ""), error.localizedDescription),
icon: "xmark.circle.fill", icon: "xmark.circle.fill",
+1
View File
@@ -76,6 +76,7 @@ enum SmartDataRefreshCoordinator {
guard !Task.isCancelled else { return false } guard !Task.isCancelled else { return false }
FuelStore.saveStations(fetched) FuelStore.saveStations(fetched)
FuelStore.saveLastRefresh(now) FuelStore.saveLastRefresh(now)
await WatchSyncManager.shared.pushSnapshot()
if let meta = LiveChainProvider.latestMeta { if let meta = LiveChainProvider.latestMeta {
FuelStore.saveRelayMeta(meta) FuelStore.saveRelayMeta(meta)
} }
+89
View File
@@ -0,0 +1,89 @@
import SwiftUI
import MapKit
import UIKit
/// Detail sheet for a tapped station (when Settings → Tap station = More info).
/// Shows a small map preview, the station name, address, distance from the
/// current location, and a Directions button that opens Apple Maps — the same
/// destination a direct tap would have opened.
struct StationDetailView: View {
let station: FuelStation
let location: Coordinate?
let distanceUnit: DistanceUnit
@Environment(\.dismiss) private var dismiss
@State private var showingReportIssueConfirmation = false
private let reportIssueURL = URL(string: "https://www.gov.uk/guidance/report-an-error-in-fuel-prices-or-forecourt-details")!
private var stationCoordinate: CLLocationCoordinate2D {
CLLocationCoordinate2D(latitude: station.lat, longitude: station.lng)
}
var body: some View {
NavigationStack {
List {
Section {
Map(position: .constant(.region(MKCoordinateRegion(
center: stationCoordinate,
span: MKCoordinateSpan(latitudeDelta: 0.02, longitudeDelta: 0.02)
)))) {
Marker(station.name, coordinate: stationCoordinate)
.tint(.red)
}
.frame(height: 180)
.clipShape(RoundedRectangle(cornerRadius: 12))
.allowsHitTesting(false)
.listRowInsets(EdgeInsets())
}
Section {
LabeledContent("Name", value: station.name)
LabeledContent("Address", value: "\(station.address), \(station.postcode)")
if let location {
let km = FuelStore.displayDistanceKM(
station: station, userLat: location.lat, userLng: location.lng
)
LabeledContent("Distance", value: distanceUnit.format(km))
.monospacedDigit()
}
ForEach(StationNotice.notices(for: station)) { notice in
Label(notice.title, systemImage: notice.symbolName)
.font(.subheadline.weight(.semibold))
.foregroundStyle(.orange)
.padding(.horizontal, 10)
.padding(.vertical, 6)
.background(Capsule().fill(Color.orange.opacity(0.14)))
}
}
Section {
Button {
if let url = station.mapsDirectionsURL {
UIApplication.shared.open(url)
}
} label: {
Label("Directions", systemImage: "arrow.triangle.turn.up.right.diamond.fill")
}
Button(role: .destructive) {
showingReportIssueConfirmation = true
} label: {
Label("Report issue", systemImage: "exclamationmark.bubble.fill")
}
}
}
.navigationTitle(station.name)
.navigationBarTitleDisplayMode(.inline)
.alert("Report an error?", isPresented: $showingReportIssueConfirmation) {
Button("Cancel", role: .cancel) { }
Button("Report") {
UIApplication.shared.open(reportIssueURL)
}
} message: {
Text("Are you sure you want to report an error in fuel prices or forecourt details? This will open in your chosen browser.")
}
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Done") { dismiss() }
}
}
}
}
}
+41
View File
@@ -0,0 +1,41 @@
import Foundation
/// Presentation-only notices derived from existing station metadata.
///
/// These do not alter `FuelStation`, persistence, filtering, sorting, or
/// navigation. They are intentionally small and reusable so additional
/// informational station notices can be added later from the UI layer.
enum StationNotice: Identifiable, Equatable {
case membershipRequired
var id: String {
switch self {
case .membershipRequired: return "membershipRequired"
}
}
var title: String {
switch self {
case .membershipRequired: return "Membership required"
}
}
var symbolName: String {
switch self {
case .membershipRequired: return "creditcard.fill"
}
}
static func notices(for station: FuelStation) -> [StationNotice] {
isCostco(station) ? [.membershipRequired] : []
}
static func isCostco(_ station: FuelStation) -> Bool {
containsCostcoToken(station.brand) || containsCostcoToken(station.name)
}
private static func containsCostcoToken(_ value: String) -> Bool {
let tokens = value.uppercased().split { !$0.isLetter && !$0.isNumber }
return tokens.contains("COSTCO")
}
}
+19 -3
View File
@@ -15,6 +15,7 @@ struct StationsView: View {
let topStationID: String? let topStationID: String?
let location: Coordinate? let location: Coordinate?
let favouriteIDs: Set<String> let favouriteIDs: Set<String>
let stationTapAction: StationTapAction
var onToggleFavourite: (FuelStation, FuelType) -> Void = { _, _ in } var onToggleFavourite: (FuelStation, FuelType) -> Void = { _, _ in }
var onRefresh: () async -> Void = {} var onRefresh: () async -> Void = {}
@@ -29,6 +30,9 @@ struct StationsView: View {
/// info button in the header — keeps the list focused on stations. /// info button in the header — keeps the list focused on stations.
@State private var showKey = false @State private var showKey = false
/// Station picked for the More-info sheet (tap action = showDetails).
@State private var detailStation: FuelStation?
/// Bottom-of-tab explainer (moved from the top 2026-08-16): mode/radius/ /// Bottom-of-tab explainer (moved from the top 2026-08-16): mode/radius/
/// directions context + "N/TOTAL stations updated". The numerator is the /// directions context + "N/TOTAL stations updated". The numerator is the
/// current list pool; the denominator is the full UK station total from /// current list pool; the denominator is the full UK station total from
@@ -38,6 +42,9 @@ struct StationsView: View {
let unit = distanceUnit.label(for: Double(miles)) let unit = distanceUnit.label(for: Double(miles))
let fuel = selectedFuel.displayName let fuel = selectedFuel.displayName
let mode = sortMode == .closest ? "Closest" : "Cheapest" let mode = sortMode == .closest ? "Closest" : "Cheapest"
let tapHint = stationTapAction == .openMap
? "tap a station for directions."
: "tap a station for details."
let ratio: String let ratio: String
if let total = FuelStore.loadStationCount() { if let total = FuelStore.loadStationCount() {
ratio = "\(totalCount)/\(total)" ratio = "\(totalCount)/\(total)"
@@ -48,9 +55,9 @@ struct StationsView: View {
if sortMode == .closest { if sortMode == .closest {
return "\(mode) \(fuel) stations — nearest first, best value within \(miles) \(unit) · \(ratio) stations updated" return "\(mode) \(fuel) stations — nearest first, best value within \(miles) \(unit) · \(ratio) stations updated"
} }
return "\(mode) \(fuel) within \(miles) \(unit) — \(ratio) stations updated · tap a station for directions." return "\(mode) \(fuel) within \(miles) \(unit) — \(ratio) stations updated · \(tapHint)"
} }
return "\(mode) \(fuel) — \(ratio) stations updated · tap a station for directions." return "\(mode) \(fuel) — \(ratio) stations updated · \(tapHint)"
} }
var body: some View { var body: some View {
@@ -126,7 +133,9 @@ struct StationsView: View {
baselinePrice: baselinePrice, baselinePrice: baselinePrice,
isTopResult: station.id == topStationID, isTopResult: station.id == topStationID,
isFavourite: favouriteIDs.contains(station.id), isFavourite: favouriteIDs.contains(station.id),
onToggleFavourite: { onToggleFavourite(station, selectedFuel) } tapAction: stationTapAction,
onToggleFavourite: { onToggleFavourite(station, selectedFuel) },
onShowDetails: { detailStation = $0 }
) )
} }
@@ -260,6 +269,13 @@ struct StationsView: View {
.presentationDetents([.fraction(0.6)]) .presentationDetents([.fraction(0.6)])
.presentationBackground(Color(UIColor.systemGroupedBackground)) .presentationBackground(Color(UIColor.systemGroupedBackground))
} }
.sheet(item: $detailStation) { station in
StationDetailView(
station: station,
location: location,
distanceUnit: distanceUnit
)
}
} }
} }
+132
View File
@@ -0,0 +1,132 @@
import Foundation
import WatchConnectivity
extension Notification.Name {
static let fuelBoardWatchRefreshRequested = Notification.Name("FuelBoardWatchRefreshRequested")
static let fuelBoardShowStationDetail = Notification.Name("FuelBoardShowStationDetail")
}
@MainActor
final class WatchSyncManager: NSObject, WCSessionDelegate {
static let shared = WatchSyncManager()
private var pendingSnapshotPush = false
private var activationRequested = false
private override init() {
super.init()
}
func activate() {
guard WCSession.isSupported() else { return }
let session = WCSession.default
guard session.isWatchAppInstalled else { return }
pendingSnapshotPush = true
if session.delegate !== self {
session.delegate = self
}
guard session.activationState != .activated else { return }
guard !activationRequested else { return }
activationRequested = true
session.activate()
}
func pushSnapshot() {
guard WCSession.isSupported() else { return }
let session = WCSession.default
guard session.isWatchAppInstalled else {
pendingSnapshotPush = false
return
}
guard session.activationState == .activated else {
pendingSnapshotPush = true
activate()
return
}
do {
try session.updateApplicationContext(snapshotContext())
pendingSnapshotPush = false
} catch {
print("WATCH-SYNC push failed: \(error.localizedDescription)")
}
}
private func snapshotContext() -> [String: Any] {
var context: [String: Any] = [:]
if let favourites = try? JSONEncoder().encode(FuelStore.loadFavourites()) {
context[FuelStore.favouritesKey] = favourites
}
// Do NOT send the full station dump to watchOS via WatchConnectivity.
// The UK dataset is megabytes large and exceeds application-context
// payload limits, which prevents any snapshot from arriving. The
// favourites payload already carries station snapshots + prices, so the
// watch can render favourites without the full list.
context[FuelStore.fuelKey] = FuelStore.loadSelectedFuel().rawValue
context[FuelStore.distanceUnitKey] = FuelStore.loadDistanceUnit().rawValue
context[FuelStore.priceDisplayStyleKey] = FuelStore.loadPriceDisplayStyle().rawValue
if let lastRefresh = FuelStore.loadLastRefresh() {
context[FuelStore.lastRefreshKey] = String(lastRefresh.timeIntervalSince1970)
}
if let handled = FuelStore.loadWatchRefreshHandled() {
context[FuelStore.watchRefreshHandledKey] = String(handled.timeIntervalSince1970)
}
if let location = FuelStore.loadLocationWithDate() {
context[FuelStore.locationKey] = "\(location.coordinate.lat),\(location.coordinate.lng),\(location.date.timeIntervalSince1970)"
}
return context
}
nonisolated func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: (any Error)?) {
Task { @MainActor in
self.activationRequested = false
}
if let error {
print("WATCH-SYNC activation failed: \(error.localizedDescription)")
return
}
Task { @MainActor in
if activationState == .activated, self.pendingSnapshotPush {
self.pushSnapshot()
}
}
}
nonisolated func sessionDidBecomeInactive(_ session: WCSession) {}
nonisolated func sessionDidDeactivate(_ session: WCSession) {
Task { @MainActor in
self.activationRequested = false
self.activate()
}
}
nonisolated func session(_ session: WCSession, didReceiveUserInfo userInfo: [String : Any] = [:]) {
handleIncomingMessage(userInfo)
}
nonisolated func session(_ session: WCSession, didReceiveMessage message: [String : Any]) {
handleIncomingMessage(message)
}
private nonisolated func handleIncomingMessage(_ payload: [String: Any]) {
if payload["fuelboard.snapshotRequest"] as? String != nil {
Task { @MainActor in
self.pushSnapshot()
}
}
guard let raw = payload[FuelStore.watchRefreshRequestKey] as? String,
let timestamp = TimeInterval(raw) else { return }
let date = Date(timeIntervalSince1970: timestamp)
Task { @MainActor in
FuelStore.requestWatchRefresh(date)
NotificationCenter.default.post(name: .fuelBoardWatchRefreshRequested, object: nil)
}
}
}
+14
View File
@@ -56,6 +56,14 @@
"Units" = "Units"; "Units" = "Units";
"Price display" = "Price display"; "Price display" = "Price display";
"Distances and search radii across the app, widget and alerts are shown in this unit. Prices can be shown as on a station sign (129.9) or in pounds and pence (£1.29⁹/L)." = "Distances and search radii across the app, widget and alerts are shown in this unit. Prices can be shown as on a station sign (129.9) or in pounds and pence (£1.29⁹/L)."; "Distances and search radii across the app, widget and alerts are shown in this unit. Prices can be shown as on a station sign (129.9) or in pounds and pence (£1.29⁹/L)." = "Distances and search radii across the app, widget and alerts are shown in this unit. Prices can be shown as on a station sign (129.9) or in pounds and pence (£1.29⁹/L).";
"Tap station" = "Tap station";
"Tap action" = "Tap action";
"Tapping a station either opens directions straight away or shows its details first." = "Tapping a station either opens directions straight away or shows its details first.";
"Open map" = "Open map";
"More info" = "More info";
"Name" = "Name";
"Address" = "Address";
"Distance" = "Distance";
"Show introduction" = "Replay onboarding"; "Show introduction" = "Replay onboarding";
"Replay the welcome screen, including the location and notification permission prompts." = "Replay the welcome screen, including the location and notification permission prompts."; "Replay the welcome screen, including the location and notification permission prompts." = "Replay the welcome screen, including the location and notification permission prompts.";
"Test alert notification (real data)" = "Test alert notification (real data)"; "Test alert notification (real data)" = "Test alert notification (real data)";
@@ -139,6 +147,12 @@
"You" = "You"; "You" = "You";
"Done" = "Done"; "Done" = "Done";
"Directions" = "Directions"; "Directions" = "Directions";
"Report issue" = "Report issue";
"Membership required" = "Membership required";
"Report an error?" = "Report an error?";
"Cancel" = "Cancel";
"Report" = "Report";
"Are you sure you want to report an error in fuel prices or forecourt details? This will open in your chosen browser." = "Are you sure you want to report an error in fuel prices or forecourt details? This will open in your chosen browser.";
/* Station row */ /* Station row */
"best" = "best"; "best" = "best";
@@ -1,24 +0,0 @@
import Foundation
enum DataRefreshMode: String, CaseIterable, Identifiable {
case standard
case smart
var id: String { rawValue }
var displayName: String {
switch self {
case .standard: return "Standard"
case .smart: return "Smart"
}
}
var summary: String {
switch self {
case .standard:
return "Saves battery. Refreshes price data up to twice daily."
case .smart:
return "Checks for newer data more often in the background and refreshes full prices only when an update is available."
}
}
}
@@ -0,0 +1 @@
../../../Shared/DataRefreshMode.swift
@@ -1,5 +1,7 @@
import XCTest import XCTest
#if SWIFT_PACKAGE
@testable import FuelBoardShared @testable import FuelBoardShared
#endif
// FuelBoard shared-logic tests. Run from FuelBoardTests/ with `swift test`. // FuelBoard shared-logic tests. Run from FuelBoardTests/ with `swift test`.
// Covers the pure logic that drives the app + widget: title sanitizer, price // Covers the pure logic that drives the app + widget: title sanitizer, price
@@ -1,5 +1,7 @@
import XCTest import XCTest
#if SWIFT_PACKAGE
@testable import FuelBoardShared @testable import FuelBoardShared
#endif
// FuelHistory — price-history store for the Favourites Trends graph. // FuelHistory — price-history store for the Favourites Trends graph.
// Covers day math, slim day parsing, series building, delta rebasing and // Covers day math, slim day parsing, series building, delta rebasing and
@@ -1,5 +1,7 @@
import XCTest import XCTest
#if SWIFT_PACKAGE
@testable import FuelBoardShared @testable import FuelBoardShared
#endif
/// P0 live chain: GitHub mirror FIRST everywhere → [dev-only LAN relay] → /// P0 live chain: GitHub mirror FIRST everywhere → [dev-only LAN relay] →
/// (app: bundled dump in ContentView's catch, widget: cache → placeholder). /// (app: bundled dump in ContentView's catch, widget: cache → placeholder).
+63
View File
@@ -0,0 +1,63 @@
{
"identifier" : "fuelboard-local-tips",
"nonRenewingSubscriptions" : [
],
"products" : [
{
"displayPrice" : "0.99",
"familyShareable" : false,
"internalID" : "fuelboard-tip099",
"localizations" : [
{
"description" : "Just enough to keep things moving.",
"displayName" : "Splash & Dash",
"locale" : "en_GB"
}
],
"productID" : "com.apt.fuelboard.tip099",
"referenceName" : "FuelBoard Tip 099 - Splash & Dash",
"type" : "Consumable"
},
{
"displayPrice" : "2.99",
"familyShareable" : false,
"internalID" : "fuelboard-tip299",
"localizations" : [
{
"description" : "A generous top-up for development.",
"displayName" : "Half a Tank",
"locale" : "en_GB"
}
],
"productID" : "com.apt.fuelboard.tip299",
"referenceName" : "FuelBoard Tip 299 - Half a Tank",
"type" : "Consumable"
},
{
"displayPrice" : "4.99",
"familyShareable" : false,
"internalID" : "fuelboard-tip499",
"localizations" : [
{
"description" : "Keeping the app on the road.",
"displayName" : "Fill 'Er Up",
"locale" : "en_GB"
}
],
"productID" : "com.apt.fuelboard.tip499",
"referenceName" : "FuelBoard Tip 499 - Fill 'Er Up",
"type" : "Consumable"
}
],
"settings" : {
},
"subscriptionGroups" : [
],
"version" : {
"major" : 2,
"minor" : 0
}
}
@@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,156 @@
{
"images": [
{
"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"
},
{
"filename": "Watch-LongLook-88.png",
"idiom": "watch",
"role": "longLook",
"scale": "2x",
"size": "44x44",
"subtype": "42mm"
},
{
"filename": "Watch-LongLook-100.png",
"idiom": "watch",
"role": "longLook",
"scale": "2x",
"size": "50x50",
"subtype": "44mm"
}
],
"info": {
"author": "xcode",
"version": 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 747 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

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