Compare commits
@@ -1,4 +1,6 @@
|
|||||||
build/
|
build/
|
||||||
|
Builds/
|
||||||
|
build-*/
|
||||||
build-release/
|
build-release/
|
||||||
build-sim/
|
build-sim/
|
||||||
build-watch/
|
build-watch/
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
|
After Width: | Height: | Size: 724 KiB |
|
After Width: | Height: | Size: 686 KiB |
|
After Width: | Height: | Size: 303 KiB |
|
After Width: | Height: | Size: 280 KiB |
|
After Width: | Height: | Size: 349 KiB |
@@ -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`,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>com.apple.security.application-groups</key>
|
||||||
|
<array>
|
||||||
|
<string>group.com.apt.fuelboard</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
|
<key>CFBundleDisplayName</key>
|
||||||
|
<string>FuelBoardWatch</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>$(PRODUCT_NAME)</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>$(MARKETING_VERSION)</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||||
|
<key>NSExtension</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSExtensionAttributes</key>
|
||||||
|
<dict>
|
||||||
|
<key>WKAppBundleIdentifier</key>
|
||||||
|
<string>com.apt.fuelboard.watchapp</string>
|
||||||
|
</dict>
|
||||||
|
<key>NSExtensionPointIdentifier</key>
|
||||||
|
<string>com.apple.watchkit</string>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
|
<key>CFBundleDisplayName</key>
|
||||||
|
<string>FuelBoard</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>FuelBoard</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>$(MARKETING_VERSION)</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||||
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||||
|
</array>
|
||||||
|
<key>WKCompanionAppBundleIdentifier</key>
|
||||||
|
<string>com.apt.fuelboard</string>
|
||||||
|
<key>WKWatchKitApp</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -17,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>
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 7.9 KiB |
|
After Width: | Height: | Size: 9.1 KiB |
|
After Width: | Height: | Size: 9.6 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 747 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 46 KiB |
@@ -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" : {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,18 +1,22 @@
|
|||||||
{
|
{
|
||||||
"images": [
|
"images" : [
|
||||||
{
|
{
|
||||||
"filename": "brand_asda_2x.png",
|
"idiom" : "universal",
|
||||||
"idiom": "universal",
|
"scale" : "1x"
|
||||||
"scale": "2x"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename": "brand_asda_3x.png",
|
"filename" : "brand_asda_2x.png",
|
||||||
"idiom": "universal",
|
"idiom" : "universal",
|
||||||
"scale": "3x"
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "brand_asda_3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info": {
|
"info" : {
|
||||||
"author": "xcode",
|
"author" : "xcode",
|
||||||
"version": 1
|
"version" : 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,18 +1,22 @@
|
|||||||
{
|
{
|
||||||
"images": [
|
"images" : [
|
||||||
{
|
{
|
||||||
"filename": "brand_bp_2x.png",
|
"idiom" : "universal",
|
||||||
"idiom": "universal",
|
"scale" : "1x"
|
||||||
"scale": "2x"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename": "brand_bp_3x.png",
|
"filename" : "brand_bp_2x.png",
|
||||||
"idiom": "universal",
|
"idiom" : "universal",
|
||||||
"scale": "3x"
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "brand_bp_3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info": {
|
"info" : {
|
||||||
"author": "xcode",
|
"author" : "xcode",
|
||||||
"version": 1
|
"version" : 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,22 @@
|
|||||||
{
|
{
|
||||||
"images": [
|
"images" : [
|
||||||
{
|
{
|
||||||
"filename": "brand_tesco_2x.png",
|
"idiom" : "universal",
|
||||||
"idiom": "universal",
|
"scale" : "1x"
|
||||||
"scale": "2x"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename": "brand_tesco_3x.png",
|
"filename" : "brand_tesco_2x.png",
|
||||||
"idiom": "universal",
|
"idiom" : "universal",
|
||||||
"scale": "3x"
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "brand_tesco_3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info": {
|
"info" : {
|
||||||
"author": "xcode",
|
"author" : "xcode",
|
||||||
"version": 1
|
"version" : 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,18 +1,22 @@
|
|||||||
{
|
{
|
||||||
"images": [
|
"images" : [
|
||||||
{
|
{
|
||||||
"filename": "brand_texaco_2x.png",
|
"idiom" : "universal",
|
||||||
"idiom": "universal",
|
"scale" : "1x"
|
||||||
"scale": "2x"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename": "brand_texaco_3x.png",
|
"filename" : "brand_texaco_2x.png",
|
||||||
"idiom": "universal",
|
"idiom" : "universal",
|
||||||
"scale": "3x"
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "brand_texaco_3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info": {
|
"info" : {
|
||||||
"author": "xcode",
|
"author" : "xcode",
|
||||||
"version": 1
|
"version" : 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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).
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 7.9 KiB |
|
After Width: | Height: | Size: 9.1 KiB |
|
After Width: | Height: | Size: 9.6 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 9.1 KiB |
|
After Width: | Height: | Size: 747 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 46 KiB |
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||