Compare commits

...
4 Commits
38 changed files with 716 additions and 250 deletions
+185 -9
View File
@@ -7,6 +7,17 @@
objects = {
/* 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, ); }; };
@@ -73,6 +84,19 @@
/* End PBXCopyFilesBuildPhase 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; };
@@ -108,6 +132,14 @@
/* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */
FBA000000000000000000302 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
FBA00000000000000000000B /* XCTest.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
ADCE5B4C3033A329008C131C /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
@@ -139,9 +171,47 @@
/* End PBXFrameworksBuildPhase 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 = {
isa = PBXGroup;
children = (
FBA000000000000000000200 /* FuelBoardTests */,
AD22FCFF30682512009859D4 /* FuelBoardTips.storekit */,
BB0000000000000000000008 /* FuelBoard */,
BB0000000000000000000009 /* FuelBoardWidgets */,
BB000000000000000000000A /* Shared */,
@@ -151,7 +221,7 @@
);
sourceTree = "<group>";
};
BB000000000000000000000E /* Products */ = {
BB000000000000000000000E = {
isa = PBXGroup;
children = (
BB0000000000000000000007 /* FuelBoard.app */,
@@ -165,6 +235,23 @@
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
FBA000000000000000000300 /* FuelBoardSharedTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = FBA000000000000000000400 /* Build configuration list for PBXNativeTarget "FuelBoardSharedTests" */;
buildPhases = (
FBA000000000000000000301 /* Sources */,
FBA000000000000000000302 /* Frameworks */,
FBA000000000000000000303 /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = FuelBoardSharedTests;
productName = FuelBoardSharedTests;
productReference = 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" */;
@@ -278,6 +365,11 @@
};
BB000000000000000000000F = {
CreatedOnToolsVersion = 16.0;
SystemCapabilities = {
com.apple.InAppPurchase = {
enabled = 1;
};
};
};
};
};
@@ -299,11 +391,19 @@
BB0000000000000000000005 /* FuelBoardWidgets */,
ADCE5B673033A32D008C131C /* FuelBoardWatch Watch App */,
ADCE5B4E3033A329008C131C /* FuelBoardWatch Watch App Extension */,
FBA000000000000000000300 /* FuelBoardSharedTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
FBA000000000000000000303 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
ADCE5B4D3033A329008C131C /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
@@ -335,6 +435,23 @@
/* End PBXResourcesBuildPhase 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;
@@ -407,7 +524,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_ENTITLEMENTS = Config/WatchApp.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 20;
CURRENT_PROJECT_VERSION = 22;
DEVELOPMENT_TEAM = F3BE6NE7U3;
ENABLE_PREVIEWS = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
@@ -473,7 +590,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_ENTITLEMENTS = Config/WatchApp.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 20;
CURRENT_PROJECT_VERSION = 22;
DEVELOPMENT_TEAM = F3BE6NE7U3;
ENABLE_PREVIEWS = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
@@ -537,7 +654,7 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 20;
CURRENT_PROJECT_VERSION = 22;
DEVELOPMENT_TEAM = F3BE6NE7U3;
ENABLE_PREVIEWS = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
@@ -602,7 +719,7 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 20;
CURRENT_PROJECT_VERSION = 22;
DEVELOPMENT_TEAM = F3BE6NE7U3;
ENABLE_PREVIEWS = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
@@ -698,8 +815,9 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = Config/App.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 20;
CURRENT_PROJECT_VERSION = 22;
DEVELOPMENT_TEAM = F3BE6NE7U3;
GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = "Config/App-Info.plist";
@@ -710,6 +828,7 @@
MARKETING_VERSION = 1.0.0;
PRODUCT_BUNDLE_IDENTIFIER = com.apt.fuelboard;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
@@ -721,8 +840,9 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = Config/App.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 20;
CURRENT_PROJECT_VERSION = 22;
DEVELOPMENT_TEAM = F3BE6NE7U3;
GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = "Config/App-Info.plist";
@@ -733,6 +853,7 @@
MARKETING_VERSION = 1.0.0;
PRODUCT_BUNDLE_IDENTIFIER = com.apt.fuelboard;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
@@ -745,7 +866,7 @@
APPLICATION_EXTENSION_API_ONLY = YES;
CODE_SIGN_ENTITLEMENTS = Config/Widget.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 20;
CURRENT_PROJECT_VERSION = 22;
DEVELOPMENT_TEAM = F3BE6NE7U3;
GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = "Config/Widget-Info.plist";
@@ -770,7 +891,7 @@
APPLICATION_EXTENSION_API_ONLY = YES;
CODE_SIGN_ENTITLEMENTS = Config/Widget.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 20;
CURRENT_PROJECT_VERSION = 22;
DEVELOPMENT_TEAM = F3BE6NE7U3;
GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = "Config/Widget-Info.plist";
@@ -789,9 +910,64 @@
};
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 */
/* Begin XCConfigurationList section */
FBA000000000000000000400 /* Build configuration list for PBXNativeTarget "FuelBoardSharedTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
FBA000000000000000000401 /* Debug */,
FBA000000000000000000402 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
ADCE5B593033A32B008C131C /* Build configuration list for PBXNativeTarget "FuelBoardWatch Watch App Extension" */ = {
isa = XCConfigurationList;
buildConfigurations = (
@@ -28,6 +28,16 @@
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">
@@ -4,6 +4,14 @@
"filename" : "AppIconArtwork.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
@@ -4,6 +4,14 @@
"filename" : "AppIconPreview.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
@@ -4,6 +4,14 @@
"filename" : "AppIconPreviewDark.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
File diff suppressed because one or more lines are too long
@@ -4,6 +4,14 @@
"filename" : "PreviewWallpaper.jpg",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
@@ -1,18 +1,22 @@
{
"images": [
"images" : [
{
"filename": "brand_applegreen_2x.png",
"idiom": "universal",
"scale": "2x"
"idiom" : "universal",
"scale" : "1x"
},
{
"filename": "brand_applegreen_3x.png",
"idiom": "universal",
"scale": "3x"
"filename" : "brand_applegreen_2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "brand_applegreen_3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info": {
"author": "xcode",
"version": 1
"info" : {
"author" : "xcode",
"version" : 1
}
}
}
+15 -11
View File
@@ -1,18 +1,22 @@
{
"images": [
"images" : [
{
"filename": "brand_asda_2x.png",
"idiom": "universal",
"scale": "2x"
"idiom" : "universal",
"scale" : "1x"
},
{
"filename": "brand_asda_3x.png",
"idiom": "universal",
"scale": "3x"
"filename" : "brand_asda_2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "brand_asda_3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info": {
"author": "xcode",
"version": 1
"info" : {
"author" : "xcode",
"version" : 1
}
}
}
+15 -11
View File
@@ -1,18 +1,22 @@
{
"images": [
"images" : [
{
"filename": "brand_bp_2x.png",
"idiom": "universal",
"scale": "2x"
"idiom" : "universal",
"scale" : "1x"
},
{
"filename": "brand_bp_3x.png",
"idiom": "universal",
"scale": "3x"
"filename" : "brand_bp_2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "brand_bp_3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info": {
"author": "xcode",
"version": 1
"info" : {
"author" : "xcode",
"version" : 1
}
}
}
@@ -1,18 +1,22 @@
{
"images": [
"images" : [
{
"filename": "brand_circle_k_2x.png",
"idiom": "universal",
"scale": "2x"
"idiom" : "universal",
"scale" : "1x"
},
{
"filename": "brand_circle_k_3x.png",
"idiom": "universal",
"scale": "3x"
"filename" : "brand_circle_k_2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "brand_circle_k_3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info": {
"author": "xcode",
"version": 1
"info" : {
"author" : "xcode",
"version" : 1
}
}
+14 -10
View File
@@ -1,18 +1,22 @@
{
"images": [
"images" : [
{
"filename": "brand_essar_2x.png",
"idiom": "universal",
"scale": "2x"
"idiom" : "universal",
"scale" : "1x"
},
{
"filename": "brand_essar_3x.png",
"idiom": "universal",
"scale": "3x"
"filename" : "brand_essar_2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "brand_essar_3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info": {
"author": "xcode",
"version": 1
"info" : {
"author" : "xcode",
"version" : 1
}
}
+15 -11
View File
@@ -1,18 +1,22 @@
{
"images": [
"images" : [
{
"filename": "brand_esso_2x.png",
"idiom": "universal",
"scale": "2x"
"idiom" : "universal",
"scale" : "1x"
},
{
"filename": "brand_esso_3x.png",
"idiom": "universal",
"scale": "3x"
"filename" : "brand_esso_2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "brand_esso_3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info": {
"author": "xcode",
"version": 1
"info" : {
"author" : "xcode",
"version" : 1
}
}
}
@@ -1,18 +1,22 @@
{
"images": [
"images" : [
{
"filename": "brand_gleaner_2x.png",
"idiom": "universal",
"scale": "2x"
"idiom" : "universal",
"scale" : "1x"
},
{
"filename": "brand_gleaner_3x.png",
"idiom": "universal",
"scale": "3x"
"filename" : "brand_gleaner_2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "brand_gleaner_3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info": {
"author": "xcode",
"version": 1
"info" : {
"author" : "xcode",
"version" : 1
}
}
+15 -11
View File
@@ -1,18 +1,22 @@
{
"images": [
"images" : [
{
"filename": "brand_gulf_2x.png",
"idiom": "universal",
"scale": "2x"
"idiom" : "universal",
"scale" : "1x"
},
{
"filename": "brand_gulf_3x.png",
"idiom": "universal",
"scale": "3x"
"filename" : "brand_gulf_2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "brand_gulf_3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info": {
"author": "xcode",
"version": 1
"info" : {
"author" : "xcode",
"version" : 1
}
}
}
@@ -1,18 +1,22 @@
{
"images": [
"images" : [
{
"filename": "brand_highland_fuels_2x.png",
"idiom": "universal",
"scale": "2x"
"idiom" : "universal",
"scale" : "1x"
},
{
"filename": "brand_highland_fuels_3x.png",
"idiom": "universal",
"scale": "3x"
"filename" : "brand_highland_fuels_2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "brand_highland_fuels_3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info": {
"author": "xcode",
"version": 1
"info" : {
"author" : "xcode",
"version" : 1
}
}
+15 -11
View File
@@ -1,18 +1,22 @@
{
"images": [
"images" : [
{
"filename": "brand_jet_2x.png",
"idiom": "universal",
"scale": "2x"
"idiom" : "universal",
"scale" : "1x"
},
{
"filename": "brand_jet_3x.png",
"idiom": "universal",
"scale": "3x"
"filename" : "brand_jet_2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "brand_jet_3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info": {
"author": "xcode",
"version": 1
"info" : {
"author" : "xcode",
"version" : 1
}
}
}
+14 -10
View File
@@ -1,18 +1,22 @@
{
"images": [
"images" : [
{
"filename": "brand_maxol_2x.png",
"idiom": "universal",
"scale": "2x"
"idiom" : "universal",
"scale" : "1x"
},
{
"filename": "brand_maxol_3x.png",
"idiom": "universal",
"scale": "3x"
"filename" : "brand_maxol_2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "brand_maxol_3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info": {
"author": "xcode",
"version": 1
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -1,18 +1,22 @@
{
"images": [
"images" : [
{
"filename": "brand_morrisons_2x.png",
"idiom": "universal",
"scale": "2x"
"idiom" : "universal",
"scale" : "1x"
},
{
"filename": "brand_morrisons_3x.png",
"idiom": "universal",
"scale": "3x"
"filename" : "brand_morrisons_2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "brand_morrisons_3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info": {
"author": "xcode",
"version": 1
"info" : {
"author" : "xcode",
"version" : 1
}
}
}
+14 -10
View File
@@ -1,18 +1,22 @@
{
"images": [
"images" : [
{
"filename": "brand_murco_2x.png",
"idiom": "universal",
"scale": "2x"
"idiom" : "universal",
"scale" : "1x"
},
{
"filename": "brand_murco_3x.png",
"idiom": "universal",
"scale": "3x"
"filename" : "brand_murco_2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "brand_murco_3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info": {
"author": "xcode",
"version": 1
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -1,18 +1,22 @@
{
"images": [
"images" : [
{
"filename": "brand_sainsburys_2x.png",
"idiom": "universal",
"scale": "2x"
"idiom" : "universal",
"scale" : "1x"
},
{
"filename": "brand_sainsburys_3x.png",
"idiom": "universal",
"scale": "3x"
"filename" : "brand_sainsburys_2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "brand_sainsburys_3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info": {
"author": "xcode",
"version": 1
"info" : {
"author" : "xcode",
"version" : 1
}
}
}
+15 -11
View File
@@ -1,18 +1,22 @@
{
"images": [
"images" : [
{
"filename": "brand_shell_2x.png",
"idiom": "universal",
"scale": "2x"
"idiom" : "universal",
"scale" : "1x"
},
{
"filename": "brand_shell_3x.png",
"idiom": "universal",
"scale": "3x"
"filename" : "brand_shell_2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "brand_shell_3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info": {
"author": "xcode",
"version": 1
"info" : {
"author" : "xcode",
"version" : 1
}
}
}
+14 -10
View File
@@ -1,18 +1,22 @@
{
"images": [
"images" : [
{
"filename": "brand_spar_2x.png",
"idiom": "universal",
"scale": "2x"
"idiom" : "universal",
"scale" : "1x"
},
{
"filename": "brand_spar_3x.png",
"idiom": "universal",
"scale": "3x"
"filename" : "brand_spar_2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "brand_spar_3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info": {
"author": "xcode",
"version": 1
"info" : {
"author" : "xcode",
"version" : 1
}
}
+15 -11
View File
@@ -1,18 +1,22 @@
{
"images": [
"images" : [
{
"filename": "brand_tesco_2x.png",
"idiom": "universal",
"scale": "2x"
"idiom" : "universal",
"scale" : "1x"
},
{
"filename": "brand_tesco_3x.png",
"idiom": "universal",
"scale": "3x"
"filename" : "brand_tesco_2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "brand_tesco_3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info": {
"author": "xcode",
"version": 1
"info" : {
"author" : "xcode",
"version" : 1
}
}
}
+15 -11
View File
@@ -1,18 +1,22 @@
{
"images": [
"images" : [
{
"filename": "brand_texaco_2x.png",
"idiom": "universal",
"scale": "2x"
"idiom" : "universal",
"scale" : "1x"
},
{
"filename": "brand_texaco_3x.png",
"idiom": "universal",
"scale": "3x"
"filename" : "brand_texaco_2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "brand_texaco_3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info": {
"author": "xcode",
"version": 1
"info" : {
"author" : "xcode",
"version" : 1
}
}
}
@@ -1,18 +1,22 @@
{
"images": [
"images" : [
{
"filename": "brand_the_co_operative_2x.png",
"idiom": "universal",
"scale": "2x"
"idiom" : "universal",
"scale" : "1x"
},
{
"filename": "brand_the_co_operative_3x.png",
"idiom": "universal",
"scale": "3x"
"filename" : "brand_the_co_operative_2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "brand_the_co_operative_3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info": {
"author": "xcode",
"version": 1
"info" : {
"author" : "xcode",
"version" : 1
}
}
+14 -10
View File
@@ -1,18 +1,22 @@
{
"images": [
"images" : [
{
"filename": "brand_valero_2x.png",
"idiom": "universal",
"scale": "2x"
"idiom" : "universal",
"scale" : "1x"
},
{
"filename": "brand_valero_3x.png",
"idiom": "universal",
"scale": "3x"
"filename" : "brand_valero_2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "brand_valero_3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info": {
"author": "xcode",
"version": 1
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -1,18 +1,22 @@
{
"images": [
"images" : [
{
"filename": "brand_welcome_break_2x.png",
"idiom": "universal",
"scale": "2x"
"idiom" : "universal",
"scale" : "1x"
},
{
"filename": "brand_welcome_break_3x.png",
"idiom": "universal",
"scale": "3x"
"filename" : "brand_welcome_break_2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "brand_welcome_break_3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info": {
"author": "xcode",
"version": 1
"info" : {
"author" : "xcode",
"version" : 1
}
}
+16 -2
View File
@@ -11,6 +11,7 @@ final class WatchSyncManager: NSObject, WCSessionDelegate {
static let shared = WatchSyncManager()
private var pendingSnapshotPush = false
private var activationRequested = false
private override init() {
super.init()
@@ -18,19 +19,28 @@ final class WatchSyncManager: NSObject, WCSessionDelegate {
func activate() {
guard WCSession.isSupported() else { return }
pendingSnapshotPush = true
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 {
@@ -73,6 +83,9 @@ final class WatchSyncManager: NSObject, WCSessionDelegate {
}
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
@@ -88,7 +101,8 @@ final class WatchSyncManager: NSObject, WCSessionDelegate {
nonisolated func sessionDidDeactivate(_ session: WCSession) {
Task { @MainActor in
WCSession.default.activate()
self.activationRequested = false
self.activate()
}
}
@@ -1,5 +1,7 @@
import XCTest
#if SWIFT_PACKAGE
@testable import FuelBoardShared
#endif
// FuelBoard shared-logic tests. Run from FuelBoardTests/ with `swift test`.
// Covers the pure logic that drives the app + widget: title sanitizer, price
@@ -1,5 +1,7 @@
import XCTest
#if SWIFT_PACKAGE
@testable import FuelBoardShared
#endif
// FuelHistory — price-history store for the Favourites Trends graph.
// Covers day math, slim day parsing, series building, delta rebasing and
@@ -1,5 +1,7 @@
import XCTest
#if SWIFT_PACKAGE
@testable import FuelBoardShared
#endif
/// P0 live chain: GitHub mirror FIRST everywhere → [dev-only LAN relay] →
/// (app: bundled dump in ContentView's catch, widget: cache → placeholder).
+12 -1
View File
@@ -157,6 +157,7 @@ private enum WatchFuelCache {
static let appGroupSuite = "group.com.apt.fuelboard"
static let favouritesKey = "fuelboard.favourites"
static let stationsKey = "fuelboard.stations"
static let stationsFileName = "fuelboard.stations.json"
static let fuelKey = "fuelboard.selectedFuel"
static let distanceUnitKey = "fuelboard.distanceUnit"
static let priceStyleKey = "fuelboard.priceDisplayStyle"
@@ -228,7 +229,17 @@ private enum WatchFuelCache {
}
static func loadStations() -> [WatchStation] {
guard let data = defaults()?.data(forKey: stationsKey),
let fileData: Data?
if let container = FileManager.default.containerURL(
forSecurityApplicationGroupIdentifier: appGroupSuite
) {
let url = container.appendingPathComponent(stationsFileName)
fileData = try? Data(contentsOf: url)
} else {
fileData = nil
}
let data = fileData ?? defaults()?.data(forKey: stationsKey)
guard let data,
let stations = try? JSONDecoder().decode([WatchStation].self, from: data) else {
return []
}
@@ -10,6 +10,7 @@ final class WatchSyncManager: NSObject, WCSessionDelegate {
private let suiteName = "group.com.apt.fuelboard"
private let snapshotRequestKey = "fuelboard.snapshotRequest"
private var activationRequested = false
private override init() {
super.init()
@@ -21,14 +22,21 @@ final class WatchSyncManager: NSObject, WCSessionDelegate {
if session.delegate !== self {
session.delegate = self
}
guard session.activationState != .activated else { return }
guard !activationRequested else { return }
activationRequested = true
session.activate()
}
func requestRefresh(at date: Date = Date()) {
guard WCSession.isSupported() else { return }
let session = WCSession.default
guard session.activationState == .activated else {
activate()
return
}
let raw = String(date.timeIntervalSince1970)
let payload = ["fuelboard.watchRefreshRequest": raw]
let session = WCSession.default
if session.isReachable {
session.sendMessage(payload, replyHandler: nil, errorHandler: nil)
@@ -39,8 +47,12 @@ final class WatchSyncManager: NSObject, WCSessionDelegate {
func requestSnapshot() {
guard WCSession.isSupported() else { return }
let payload = [snapshotRequestKey: String(Date().timeIntervalSince1970)]
let session = WCSession.default
guard session.activationState == .activated else {
activate()
return
}
let payload = [snapshotRequestKey: String(Date().timeIntervalSince1970)]
if session.isReachable {
session.sendMessage(payload, replyHandler: nil, errorHandler: nil)
@@ -82,6 +94,7 @@ final class WatchSyncManager: NSObject, WCSessionDelegate {
}
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: (any Error)?) {
activationRequested = false
if let error {
print("WATCH-SYNC activation failed: \(error.localizedDescription)")
return
+39 -7
View File
@@ -419,13 +419,37 @@ struct FuelStore {
static let watchRefreshHandledKey = "fuelboard.watchRefreshHandled" // TimeInterval (seconds since 1970)
static let relaySourceKey = "fuelboard.relaySource" // String — "api" | "csv"
static let stationCountKey = "fuelboard.stationCount" // String — station count
static let dataUpdatedKey = "fuelboard.dataUpdated" // String — govUK dataset update time
static let dataUpdatedKey = "fuelboard.dataUpdated" // govUK dataset update time
// Large shared payloads belong in the App Group container, not
// UserDefaults/CFPreferences (which has a roughly 4 MB limit).
static let stationsFileName = "fuelboard.stations.json"
static func appGroupFileURL(_ name: String) -> URL? {
FileManager.default
.containerURL(forSecurityApplicationGroupIdentifier: appGroupSuite)?
.appendingPathComponent(name, isDirectory: false)
}
static func loadAppGroupFile(_ name: String) -> Data? {
guard let url = appGroupFileURL(name) else { return nil }
return try? Data(contentsOf: url)
}
@discardableResult
static func saveAppGroupFile(_ data: Data, name: String) -> Bool {
guard let url = appGroupFileURL(name) else { return false }
do {
try data.write(to: url, options: .atomic)
return true
} catch {
return false
}
}
// MARK: Stations
// The full-UK dataset (~2.9 MB) lives in app-group UserDefaults only —
// keychain is for small values and cannot hold it. Read order is
// defaults-first for stations (keychain may hold a legacy small set from
// older builds; the full country dump always wins).
// The full-UK dataset is stored as an App Group file. UserDefaults is
// reserved for small preferences and has a roughly 4 MB total limit.
/// Stations from `stations` tied with `price` for `fuel` (within 0.01p),
/// sorted nearest-first from `lat`/`lng`. Used to offer multiple
@@ -444,10 +468,19 @@ struct FuelStore {
}
static func loadStations() -> [FuelStation] {
if let data = loadAppGroupFile(stationsFileName),
let stations = try? JSONDecoder().decode([FuelStation].self, from: data),
!stations.isEmpty {
return stations.map(sanitized)
}
// Migrate the legacy App Group UserDefaults value once.
if let defaults = UserDefaults(suiteName: appGroupSuite),
let data = defaults.data(forKey: stationsKey),
let stations = try? JSONDecoder().decode([FuelStation].self, from: data),
!stations.isEmpty {
if saveAppGroupFile(data, name: stationsFileName) {
defaults.removeObject(forKey: stationsKey)
}
return stations.map(sanitized)
}
if let data = keychainData(service: stationsKey),
@@ -468,8 +501,7 @@ struct FuelStore {
static func saveStations(_ stations: [FuelStation]) {
if let data = try? JSONEncoder().encode(stations) {
UserDefaults(suiteName: appGroupSuite)?.set(data, forKey: stationsKey)
// Intentionally NOT written to keychain — 2.9 MB exceeds its limits.
_ = saveAppGroupFile(data, name: stationsFileName)
}
}
+13 -4
View File
@@ -37,10 +37,11 @@ struct MirrorFuelProvider: FuelPriceProviding {
/// surfaced via `LiveChainProvider.latestMeta` in Settings → About.
static var latestMeta: RelayMeta?
/// App-group cache of the last fetched FULL dump, keyed by snapshot day.
/// App-group file cache of the last fetched FULL dump, keyed by snapshot day.
/// The mirror pushes once/day, so between pushes the app reuses the
/// cached dump instead of re-downloading ~2.8 MB at every 12 h gate.
static let liveDumpCacheKey = "fuelboard.liveDumpCache"
static let liveDumpCacheFileName = "fuelboard.liveDumpCache.json"
struct DumpCache: Codable {
let day: String
@@ -48,18 +49,26 @@ struct MirrorFuelProvider: FuelPriceProviding {
}
static func loadDumpCache() -> DumpCache? {
if let raw = FuelStore.loadAppGroupFile(liveDumpCacheFileName),
let cache = try? JSONDecoder().decode(DumpCache.self, from: raw) {
return cache
}
// Migrate the legacy UserDefaults cache when present.
guard let defaults = UserDefaults(suiteName: FuelStore.appGroupSuite),
let raw = defaults.data(forKey: liveDumpCacheKey),
let cache = try? JSONDecoder().decode(DumpCache.self, from: raw) else {
return nil
}
if FuelStore.saveAppGroupFile(raw, name: liveDumpCacheFileName) {
defaults.removeObject(forKey: liveDumpCacheKey)
}
return cache
}
static func saveDumpCache(day: String, data: Data) {
guard let defaults = UserDefaults(suiteName: FuelStore.appGroupSuite),
let raw = try? JSONEncoder().encode(DumpCache(day: day, data: data)) else { return }
defaults.set(raw, forKey: liveDumpCacheKey)
guard let raw = try? JSONEncoder().encode(DumpCache(day: day, data: data)) else { return }
_ = FuelStore.saveAppGroupFile(raw, name: liveDumpCacheFileName)
}
/// True when the cached dump is already the freshest the mirror has —
+9 -3
View File
@@ -6,12 +6,14 @@ cd "$(dirname "$0")/.."
PROJECT="$PWD/FuelBoard.xcodeproj"
SCHEME="FuelBoard"
DERIVED_DATA="/tmp/fuelboard-build-ipas"
SERVE_DIR="/tmp/ipa-serve"
SERVE_DIR="$PWD/Builds"
FULL_IPA="$SERVE_DIR/FuelBoard-watch.ipa"
SIDELOAD_IPA="$SERVE_DIR/FuelBoard-sideload.ipa"
HOST="192.168.1.131"
PORT="8765"
export SERVE_DIR
APP_PATH="$DERIVED_DATA/Build/Products/Release-iphoneos/FuelBoard.app"
STAGE_FULL="/tmp/fuelboard-ipa-stage-full"
STAGE_SIDELOAD="/tmp/fuelboard-ipa-stage-sideload"
@@ -46,7 +48,9 @@ import os, shutil, zipfile
app = '/tmp/fuelboard-build-ipas/Build/Products/Release-iphoneos/FuelBoard.app'
stage = '/tmp/fuelboard-ipa-stage-full'
payload = os.path.join(stage, 'Payload')
out = '/tmp/ipa-serve/FuelBoard-watch.ipa'
serve = os.environ['SERVE_DIR']
out = os.path.join(serve, 'FuelBoard-watch.ipa')
os.makedirs(serve, exist_ok=True)
if os.path.exists(stage):
shutil.rmtree(stage)
os.makedirs(payload, exist_ok=True)
@@ -73,7 +77,8 @@ import os, shutil, zipfile
app = '/tmp/fuelboard-build-ipas/Build/Products/Release-iphoneos/FuelBoard.app'
stage = '/tmp/fuelboard-ipa-stage-sideload'
payload = os.path.join(stage, 'Payload')
out = '/tmp/ipa-serve/FuelBoard-sideload.ipa'
serve = os.environ['SERVE_DIR']
out = os.path.join(serve, 'FuelBoard-sideload.ipa')
if os.path.exists(stage):
shutil.rmtree(stage)
os.makedirs(payload, exist_ok=True)
@@ -117,6 +122,7 @@ for name, path in paths.items():
PY
echo "==> URLs"
echo "Serving: ipa-serve # in $(basename "$PWD"), or: ipa-serve fuelboard"
echo "Watch-capable IPA: http://$HOST:$PORT/FuelBoard-watch.ipa"
echo "Sideload-safe IPA: http://$HOST:$PORT/FuelBoard-sideload.ipa"
echo "Xcode log: /tmp/fuelboard-build-ipas-xcodebuild.log"
+73
View File
@@ -0,0 +1,73 @@
#!/bin/bash
# install.sh — build FuelBoard dev-signed and install straight to a paired iPhone.
#
# Streamlined flow (single command):
# ./scripts/install.sh # build + install + launch on the first paired iPhone
# ./scripts/install.sh --device UDID # target a specific device (xcrun devicectl list devices)
# ./scripts/install.sh --no-launch # install but don't auto-launch
#
# Why this exists: sideloading (SideStore/AltStore) got replaced on 2026-09-25 by a
# proper dev-signed install via devicectl — the phone is paired, so no LAN server,
# no delete/reinstall dance, and the installed app is a real signed build. The
# Release config targets the device (not the simulator) and reuses Xcode automatic
# signing with the project's DEVELOPMENT_TEAM.
#
# Requires: a paired + unlocked iPhone (xcrun devicectl list devices), and the
# Apple Development identity in the keychain.
set -euo pipefail
cd "$(dirname "$0")/.."
PROJECT="$PWD/FuelBoard.xcodeproj"
SCHEME="FuelBoard"
DERIVED_DATA="/tmp/fuelboard-devinstall"
APP="$DERIVED_DATA/Build/Products/Release-iphoneos/FuelBoard.app"
DEVICE=""
DEVICE_NAME="(first paired iPhone)"
LAUNCH=1
while [[ $# -gt 0 ]]; do
case "$1" in
--device) DEVICE="$2"; shift 2 ;;
--no-launch) LAUNCH=0; shift ;;
*) echo "unknown arg: $1" >&2; exit 1 ;;
esac
done
# Resolve the device if not given: the first connected physical iPhone.
# devicectl --device wants the CoreDevice Identifier (field 3 of `list devices`,
# e.g. C519BE18-...). NOTE: xcodebuild -destination wants the hardware UDID
# (00008130-...), so we build with the generic platform destination and only use
# the CoreDevice Identifier for devicectl install/launch.
if [[ -z "$DEVICE" ]]; then
DEVICE="$(xcrun devicectl list devices 2>/dev/null | awk '/(available \(paired\))/ && /iPhone/ {print $3; exit}')"
if [[ -z "$DEVICE" ]]; then
echo "install.sh: no paired iPhone found. Check: xcrun devicectl list devices" >&2
exit 1
fi
fi
echo "==> Build (Release, dev-signed, generic iOS destination)"
rm -rf "$DERIVED_DATA"
xcodebuild -project "$PROJECT" -scheme "$SCHEME" -configuration Release \
-destination 'generic/platform=iOS' \
-derivedDataPath "$DERIVED_DATA" \
CODE_SIGN_STYLE=Automatic \
build 2>&1 | grep -E "error:|BUILD (SUCCEEDED|FAILED)|Signing Identity" | tail -8
echo "==> Verify bundle"
test -d "$APP" || { echo "build failed: no app at $APP" >&2; exit 1; }
echo " bundle: $(plutil -extract CFBundleIdentifier raw "$APP/Info.plist")" \
" $(plutil -extract CFBundleShortVersionString raw "$APP/Info.plist")" \
"($(plutil -extract CFBundleVersion raw "$APP/Info.plist"))"
echo "==> Install via devicectl"
xcrun devicectl device install app --device "$DEVICE" "$APP" 2>&1 | tail -6
if [[ "$LAUNCH" -eq 1 ]]; then
BID="$(plutil -extract CFBundleIdentifier raw "$APP/Info.plist")"
echo "==> Launch"
xcrun devicectl device process launch --device "$DEVICE" "$BID" 2>&1 | tail -4
fi
echo "==> Done ✅ (delete the old icon on the phone if you want it refreshed)"