diff --git a/Configuration/Firebase/Example/GoogleService-Info.plist b/Configuration/Firebase/Example/GoogleService-Info.plist
deleted file mode 100644
index f371a38b..00000000
--- a/Configuration/Firebase/Example/GoogleService-Info.plist
+++ /dev/null
@@ -1,30 +0,0 @@
-
-
-
-
- API_KEY
-
- GCM_SENDER_ID
-
- PLIST_VERSION
- 1
- BUNDLE_ID
- UNCONFIGURED
- PROJECT_ID
-
- STORAGE_BUCKET
-
- IS_ADS_ENABLED
-
- IS_ANALYTICS_ENABLED
-
- IS_APPINVITE_ENABLED
-
- IS_GCM_ENABLED
-
- IS_SIGNIN_ENABLED
-
- GOOGLE_APP_ID
-
-
-
diff --git a/Configuration/README.md b/Configuration/README.md
index d0af99ce..502b9e52 100644
--- a/Configuration/README.md
+++ b/Configuration/README.md
@@ -2,9 +2,9 @@
The repository does not contain Firebase project configuration or RevenueCat SDK
keys. Both app targets use `Services.xcconfig` for Debug and Release builds.
-Without local overrides, Firebase stays unconfigured, Crashlytics symbol upload
-is disabled, and RevenueCat purchases are unavailable. This does not bypass Pro
-feature access checks.
+Without a local Firebase plist or RevenueCat key, the corresponding service stays
+unconfigured. Crashlytics symbol upload is disabled by default. This does not
+bypass Pro feature access checks.
## Configure your own services
@@ -13,17 +13,17 @@ feature access checks.
2. Set `REVENUECAT_API_KEY` to your RevenueCat public Apple SDK key and set
`REVENUECAT_ENTITLEMENT_ID` to the entitlement configured in your project.
Never put a private RevenueCat management key in an app build.
-3. Download your own Firebase Apple app's `GoogleService-Info.plist` to
- `Configuration/Firebase/GoogleService-Info.plist`. Its bundle ID must match the
- app being built. Uncomment `FIREBASE_CONFIG_PATH` in the local xcconfig.
+3. Create `Configuration/Firebase/` and download your own Firebase Apple app's
+ `GoogleService-Info.plist` there. Its bundle ID must match the app being built.
4. To upload Crashlytics symbols from Release builds, also uncomment
`FIREBASE_UPLOAD_SYMBOLS = YES`. Runtime diagnostics still require user consent.
-The local xcconfig and Firebase plist are ignored by Git. Keep the tracked
-Firebase example unconfigured: its intentionally mismatched bundle ID prevents
-SDK initialization. CI can provision the same ignored files from its secret
-store, or pass build-setting overrides to `xcodebuild`. Never publish those files
-or build logs containing their values.
+The local xcconfig and Firebase plist are ignored by Git. There is no Firebase
+example plist: the build copies the real file only when it exists, and removes a
+previously bundled copy when it does not. A missing plist also skips symbol upload.
+CI can provision the same ignored files from its secret store, or pass RevenueCat
+and symbol-upload build-setting overrides to `xcodebuild`. Never publish those
+files or build logs containing their values.
Firebase client configuration and RevenueCat public SDK keys are shipped in app
binaries; they are not server-side secrets. Keeping them out of this repository
diff --git a/Configuration/Services.local.xcconfig.example b/Configuration/Services.local.xcconfig.example
index ea3c1d30..d4df549e 100644
--- a/Configuration/Services.local.xcconfig.example
+++ b/Configuration/Services.local.xcconfig.example
@@ -3,6 +3,5 @@
REVENUECAT_API_KEY =
REVENUECAT_ENTITLEMENT_ID = termind_pro
-// Download your own Firebase Apple app configuration before enabling these.
-// FIREBASE_CONFIG_PATH = Configuration/Firebase/GoogleService-Info.plist
+// Download your own Firebase Apple app configuration before enabling uploads.
// FIREBASE_UPLOAD_SYMBOLS = YES
diff --git a/Configuration/Services.xcconfig b/Configuration/Services.xcconfig
index 386ffac1..2fafde5b 100644
--- a/Configuration/Services.xcconfig
+++ b/Configuration/Services.xcconfig
@@ -1,7 +1,9 @@
// Public builds have no service credentials. Local overrides are never tracked.
REVENUECAT_API_KEY =
REVENUECAT_ENTITLEMENT_ID = termind_pro
-FIREBASE_CONFIG_PATH = Configuration/Firebase/Example/GoogleService-Info.plist
FIREBASE_UPLOAD_SYMBOLS = NO
+// Let sandboxed scripts read the optional plist through their directory input.
+USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES
+
#include? "Services.local.xcconfig"
diff --git a/Scripts/Tests/test_firebase_configuration.py b/Scripts/Tests/test_firebase_configuration.py
new file mode 100644
index 00000000..9f4390e0
--- /dev/null
+++ b/Scripts/Tests/test_firebase_configuration.py
@@ -0,0 +1,83 @@
+import os
+from pathlib import Path
+import plistlib
+import subprocess
+import tempfile
+import unittest
+
+
+SCRIPT = Path(__file__).resolve().parents[1] / "configure-firebase.sh"
+
+
+class FirebaseConfigurationTests(unittest.TestCase):
+ def setUp(self):
+ self.temporary_directory = tempfile.TemporaryDirectory()
+ self.addCleanup(self.temporary_directory.cleanup)
+ self.root = Path(self.temporary_directory.name)
+ self.source = self.root / "Configuration/Firebase/GoogleService-Info.plist"
+ self.bundled = self.root / "Products/Termind.app/Contents/Resources/GoogleService-Info.plist"
+ self.upload_marker = self.root / "upload-arguments"
+ uploader = self.root / "SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run"
+ uploader.parent.mkdir(parents=True)
+ uploader.write_text('#!/bin/sh\nprintf "%s\\n" "$@" > "$UPLOAD_MARKER"\n')
+ uploader.chmod(0o700)
+ self.environment = {
+ **os.environ,
+ "SRCROOT": str(self.root),
+ "TARGET_BUILD_DIR": str(self.root / "Products"),
+ "UNLOCALIZED_RESOURCES_FOLDER_PATH": "Termind.app/Contents/Resources",
+ "BUILD_DIR": str(self.root / "Build/Products"),
+ "UPLOAD_MARKER": str(self.upload_marker),
+ }
+
+ def write_configuration(self):
+ self.source.parent.mkdir(parents=True)
+ self.source.write_bytes(plistlib.dumps({"BUNDLE_ID": "com.example.test"}))
+
+ def run_phase(self, configuration="Debug", upload="NO"):
+ subprocess.run(
+ ["/bin/sh", str(SCRIPT)],
+ env={
+ **self.environment,
+ "CONFIGURATION": configuration,
+ "FIREBASE_UPLOAD_SYMBOLS": upload,
+ },
+ check=True,
+ capture_output=True,
+ text=True,
+ )
+
+ def test_missing_configuration_neither_creates_a_plist_nor_uploads(self):
+ self.run_phase(configuration="Release", upload="YES")
+ self.assertFalse(self.bundled.exists())
+ self.assertFalse(self.upload_marker.exists())
+
+ def test_local_configuration_is_copied_without_uploading_by_default(self):
+ self.write_configuration()
+ self.run_phase(configuration="Release")
+ self.assertEqual(self.bundled.read_bytes(), self.source.read_bytes())
+ self.assertFalse(self.upload_marker.exists())
+
+ def test_debug_never_uploads_even_when_enabled(self):
+ self.write_configuration()
+ self.run_phase(upload="YES")
+ self.assertTrue(self.bundled.exists())
+ self.assertFalse(self.upload_marker.exists())
+
+ def test_release_uploads_the_bundled_configuration_only_when_enabled(self):
+ self.write_configuration()
+ self.run_phase(configuration="Release", upload="YES")
+ self.assertEqual(self.upload_marker.read_text().splitlines(), ["-gsp", str(self.bundled)])
+
+ def test_removing_local_configuration_clears_a_previous_build(self):
+ self.write_configuration()
+ self.run_phase()
+ self.assertTrue(self.bundled.exists())
+ self.source.unlink()
+ self.run_phase(configuration="Release", upload="YES")
+ self.assertFalse(self.bundled.exists())
+ self.assertFalse(self.upload_marker.exists())
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/Scripts/configure-firebase.sh b/Scripts/configure-firebase.sh
new file mode 100644
index 00000000..dae66e5f
--- /dev/null
+++ b/Scripts/configure-firebase.sh
@@ -0,0 +1,18 @@
+#!/bin/sh
+set -eu
+
+source_plist="${SRCROOT}/Configuration/Firebase/GoogleService-Info.plist"
+bundled_plist="${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleService-Info.plist"
+
+if [ ! -f "${source_plist}" ]; then
+ # An incremental build must not retain another developer's service configuration.
+ rm -f "${bundled_plist}"
+ exit 0
+fi
+
+mkdir -p "$(dirname "${bundled_plist}")"
+cp "${source_plist}" "${bundled_plist}"
+
+if [ "${CONFIGURATION}" = "Release" ] && [ "${FIREBASE_UPLOAD_SYMBOLS:-NO}" = "YES" ]; then
+ "${BUILD_DIR%/Build/*}/SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run" -gsp "${bundled_plist}"
+fi
diff --git a/TFMTests/ArchitectureBoundaryTests.swift b/TFMTests/ArchitectureBoundaryTests.swift
index 69d6fbc7..1c091ae5 100644
--- a/TFMTests/ArchitectureBoundaryTests.swift
+++ b/TFMTests/ArchitectureBoundaryTests.swift
@@ -66,42 +66,6 @@ struct ArchitectureBoundaryTests {
try assertNoImports(in: "../Frameworks/TermindInfrastructure/Commerce", forbidden: ["StoreKit"])
}
- @Test
- func serviceCredentialsStayOutsideSharedBuildSettings() throws {
- let repositoryRoot = sourceRoot.deletingLastPathComponent().standardizedFileURL
- let projectData = try Data(contentsOf: repositoryRoot.appending(path: "Termind.xcodeproj/project.pbxproj"))
- let project = try #require(
- PropertyListSerialization.propertyList(from: projectData, format: nil) as? [String: Any])
- let objects = try #require(project["objects"] as? [String: [String: Any]])
- let rootID = try #require(project["rootObject"] as? String)
- let configurationListID = try #require(objects[rootID]?["buildConfigurationList"] as? String)
- let configurationIDs = try #require(objects[configurationListID]?["buildConfigurations"] as? [String])
- for id in configurationIDs {
- let configuration = try #require(objects[id])
- let reference = try #require(configuration["baseConfigurationReference"] as? String)
- #expect(objects[reference]?["path"] as? String == "Configuration/Services.xcconfig")
- }
- for object in objects.values where object["isa"] as? String == "XCBuildConfiguration" {
- let settings = try #require(object["buildSettings"] as? [String: Any])
- #expect(settings["REVENUECAT_API_KEY"] == nil)
- }
-
- let defaults = try String(
- contentsOf: repositoryRoot.appending(path: "Configuration/Services.xcconfig"), encoding: .utf8)
- #expect(defaults.contains("REVENUECAT_API_KEY =\n"))
- #expect(defaults.contains("FIREBASE_UPLOAD_SYMBOLS = NO\n"))
- #expect(defaults.contains("#include? \"Services.local.xcconfig\""))
-
- let templateData = try Data(
- contentsOf: repositoryRoot.appending(path: "Configuration/Firebase/Example/GoogleService-Info.plist"))
- let template = try #require(
- PropertyListSerialization.propertyList(from: templateData, format: nil) as? [String: Any])
- for field in ["API_KEY", "GCM_SENDER_ID", "PROJECT_ID", "STORAGE_BUCKET", "GOOGLE_APP_ID"] {
- #expect((template[field] as? String)?.isEmpty == true)
- }
- #expect(template["BUNDLE_ID"] as? String == "UNCONFIGURED")
- }
-
@Test
func diagnosisSDKStaysBehindInfrastructureAndStartsWithCollectionDisabled() throws {
for module in ["TermindDomain", "TermindApplication", "TermindPresentation", "TermindPersistence"] {
diff --git a/Termind.xcodeproj/project.pbxproj b/Termind.xcodeproj/project.pbxproj
index 485442ad..e5286d60 100644
--- a/Termind.xcodeproj/project.pbxproj
+++ b/Termind.xcodeproj/project.pbxproj
@@ -100,8 +100,6 @@
F1BA00000000000000000302 /* FirebaseCrashlytics in Frameworks */ = {isa = PBXBuildFile; productRef = F1BA00000000000000000002 /* FirebaseCrashlytics */; };
F1BA00000000000000000401 /* FirebaseCore in Frameworks */ = {isa = PBXBuildFile; productRef = F1BA00000000000000000001 /* FirebaseCore */; };
F1BA00000000000000000402 /* FirebaseCrashlytics in Frameworks */ = {isa = PBXBuildFile; productRef = F1BA00000000000000000002 /* FirebaseCrashlytics */; };
- F1BA0000000000000000A001 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = F1BA00000000000000000020 /* GoogleService-Info.plist */; };
- F1BA0000000000000000A002 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = F1BA00000000000000000020 /* GoogleService-Info.plist */; };
F6ACDF3012081771F6C8F659 /* AgentKit in Frameworks */ = {isa = PBXBuildFile; productRef = TRMD0000000000000010001B /* AgentKit */; };
F996BFD771BC77E67AB91FFB /* TermindInfrastructure.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 67B48098792E58C2955768F2 /* TermindInfrastructure.framework */; };
FACB579B671967F922A33CD9 /* TermindApplication.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 10111AE8F047FB59B318813A /* TermindApplication.framework */; };
@@ -493,7 +491,6 @@
DBE378B130478CFB00B16AA1 /* Termind.storekit */ = {isa = PBXFileReference; lastKnownFileType = text; name = Termind.storekit; path = StoreKit/Termind.storekit; sourceTree = ""; };
F04700000000000000000011 /* TFMTests/SSHForwardingPumpTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TFMTests/SSHForwardingPumpTests.swift; sourceTree = SOURCE_ROOT; };
F04700000000000000000012 /* TFMTests/SSHForwardingWireTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TFMTests/SSHForwardingWireTests.swift; sourceTree = SOURCE_ROOT; };
- F1BA00000000000000000020 /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "$(FIREBASE_CONFIG_PATH)"; sourceTree = SOURCE_ROOT; };
F1BA00000000000000000021 /* Services.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Configuration/Services.xcconfig; sourceTree = SOURCE_ROOT; };
TRMD00000000000000000001 /* Termind.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Termind.app; sourceTree = BUILT_PRODUCTS_DIR; };
TRMD00000000000000020001 /* TFMTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TFMTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -858,7 +855,6 @@
TRMD00000000000000000030 = {
isa = PBXGroup;
children = (
- F1BA00000000000000000020 /* GoogleService-Info.plist */,
F1BA00000000000000000021 /* Services.xcconfig */,
BENC00000000000000000010 /* Benchmarks/PortForwarding */,
CB622EAF077B1253532F1E18 /* TFIWidgets */,
@@ -908,7 +904,7 @@
AC89B83FA8E4154383D9AC81 /* Sources */,
7B09CFC5B32584951BD4C8F9 /* Frameworks */,
8F0C5245592A0E2AFB85E50F /* Resources */,
- F1BA00000000000000003002 /* Upload Crashlytics Symbols */,
+ F1BA00000000000000003002 /* Configure Firebase */,
);
buildRules = (
);
@@ -1236,7 +1232,7 @@
TRMD00000000000000000020 /* Frameworks */,
TRMD00000000000000000080 /* Resources */,
TRMD00000000000000000085 /* Embed MCP Bridge */,
- F1BA00000000000000002002 /* Upload Crashlytics Symbols */,
+ F1BA00000000000000002002 /* Configure Firebase */,
);
buildRules = (
);
@@ -1482,7 +1478,6 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
- F1BA0000000000000000A002 /* GoogleService-Info.plist in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1511,7 +1506,6 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
- F1BA0000000000000000A001 /* GoogleService-Info.plist in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1525,7 +1519,7 @@
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
- F1BA00000000000000002002 /* Upload Crashlytics Symbols */ = {
+ F1BA00000000000000002002 /* Configure Firebase */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
@@ -1537,18 +1531,20 @@
"$(DWARF_DSYM_FOLDER_PATH)/$(DWARF_DSYM_FILE_NAME)",
"$(DWARF_DSYM_FOLDER_PATH)/$(DWARF_DSYM_FILE_NAME)/Contents/Info.plist",
"$(DWARF_DSYM_FOLDER_PATH)/$(DWARF_DSYM_FILE_NAME)/Contents/Resources/DWARF/$(PRODUCT_NAME)",
- "$(TARGET_BUILD_DIR)/$(UNLOCALIZED_RESOURCES_FOLDER_PATH)/GoogleService-Info.plist",
+ "$(SRCROOT)/Configuration/",
+ "$(SRCROOT)/Scripts/configure-firebase.sh",
"$(TARGET_BUILD_DIR)/$(EXECUTABLE_PATH)",
"$(TARGET_BUILD_DIR)/$(INFOPLIST_PATH)",
);
- name = "Upload Crashlytics Symbols";
+ name = "Configure Firebase";
outputPaths = (
+ "$(TARGET_BUILD_DIR)/$(UNLOCALIZED_RESOURCES_FOLDER_PATH)/GoogleService-Info.plist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
- shellScript = "if [ \"${CONFIGURATION}\" = \"Release\" ] && [ \"${FIREBASE_UPLOAD_SYMBOLS}\" = \"YES\" ]; then\n \"${BUILD_DIR%/Build/*}/SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run\" -gsp \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleService-Info.plist\"\nfi\n";
+ shellScript = "/bin/sh \"${SRCROOT}/Scripts/configure-firebase.sh\"\n";
};
- F1BA00000000000000003002 /* Upload Crashlytics Symbols */ = {
+ F1BA00000000000000003002 /* Configure Firebase */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
@@ -1560,16 +1556,18 @@
"$(DWARF_DSYM_FOLDER_PATH)/$(DWARF_DSYM_FILE_NAME)",
"$(DWARF_DSYM_FOLDER_PATH)/$(DWARF_DSYM_FILE_NAME)/Contents/Info.plist",
"$(DWARF_DSYM_FOLDER_PATH)/$(DWARF_DSYM_FILE_NAME)/Contents/Resources/DWARF/$(PRODUCT_NAME)",
- "$(TARGET_BUILD_DIR)/$(UNLOCALIZED_RESOURCES_FOLDER_PATH)/GoogleService-Info.plist",
+ "$(SRCROOT)/Configuration/",
+ "$(SRCROOT)/Scripts/configure-firebase.sh",
"$(TARGET_BUILD_DIR)/$(EXECUTABLE_PATH)",
"$(TARGET_BUILD_DIR)/$(INFOPLIST_PATH)",
);
- name = "Upload Crashlytics Symbols";
+ name = "Configure Firebase";
outputPaths = (
+ "$(TARGET_BUILD_DIR)/$(UNLOCALIZED_RESOURCES_FOLDER_PATH)/GoogleService-Info.plist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
- shellScript = "if [ \"${CONFIGURATION}\" = \"Release\" ] && [ \"${FIREBASE_UPLOAD_SYMBOLS}\" = \"YES\" ]; then\n \"${BUILD_DIR%/Build/*}/SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run\" -gsp \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleService-Info.plist\"\nfi\n";
+ shellScript = "/bin/sh \"${SRCROOT}/Scripts/configure-firebase.sh\"\n";
};
/* End PBXShellScriptBuildPhase section */