From e4af202b6cec3d1e8583f111c204ccdf4027c8bd Mon Sep 17 00:00:00 2001 From: Frederik St-Onge Date: Tue, 11 Nov 2025 07:42:06 -0500 Subject: [PATCH 01/19] Add display name parameter --- .../flutterfire_cli/lib/src/commands/config.dart | 13 +++++++++++++ packages/flutterfire_cli/lib/src/common/utils.dart | 1 + .../lib/src/firebase/firebase_android_options.dart | 3 ++- .../lib/src/firebase/firebase_apple_options.dart | 3 ++- .../lib/src/firebase/firebase_dart_options.dart | 3 ++- .../lib/src/firebase/firebase_platform_options.dart | 7 +++++++ 6 files changed, 27 insertions(+), 3 deletions(-) diff --git a/packages/flutterfire_cli/lib/src/commands/config.dart b/packages/flutterfire_cli/lib/src/commands/config.dart index 5b2b5d19..428b3b0e 100644 --- a/packages/flutterfire_cli/lib/src/commands/config.dart +++ b/packages/flutterfire_cli/lib/src/commands/config.dart @@ -62,6 +62,14 @@ class ConfigCommand extends FlutterFireCommand { 'Optionally specify the platforms to generate configuration options for ' 'as a comma separated list. For example "android,ios,macos,web,linux,windows".', ); + argParser.addOption( + kDisplayNameFlag, + valueHelp: 'displayName', + abbr: 'n', + help: 'The display name of your app, e.g. "My Cool App". ' + 'If no display name is provided then an attempt will be made to ' + 'automatically detect it from your project configuration (if it exists).', + ); argParser.addOption( kIosBundleIdFlag, valueHelp: 'bundleIdentifier', @@ -243,6 +251,10 @@ class ConfigCommand extends FlutterFireCommand { .toList(); } + String? get appDisplayName { + return argResults![kDisplayNameFlag] as String?; + } + bool get applyGradlePlugins { return argResults!['apply-gradle-plugins'] as bool; } @@ -625,6 +637,7 @@ class ConfigCommand extends FlutterFireCommand { iosBundleId: iosBundleId, macosBundleId: macosBundleId, token: token, + appDisplayName: appDisplayName, serviceAccount: serviceAccount, webAppId: webAppId, windowsAppId: windowsAppId, diff --git a/packages/flutterfire_cli/lib/src/common/utils.dart b/packages/flutterfire_cli/lib/src/common/utils.dart index b99caf47..169aacab 100644 --- a/packages/flutterfire_cli/lib/src/common/utils.dart +++ b/packages/flutterfire_cli/lib/src/common/utils.dart @@ -63,6 +63,7 @@ const String kConfigurations = 'configurations'; const String kOutFlag = 'out'; const String kYesFlag = 'yes'; const String kPlatformsFlag = 'platforms'; +const String kDisplayNameFlag = 'display-name'; const String kIosBundleIdFlag = 'ios-bundle-id'; const String kMacosBundleIdFlag = 'macos-bundle-id'; const String kAndroidAppIdFlag = 'android-app-id'; diff --git a/packages/flutterfire_cli/lib/src/firebase/firebase_android_options.dart b/packages/flutterfire_cli/lib/src/firebase/firebase_android_options.dart index a66e7c99..a5a7098e 100644 --- a/packages/flutterfire_cli/lib/src/firebase/firebase_android_options.dart +++ b/packages/flutterfire_cli/lib/src/firebase/firebase_android_options.dart @@ -40,6 +40,7 @@ extension FirebaseAndroidOptions on FirebaseOptions { String? firebaseAccount, required String? token, required String? serviceAccount, + required String? displayName, }) async { var selectedAndroidApplicationId = androidApplicationId ?? flutterApp.androidApplicationId; @@ -56,7 +57,7 @@ extension FirebaseAndroidOptions on FirebaseOptions { ); final firebaseApp = await firebase.findOrCreateFirebaseApp( packageNameOrBundleIdentifier: selectedAndroidApplicationId, - displayName: flutterApp.package.pubSpec.name, + displayName: displayName ?? flutterApp.package.pubSpec.name, platform: kAndroid, project: firebaseProjectId, account: firebaseAccount, diff --git a/packages/flutterfire_cli/lib/src/firebase/firebase_apple_options.dart b/packages/flutterfire_cli/lib/src/firebase/firebase_apple_options.dart index 36debd88..070cdd19 100644 --- a/packages/flutterfire_cli/lib/src/firebase/firebase_apple_options.dart +++ b/packages/flutterfire_cli/lib/src/firebase/firebase_apple_options.dart @@ -33,6 +33,7 @@ extension FirebaseAppleOptions on FirebaseOptions { String? firebaseAccount, required String? token, required String? serviceAccount, + required String? displayName, }) async { final platformIdentifier = macos ? kMacos : kIos; var selectedAppleBundleId = appleBundleIdentifier ?? @@ -58,7 +59,7 @@ extension FirebaseAppleOptions on FirebaseOptions { ); final firebaseApp = await firebase.findOrCreateFirebaseApp( packageNameOrBundleIdentifier: selectedAppleBundleId, - displayName: flutterApp.package.pubSpec.name, + displayName: displayName ?? flutterApp.package.pubSpec.name, platform: platformIdentifier, project: firebaseProjectId, account: firebaseAccount, diff --git a/packages/flutterfire_cli/lib/src/firebase/firebase_dart_options.dart b/packages/flutterfire_cli/lib/src/firebase/firebase_dart_options.dart index edadd712..b563f017 100644 --- a/packages/flutterfire_cli/lib/src/firebase/firebase_dart_options.dart +++ b/packages/flutterfire_cli/lib/src/firebase/firebase_dart_options.dart @@ -32,9 +32,10 @@ extension FirebaseDartOptions on FirebaseOptions { String platform = kWeb, required String? token, required String? serviceAccount, + required String? displayName, }) async { final firebaseApp = await firebase.findOrCreateFirebaseApp( - displayName: flutterApp.package.pubSpec.name, + displayName: displayName ?? flutterApp.package.pubSpec.name, platform: platform, project: firebaseProjectId, account: firebaseAccount, diff --git a/packages/flutterfire_cli/lib/src/firebase/firebase_platform_options.dart b/packages/flutterfire_cli/lib/src/firebase/firebase_platform_options.dart index 1a9d50b4..6ba01752 100644 --- a/packages/flutterfire_cli/lib/src/firebase/firebase_platform_options.dart +++ b/packages/flutterfire_cli/lib/src/firebase/firebase_platform_options.dart @@ -40,6 +40,7 @@ Future fetchAllFirebaseOptions({ String? windowsAppId, String? token, String? serviceAccount, + required String? appDisplayName, }) async { FirebaseOptions? androidOptions; FirebaseOptions? iosOptions; @@ -56,6 +57,7 @@ Future fetchAllFirebaseOptions({ firebaseAccount: firebaseAccount, token: token, serviceAccount: serviceAccount, + displayName: appDisplayName, ); } @@ -67,6 +69,7 @@ Future fetchAllFirebaseOptions({ firebaseAccount: firebaseAccount, token: token, serviceAccount: serviceAccount, + displayName: appDisplayName, ); } if (macos) { @@ -78,6 +81,7 @@ Future fetchAllFirebaseOptions({ macos: true, token: token, serviceAccount: serviceAccount, + displayName: appDisplayName, ); } @@ -89,6 +93,7 @@ Future fetchAllFirebaseOptions({ webAppId: webAppId, token: token, serviceAccount: serviceAccount, + displayName: appDisplayName, ); } @@ -101,6 +106,7 @@ Future fetchAllFirebaseOptions({ token: token, webAppId: windowsAppId, serviceAccount: serviceAccount, + displayName: appDisplayName, ); } @@ -112,6 +118,7 @@ Future fetchAllFirebaseOptions({ platform: kLinux, token: token, serviceAccount: serviceAccount, + displayName: appDisplayName, ); } From 00cb73d736a7cc958547883fad9ba1c0f1a4d55c Mon Sep 17 00:00:00 2001 From: Frederik St-Onge Date: Tue, 11 Nov 2025 07:45:41 -0500 Subject: [PATCH 02/19] Add example in readme --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index c678276a..9e181def 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,11 @@ If you wish to be specific about which platforms you want to configure, use the flutterfire configure --yes --project= --platforms=android,ios,web ``` +If you want to specify a display name you want to configurem use the `--display-name` flag: +```bash +flutterfire configure --yes --project= --display-name=test +``` + FlutterFire CLI now supports Windows applications. The Firebase console does not allow you to create an application for Windows, FlutterFire CLI will create a **web app** if you specify windows as a platform you want to configure: ```bash From 664df7f233779d83b1e29009defc2de14bf314be Mon Sep 17 00:00:00 2001 From: Frederik St-Onge Date: Tue, 11 Nov 2025 07:51:11 -0500 Subject: [PATCH 03/19] Rename variable --- .../flutterfire_cli/lib/src/commands/config.dart | 4 ++-- .../src/firebase/firebase_platform_options.dart | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/flutterfire_cli/lib/src/commands/config.dart b/packages/flutterfire_cli/lib/src/commands/config.dart index 428b3b0e..068b3786 100644 --- a/packages/flutterfire_cli/lib/src/commands/config.dart +++ b/packages/flutterfire_cli/lib/src/commands/config.dart @@ -251,7 +251,7 @@ class ConfigCommand extends FlutterFireCommand { .toList(); } - String? get appDisplayName { + String? get displayName { return argResults![kDisplayNameFlag] as String?; } @@ -637,7 +637,7 @@ class ConfigCommand extends FlutterFireCommand { iosBundleId: iosBundleId, macosBundleId: macosBundleId, token: token, - appDisplayName: appDisplayName, + displayName: displayName, serviceAccount: serviceAccount, webAppId: webAppId, windowsAppId: windowsAppId, diff --git a/packages/flutterfire_cli/lib/src/firebase/firebase_platform_options.dart b/packages/flutterfire_cli/lib/src/firebase/firebase_platform_options.dart index 6ba01752..62dcdcec 100644 --- a/packages/flutterfire_cli/lib/src/firebase/firebase_platform_options.dart +++ b/packages/flutterfire_cli/lib/src/firebase/firebase_platform_options.dart @@ -40,7 +40,7 @@ Future fetchAllFirebaseOptions({ String? windowsAppId, String? token, String? serviceAccount, - required String? appDisplayName, + String? displayName, }) async { FirebaseOptions? androidOptions; FirebaseOptions? iosOptions; @@ -57,7 +57,7 @@ Future fetchAllFirebaseOptions({ firebaseAccount: firebaseAccount, token: token, serviceAccount: serviceAccount, - displayName: appDisplayName, + displayName: displayName, ); } @@ -69,7 +69,7 @@ Future fetchAllFirebaseOptions({ firebaseAccount: firebaseAccount, token: token, serviceAccount: serviceAccount, - displayName: appDisplayName, + displayName: displayName, ); } if (macos) { @@ -81,7 +81,7 @@ Future fetchAllFirebaseOptions({ macos: true, token: token, serviceAccount: serviceAccount, - displayName: appDisplayName, + displayName: displayName, ); } @@ -93,7 +93,7 @@ Future fetchAllFirebaseOptions({ webAppId: webAppId, token: token, serviceAccount: serviceAccount, - displayName: appDisplayName, + displayName: displayName, ); } @@ -106,7 +106,7 @@ Future fetchAllFirebaseOptions({ token: token, webAppId: windowsAppId, serviceAccount: serviceAccount, - displayName: appDisplayName, + displayName: displayName, ); } @@ -118,7 +118,7 @@ Future fetchAllFirebaseOptions({ platform: kLinux, token: token, serviceAccount: serviceAccount, - displayName: appDisplayName, + displayName: displayName, ); } From d4ffdbaf69c67d64c94cc1fa4041b085ed8037a7 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Thu, 27 Nov 2025 15:58:42 +0000 Subject: [PATCH 04/19] fix: set display name early --- packages/flutterfire_cli/lib/src/commands/config.dart | 5 +++-- .../lib/src/firebase/firebase_android_options.dart | 4 ++-- .../lib/src/firebase/firebase_apple_options.dart | 4 ++-- .../lib/src/firebase/firebase_dart_options.dart | 4 ++-- .../lib/src/firebase/firebase_platform_options.dart | 2 +- 5 files changed, 10 insertions(+), 9 deletions(-) diff --git a/packages/flutterfire_cli/lib/src/commands/config.dart b/packages/flutterfire_cli/lib/src/commands/config.dart index 068b3786..0c0330c2 100644 --- a/packages/flutterfire_cli/lib/src/commands/config.dart +++ b/packages/flutterfire_cli/lib/src/commands/config.dart @@ -251,8 +251,9 @@ class ConfigCommand extends FlutterFireCommand { .toList(); } - String? get displayName { - return argResults![kDisplayNameFlag] as String?; + String get displayName { + final flagValue = argResults![kDisplayNameFlag] as String?; + return flagValue ?? flutterApp!.package.pubSpec.name; } bool get applyGradlePlugins { diff --git a/packages/flutterfire_cli/lib/src/firebase/firebase_android_options.dart b/packages/flutterfire_cli/lib/src/firebase/firebase_android_options.dart index a5a7098e..aeadeb30 100644 --- a/packages/flutterfire_cli/lib/src/firebase/firebase_android_options.dart +++ b/packages/flutterfire_cli/lib/src/firebase/firebase_android_options.dart @@ -40,7 +40,7 @@ extension FirebaseAndroidOptions on FirebaseOptions { String? firebaseAccount, required String? token, required String? serviceAccount, - required String? displayName, + required String displayName, }) async { var selectedAndroidApplicationId = androidApplicationId ?? flutterApp.androidApplicationId; @@ -57,7 +57,7 @@ extension FirebaseAndroidOptions on FirebaseOptions { ); final firebaseApp = await firebase.findOrCreateFirebaseApp( packageNameOrBundleIdentifier: selectedAndroidApplicationId, - displayName: displayName ?? flutterApp.package.pubSpec.name, + displayName: displayName, platform: kAndroid, project: firebaseProjectId, account: firebaseAccount, diff --git a/packages/flutterfire_cli/lib/src/firebase/firebase_apple_options.dart b/packages/flutterfire_cli/lib/src/firebase/firebase_apple_options.dart index 070cdd19..d6fde9ce 100644 --- a/packages/flutterfire_cli/lib/src/firebase/firebase_apple_options.dart +++ b/packages/flutterfire_cli/lib/src/firebase/firebase_apple_options.dart @@ -33,7 +33,7 @@ extension FirebaseAppleOptions on FirebaseOptions { String? firebaseAccount, required String? token, required String? serviceAccount, - required String? displayName, + required String displayName, }) async { final platformIdentifier = macos ? kMacos : kIos; var selectedAppleBundleId = appleBundleIdentifier ?? @@ -59,7 +59,7 @@ extension FirebaseAppleOptions on FirebaseOptions { ); final firebaseApp = await firebase.findOrCreateFirebaseApp( packageNameOrBundleIdentifier: selectedAppleBundleId, - displayName: displayName ?? flutterApp.package.pubSpec.name, + displayName: displayName, platform: platformIdentifier, project: firebaseProjectId, account: firebaseAccount, diff --git a/packages/flutterfire_cli/lib/src/firebase/firebase_dart_options.dart b/packages/flutterfire_cli/lib/src/firebase/firebase_dart_options.dart index b563f017..3526e586 100644 --- a/packages/flutterfire_cli/lib/src/firebase/firebase_dart_options.dart +++ b/packages/flutterfire_cli/lib/src/firebase/firebase_dart_options.dart @@ -32,10 +32,10 @@ extension FirebaseDartOptions on FirebaseOptions { String platform = kWeb, required String? token, required String? serviceAccount, - required String? displayName, + required String displayName, }) async { final firebaseApp = await firebase.findOrCreateFirebaseApp( - displayName: displayName ?? flutterApp.package.pubSpec.name, + displayName: displayName, platform: platform, project: firebaseProjectId, account: firebaseAccount, diff --git a/packages/flutterfire_cli/lib/src/firebase/firebase_platform_options.dart b/packages/flutterfire_cli/lib/src/firebase/firebase_platform_options.dart index 62dcdcec..09d063c2 100644 --- a/packages/flutterfire_cli/lib/src/firebase/firebase_platform_options.dart +++ b/packages/flutterfire_cli/lib/src/firebase/firebase_platform_options.dart @@ -40,7 +40,7 @@ Future fetchAllFirebaseOptions({ String? windowsAppId, String? token, String? serviceAccount, - String? displayName, + required String displayName, }) async { FirebaseOptions? androidOptions; FirebaseOptions? iosOptions; From 0fb1d8c145c3f809b348ab1191478ec121e4b8e9 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Thu, 27 Nov 2025 16:00:28 +0000 Subject: [PATCH 05/19] test: add a couple of unit tests --- packages/flutterfire_cli/test/unit_test.dart | 44 ++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/packages/flutterfire_cli/test/unit_test.dart b/packages/flutterfire_cli/test/unit_test.dart index 292473d8..a804d6d2 100644 --- a/packages/flutterfire_cli/test/unit_test.dart +++ b/packages/flutterfire_cli/test/unit_test.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:flutterfire_cli/src/common/strings.dart'; import 'package:flutterfire_cli/src/common/utils.dart'; +import 'package:flutterfire_cli/src/firebase/firebase_app.dart'; import 'package:test/test.dart'; void main() { @@ -142,6 +143,49 @@ void main() { }); }); + group('FirebaseApp displayName handling', () { + test('FirebaseApp.fromJson correctly parses displayName from JSON', () { + final json = { + 'platform': 'ANDROID', + 'appId': '1:123456:android:abc', + 'displayName': 'My Cool App', + 'name': 'projects/test/apps/abc', + 'packageName': 'com.example.app', + }; + + final app = FirebaseApp.fromJson(json); + + expect(app.displayName, 'My Cool App'); + expect(app.name, 'projects/test/apps/abc'); + expect(app.platform, 'android'); + expect(app.packageNameOrBundleIdentifier, 'com.example.app'); + }); + + test('FirebaseApp.fromJson handles null or missing displayName', () { + // Test with null displayName + final jsonWithNull = { + 'platform': 'IOS', + 'appId': '1:123456:ios:xyz', + 'displayName': null, + 'name': 'projects/test/apps/xyz', + 'bundleId': 'com.example.app', + }; + + final appWithNull = FirebaseApp.fromJson(jsonWithNull); + expect(appWithNull.displayName, null); + + // Test with missing displayName field + final jsonWithoutField = { + 'platform': 'WEB', + 'appId': '1:123456:web:def', + 'name': 'projects/test/apps/def', + }; + + final appWithoutField = FirebaseApp.fromJson(jsonWithoutField); + expect(appWithoutField.displayName, null); + }); + }); + group( 'Firebase CLI JSON response parser function `firebaseCLIJsonParse()`', () { From e33dbcb96241ac006a8150e9801ca9456bb54cee Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Thu, 27 Nov 2025 16:01:31 +0000 Subject: [PATCH 06/19] chore: update naming --- packages/flutterfire_cli/lib/src/commands/config.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/flutterfire_cli/lib/src/commands/config.dart b/packages/flutterfire_cli/lib/src/commands/config.dart index 0c0330c2..92d3078b 100644 --- a/packages/flutterfire_cli/lib/src/commands/config.dart +++ b/packages/flutterfire_cli/lib/src/commands/config.dart @@ -252,8 +252,8 @@ class ConfigCommand extends FlutterFireCommand { } String get displayName { - final flagValue = argResults![kDisplayNameFlag] as String?; - return flagValue ?? flutterApp!.package.pubSpec.name; + final name = argResults![kDisplayNameFlag] as String?; + return name ?? flutterApp!.package.pubSpec.name; } bool get applyGradlePlugins { From f8841b616d1591fb91411b7753ac62d719a1b946 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Thu, 27 Nov 2025 16:03:29 +0000 Subject: [PATCH 07/19] docs: update wording for display-name flag --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9e181def..68d00b64 100644 --- a/README.md +++ b/README.md @@ -120,7 +120,7 @@ If you wish to be specific about which platforms you want to configure, use the flutterfire configure --yes --project= --platforms=android,ios,web ``` -If you want to specify a display name you want to configurem use the `--display-name` flag: +If you want to specify a display name (i.e. how it appears in the Firebase console), use the `--display-name` flag: ```bash flutterfire configure --yes --project= --display-name=test ``` From a46adf21aa1ecc9093d795fa024e9cce2fd179cf Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Thu, 27 Nov 2025 16:06:43 +0000 Subject: [PATCH 08/19] docs: update with further info on display flag --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 68d00b64..e0193612 100644 --- a/README.md +++ b/README.md @@ -120,7 +120,7 @@ If you wish to be specific about which platforms you want to configure, use the flutterfire configure --yes --project= --platforms=android,ios,web ``` -If you want to specify a display name (i.e. how it appears in the Firebase console), use the `--display-name` flag: +If you want to specify a display name (i.e. how it appears in the Firebase console), use the `--display-name` flag. It will still have the platform as a suffix (e.g. `--display-name=test` will appear as "test (ios), "test (web)", etc"): ```bash flutterfire configure --yes --project= --display-name=test ``` From 950514262690feba9953f393957ef7ba2c47718d Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Tue, 16 Dec 2025 15:10:50 +0000 Subject: [PATCH 09/19] test: ensure deployment target is 15 for iOS --- packages/flutterfire_cli/test/test_utils.dart | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/packages/flutterfire_cli/test/test_utils.dart b/packages/flutterfire_cli/test/test_utils.dart index 606b7891..0df1ce07 100644 --- a/packages/flutterfire_cli/test/test_utils.dart +++ b/packages/flutterfire_cli/test/test_utils.dart @@ -55,6 +55,26 @@ Future createFlutterProject() async { final flutterProjectPath = p.join(tempDir.path, flutterProject); + // Set iOS minimum deployment target to 15.0 + const iosVersion = '15.0'; + final pbxprojPath = + p.join(flutterProjectPath, 'ios', 'Runner.xcodeproj', 'project.pbxproj'); + final pbxprojResult = await Process.run( + 'sed', + [ + '-i', + '', + 's/IPHONEOS_DEPLOYMENT_TARGET = [0-9.]*;/IPHONEOS_DEPLOYMENT_TARGET = $iosVersion;/', + pbxprojPath, + ], + runInShell: true, + ); + + if (pbxprojResult.exitCode != 0) { + throw Exception( + 'Failed to set iOS deployment target: ${pbxprojResult.stderr}'); + } + return flutterProjectPath; } From 4775938a256264968437c2fd2a62816a9fe91cfd Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Tue, 16 Dec 2025 15:16:37 +0000 Subject: [PATCH 10/19] chore: format --- packages/flutterfire_cli/test/test_utils.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/flutterfire_cli/test/test_utils.dart b/packages/flutterfire_cli/test/test_utils.dart index 0df1ce07..65d62663 100644 --- a/packages/flutterfire_cli/test/test_utils.dart +++ b/packages/flutterfire_cli/test/test_utils.dart @@ -72,7 +72,8 @@ Future createFlutterProject() async { if (pbxprojResult.exitCode != 0) { throw Exception( - 'Failed to set iOS deployment target: ${pbxprojResult.stderr}'); + 'Failed to set iOS deployment target: ${pbxprojResult.stderr}', + ); } return flutterProjectPath; From eddddc56cb13a24f71c12c8d2e336ed031175bcb Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Tue, 16 Dec 2025 17:29:05 +0000 Subject: [PATCH 11/19] fix: deployment script --- packages/flutterfire_cli/test/test_utils.dart | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/packages/flutterfire_cli/test/test_utils.dart b/packages/flutterfire_cli/test/test_utils.dart index 65d62663..5a31e9f6 100644 --- a/packages/flutterfire_cli/test/test_utils.dart +++ b/packages/flutterfire_cli/test/test_utils.dart @@ -57,17 +57,17 @@ Future createFlutterProject() async { // Set iOS minimum deployment target to 15.0 const iosVersion = '15.0'; - final pbxprojPath = - p.join(flutterProjectPath, 'ios', 'Runner.xcodeproj', 'project.pbxproj'); + + // Update project.pbxproj final pbxprojResult = await Process.run( 'sed', [ '-i', '', 's/IPHONEOS_DEPLOYMENT_TARGET = [0-9.]*;/IPHONEOS_DEPLOYMENT_TARGET = $iosVersion;/', - pbxprojPath, + 'ios/Runner.xcodeproj/project.pbxproj', ], - runInShell: true, + workingDirectory: flutterProjectPath, ); if (pbxprojResult.exitCode != 0) { @@ -76,6 +76,18 @@ Future createFlutterProject() async { ); } + // Update Podfile if it exists + final podfilePath = p.join(flutterProjectPath, 'ios', 'Podfile'); + final podfile = File(podfilePath); + if (podfile.existsSync()) { + var podfileContent = podfile.readAsStringSync(); + podfileContent = podfileContent.replaceFirst( + RegExp("platform :ios, '[0-9.]+'"), + "platform :ios, '$iosVersion'", + ); + podfile.writeAsStringSync(podfileContent); + } + return flutterProjectPath; } From afc339f25d797592f0cfcd61b9887bca4c38b1de Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Wed, 17 Dec 2025 10:37:27 +0000 Subject: [PATCH 12/19] test: separate tests that need ios deployment target updating --- .../flutterfire_cli/test/configure_test.dart | 2772 +++++++++-------- packages/flutterfire_cli/test/test_utils.dart | 62 +- 2 files changed, 1420 insertions(+), 1414 deletions(-) diff --git a/packages/flutterfire_cli/test/configure_test.dart b/packages/flutterfire_cli/test/configure_test.dart index ba5cd9e5..ae75057c 100644 --- a/packages/flutterfire_cli/test/configure_test.dart +++ b/packages/flutterfire_cli/test/configure_test.dart @@ -9,1654 +9,1658 @@ import 'package:test/test.dart'; import 'test_utils.dart'; void main() { - String? projectPath; - setUp(() async { - projectPath = await createFlutterProject(); - }); - - tearDown(() { - Directory(p.dirname(projectPath!)).delete(recursive: true); - }); + // Default group for tests that don't need iOS 15.0 + group('FlutterFire configure tests', () { + String? projectPath; + setUp(() async { + projectPath = await createFlutterProject(); + }); + + tearDown(() { + Directory(p.dirname(projectPath!)).delete(recursive: true); + }); + + test( + 'flutterfire configure: android - "default" Apple - "default"', + () async { + // the most basic 'flutterfire configure' command that can be run without command line prompts + const defaultTarget = 'Runner'; + final result = Process.runSync( + 'flutterfire', + [ + 'configure', + '--yes', + '--platforms=android,ios,macos,web,windows', + '--web-app-id=$webAppId', + '--windows-app-id=$windowsAppId', + '--project=$firebaseProjectId', + ], + workingDirectory: projectPath, + runInShell: true, + ); - test( - 'flutterfire configure: android - "default" Apple - "default"', - () async { - // the most basic 'flutterfire configure' command that can be run without command line prompts - const defaultTarget = 'Runner'; - final result = Process.runSync( - 'flutterfire', - [ - 'configure', - '--yes', - '--platforms=android,ios,macos,web,windows', - '--web-app-id=$webAppId', - '--windows-app-id=$windowsAppId', - '--project=$firebaseProjectId', - ], - workingDirectory: projectPath, - runInShell: true, - ); + if (result.exitCode != 0) { + fail(result.stderr as String); + } - if (result.exitCode != 0) { - fail(result.stderr as String); - } + if (Platform.isMacOS) { + // check Apple service files were created and have correct content + final iosPath = + p.join(projectPath!, kIos, defaultTarget, appleServiceFileName); + final macosPath = p.join(projectPath!, kMacos, defaultTarget); + + await testAppleServiceFileValues(iosPath); + await testAppleServiceFileValues( + macosPath, + platform: kMacos, + ); + + // check default "firebase.json" was created and has correct content + final firebaseJsonFile = p.join(projectPath!, 'firebase.json'); + final firebaseJsonFileContent = + await File(firebaseJsonFile).readAsString(); + + final decodedFirebaseJson = + jsonDecode(firebaseJsonFileContent) as Map; + + checkAppleFirebaseJsonValues( + decodedFirebaseJson, + [kFlutter, kPlatforms, kIos, kDefaultConfig], + '$kIos/$defaultTarget/$appleServiceFileName', + ); + checkAppleFirebaseJsonValues( + decodedFirebaseJson, + [ + kFlutter, + kPlatforms, + kMacos, + kDefaultConfig, + ], + '$kMacos/$defaultTarget/$appleServiceFileName', + ); + + checkAndroidFirebaseJsonValues( + decodedFirebaseJson, + [ + kFlutter, + kPlatforms, + kAndroid, + kDefaultConfig, + ], + 'android/app/$androidServiceFileName', + ); + + const defaultFilePath = 'lib/firebase_options.dart'; + final keysToMapDart = [kFlutter, kPlatforms, kDart, defaultFilePath]; + + checkDartFirebaseJsonValues( + decodedFirebaseJson, + keysToMapDart, + ); + + // check GoogleService-Info.plist file is included & debug symbols script (until firebase crashlytics is a dependency) is not included in Apple "project.pbxproj" files + final iosXcodeProject = p.join( + projectPath!, + kIos, + 'Runner.xcodeproj', + ); - if (Platform.isMacOS) { - // check Apple service files were created and have correct content - final iosPath = - p.join(projectPath!, kIos, defaultTarget, appleServiceFileName); - final macosPath = p.join(projectPath!, kMacos, defaultTarget); + final scriptToCheckIosPbxprojFile = + rubyScriptForTestingDefaultConfigure(iosXcodeProject); - await testAppleServiceFileValues(iosPath); - await testAppleServiceFileValues( - macosPath, - platform: kMacos, - ); + final iosResult = Process.runSync( + 'ruby', + [ + '-e', + scriptToCheckIosPbxprojFile, + ], + runInShell: true, + ); - // check default "firebase.json" was created and has correct content - final firebaseJsonFile = p.join(projectPath!, 'firebase.json'); - final firebaseJsonFileContent = - await File(firebaseJsonFile).readAsString(); + if (iosResult.exitCode != 0) { + fail(iosResult.stderr as String); + } - final decodedFirebaseJson = - jsonDecode(firebaseJsonFileContent) as Map; + expect(iosResult.stdout, 'success'); - checkAppleFirebaseJsonValues( - decodedFirebaseJson, - [kFlutter, kPlatforms, kIos, kDefaultConfig], - '$kIos/$defaultTarget/$appleServiceFileName', - ); - checkAppleFirebaseJsonValues( - decodedFirebaseJson, - [ - kFlutter, - kPlatforms, + final macosXcodeProject = p.join( + projectPath!, kMacos, - kDefaultConfig, - ], - '$kMacos/$defaultTarget/$appleServiceFileName', - ); + 'Runner.xcodeproj', + ); + + final scriptToCheckMacosPbxprojFile = + rubyScriptForTestingDefaultConfigure( + macosXcodeProject, + ); + + final macosResult = Process.runSync( + 'ruby', + [ + '-e', + scriptToCheckMacosPbxprojFile, + ], + runInShell: true, + ); + + if (macosResult.exitCode != 0) { + fail(macosResult.stderr as String); + } + + expect(macosResult.stdout, 'success'); + } - checkAndroidFirebaseJsonValues( - decodedFirebaseJson, - [ - kFlutter, - kPlatforms, - kAndroid, - kDefaultConfig, - ], - 'android/app/$androidServiceFileName', + // check google-services.json was created and has correct content + final androidServiceFilePath = p.join( + projectPath!, + 'android', + 'app', + androidServiceFileName, ); + testAndroidServiceFileValues(androidServiceFilePath); - const defaultFilePath = 'lib/firebase_options.dart'; - final keysToMapDart = [kFlutter, kPlatforms, kDart, defaultFilePath]; + // Check android "android/settings.gradle" & "android/app/build.gradle" were updated + await checkBuildGradleFileUpdated(projectPath!); - checkDartFirebaseJsonValues( - decodedFirebaseJson, - keysToMapDart, - ); - - // check GoogleService-Info.plist file is included & debug symbols script (until firebase crashlytics is a dependency) is not included in Apple "project.pbxproj" files - final iosXcodeProject = p.join( - projectPath!, - kIos, - 'Runner.xcodeproj', - ); + // check "firebase_options.dart" file is created in lib directory + final firebaseOptions = + p.join(projectPath!, 'lib', 'firebase_options.dart'); - final scriptToCheckIosPbxprojFile = - rubyScriptForTestingDefaultConfigure(iosXcodeProject); + await testFirebaseOptionsFileValues(firebaseOptions); + }, + timeout: const Timeout( + Duration(minutes: 2), + ), + ); - final iosResult = Process.runSync( - 'ruby', + test( + 'flutterfire configure: android - "build configuration" Apple - "build configuration"', + () async { + final result = Process.runSync( + 'flutterfire', [ - '-e', - scriptToCheckIosPbxprojFile, + 'configure', + '--yes', + '--project=$firebaseProjectId', + '--platforms=android,ios,macos,web,windows', + '--web-app-id=$webAppId', + '--windows-app-id=$windowsAppId', + // Android just requires the `--android-out` flag to be set + '--android-out=android/app/$buildType', + // Apple required the `--ios-out` and `--macos-out` flags to be set & the build type, + // We're using `Debug` for both which is a standard build configuration for an apple Flutter app + '--ios-out=ios/$buildType', + '--ios-build-config=$appleBuildConfiguration', + '--macos-out=macos/$buildType', + '--macos-build-config=$appleBuildConfiguration', ], + workingDirectory: projectPath, runInShell: true, ); - if (iosResult.exitCode != 0) { - fail(iosResult.stderr as String); + if (result.exitCode != 0) { + fail(result.stderr as String); } - expect(iosResult.stdout, 'success'); + if (Platform.isMacOS) { + // check Apple service files were created and have correct content + final iosPath = p.join( + projectPath!, + kIos, + buildType, + appleServiceFileName, + ); + final macosPath = p.join( + projectPath!, + kMacos, + buildType, + ); + + await testAppleServiceFileValues(iosPath); + await testAppleServiceFileValues( + macosPath, + platform: kMacos, + ); + + // check default "firebase.json" was created and has correct content + final firebaseJsonFile = p.join(projectPath!, 'firebase.json'); + final firebaseJsonFileContent = + await File(firebaseJsonFile).readAsString(); + + final decodedFirebaseJson = + jsonDecode(firebaseJsonFileContent) as Map; + + checkAppleFirebaseJsonValues( + decodedFirebaseJson, + [ + kFlutter, + kPlatforms, + kIos, + kBuildConfiguration, + appleBuildConfiguration, + ], + 'ios/$buildType/GoogleService-Info.plist', + ); + + checkAppleFirebaseJsonValues( + decodedFirebaseJson, + [ + kFlutter, + kPlatforms, + kMacos, + kBuildConfiguration, + appleBuildConfiguration, + ], + 'macos/$buildType/GoogleService-Info.plist', + ); + + checkAndroidFirebaseJsonValues( + decodedFirebaseJson, + [ + kFlutter, + kPlatforms, + kAndroid, + kBuildConfiguration, + buildType, + ], + 'android/app/$buildType/google-services.json', + ); + + const defaultFilePath = 'lib/firebase_options.dart'; + final keysToMapDart = [kFlutter, kPlatforms, kDart, defaultFilePath]; + checkDartFirebaseJsonValues( + decodedFirebaseJson, + keysToMapDart, + ); + + final scriptToCheckIosPbxprojFile = + rubyScriptForCheckingBundleResourcesScript( + projectPath!, + kIos, + ); + + final iosResult = Process.runSync( + 'ruby', + [ + '-e', + scriptToCheckIosPbxprojFile, + ], + runInShell: true, + ); + + if (iosResult.exitCode != 0) { + fail(iosResult.stderr as String); + } + + expect(iosResult.stdout, 'success'); + + final scriptToCheckMacosPbxprojFile = + rubyScriptForCheckingBundleResourcesScript( + projectPath!, + kMacos, + ); + + final macosResult = Process.runSync( + 'ruby', + [ + '-e', + scriptToCheckMacosPbxprojFile, + ], + runInShell: true, + ); + + if (macosResult.exitCode != 0) { + fail(macosResult.stderr as String); + } + + expect(macosResult.stdout, 'success'); + } - final macosXcodeProject = p.join( + // check google-services.json was created and has correct content + final androidServiceFilePath = p.join( projectPath!, - kMacos, - 'Runner.xcodeproj', + 'android', + 'app', + buildType, + 'google-services.json', ); + testAndroidServiceFileValues(androidServiceFilePath); - final scriptToCheckMacosPbxprojFile = - rubyScriptForTestingDefaultConfigure( - macosXcodeProject, - ); + // Check android "android/settings.gradle" & "android/app/build.gradle" were updated + await checkBuildGradleFileUpdated(projectPath!); - final macosResult = Process.runSync( - 'ruby', + // check "firebase_options.dart" file is created in lib directory + final firebaseOptions = + p.join(projectPath!, 'lib', 'firebase_options.dart'); + + await testFirebaseOptionsFileValues(firebaseOptions); + }, + timeout: const Timeout( + Duration(minutes: 2), + ), + ); + + test( + 'flutterfire configure: android - "default" Apple - "target"', + () async { + const targetType = 'Runner'; + const applePath = 'staging/target'; + const androidBuildConfiguration = 'development'; + final result = Process.runSync( + 'flutterfire', [ - '-e', - scriptToCheckMacosPbxprojFile, + 'configure', + '--yes', + '--project=$firebaseProjectId', + // The below args needed for CI + '--platforms=android,ios,macos,web,windows', + '--web-app-id=$webAppId', + '--windows-app-id=$windowsAppId', + // Android just requires the `--android-out` flag to be set + '--android-out=android/app/$androidBuildConfiguration', + // Apple required the `--ios-out` and `--macos-out` flags to be set & the build type, + // We're using `Runner` target for both which is the standard target for an apple Flutter app + '--ios-out=ios/$applePath', + '--ios-target=$targetType', + '--macos-out=macos/$applePath', + '--macos-target=$targetType', ], + workingDirectory: projectPath, runInShell: true, ); - if (macosResult.exitCode != 0) { - fail(macosResult.stderr as String); + if (result.exitCode != 0) { + fail(result.stderr as String); } - expect(macosResult.stdout, 'success'); - } - - // check google-services.json was created and has correct content - final androidServiceFilePath = p.join( - projectPath!, - 'android', - 'app', - androidServiceFileName, - ); - testAndroidServiceFileValues(androidServiceFilePath); + if (Platform.isMacOS) { + // check Apple service files were created and have correct content + final iosPath = + p.join(projectPath!, kIos, applePath, appleServiceFileName); + final macosPath = p.join(projectPath!, kMacos, applePath); + + await testAppleServiceFileValues(iosPath); + await testAppleServiceFileValues( + macosPath, + platform: kMacos, + ); + + // check default "firebase.json" was created and has correct content + final firebaseJsonFile = p.join(projectPath!, 'firebase.json'); + final firebaseJsonFileContent = + await File(firebaseJsonFile).readAsString(); + + final decodedFirebaseJson = + jsonDecode(firebaseJsonFileContent) as Map; + + checkAppleFirebaseJsonValues( + decodedFirebaseJson, + [ + kFlutter, + kPlatforms, + kIos, + kTargets, + targetType, + ], + 'ios/$applePath/GoogleService-Info.plist', + ); + + checkAppleFirebaseJsonValues( + decodedFirebaseJson, + [kFlutter, kPlatforms, kMacos, kTargets, targetType], + 'macos/$applePath/GoogleService-Info.plist', + ); + + checkAndroidFirebaseJsonValues( + decodedFirebaseJson, + [ + kFlutter, + kPlatforms, + kAndroid, + kBuildConfiguration, + androidBuildConfiguration, + ], + 'android/app/$androidBuildConfiguration/google-services.json', + ); + + // Check dart map is correct + const defaultFilePath = 'lib/firebase_options.dart'; + final keysToMapDart = [kFlutter, kPlatforms, kDart, defaultFilePath]; + checkDartFirebaseJsonValues(decodedFirebaseJson, keysToMapDart); + + // check GoogleService-Info.plist file is included & debug symbols script (until firebase crashlytics is a dependency) is not included in Apple "project.pbxproj" files + final iosXcodeProject = p.join( + projectPath!, + kIos, + 'Runner.xcodeproj', + ); - // Check android "android/settings.gradle" & "android/app/build.gradle" were updated - await checkBuildGradleFileUpdated(projectPath!); + final scriptToCheckIosPbxprojFile = + rubyScriptForTestingDefaultConfigure(iosXcodeProject); - // check "firebase_options.dart" file is created in lib directory - final firebaseOptions = - p.join(projectPath!, 'lib', 'firebase_options.dart'); + final iosResult = Process.runSync( + 'ruby', + [ + '-e', + scriptToCheckIosPbxprojFile, + ], + runInShell: true, + ); - await testFirebaseOptionsFileValues(firebaseOptions); - }, - timeout: const Timeout( - Duration(minutes: 2), - ), - ); + if (iosResult.exitCode != 0) { + fail(iosResult.stderr as String); + } - test( - 'flutterfire configure: android - "build configuration" Apple - "build configuration"', - () async { - final result = Process.runSync( - 'flutterfire', - [ - 'configure', - '--yes', - '--project=$firebaseProjectId', - '--platforms=android,ios,macos,web,windows', - '--web-app-id=$webAppId', - '--windows-app-id=$windowsAppId', - // Android just requires the `--android-out` flag to be set - '--android-out=android/app/$buildType', - // Apple required the `--ios-out` and `--macos-out` flags to be set & the build type, - // We're using `Debug` for both which is a standard build configuration for an apple Flutter app - '--ios-out=ios/$buildType', - '--ios-build-config=$appleBuildConfiguration', - '--macos-out=macos/$buildType', - '--macos-build-config=$appleBuildConfiguration', - ], - workingDirectory: projectPath, - runInShell: true, - ); + expect(iosResult.stdout, 'success'); - if (result.exitCode != 0) { - fail(result.stderr as String); - } + final macosXcodeProject = p.join( + projectPath!, + kMacos, + 'Runner.xcodeproj', + ); + + final scriptToCheckMacosPbxprojFile = + rubyScriptForTestingDefaultConfigure( + macosXcodeProject, + ); + + final macosResult = Process.runSync( + 'ruby', + [ + '-e', + scriptToCheckMacosPbxprojFile, + ], + runInShell: true, + ); + + if (macosResult.exitCode != 0) { + fail(macosResult.stderr as String); + } + + expect(macosResult.stdout, 'success'); + } - if (Platform.isMacOS) { - // check Apple service files were created and have correct content - final iosPath = p.join( + // check google-services.json was created and has correct content + final androidServiceFilePath = p.join( projectPath!, - kIos, - buildType, - appleServiceFileName, - ); - final macosPath = p.join( - projectPath!, - kMacos, - buildType, + 'android', + 'app', + androidBuildConfiguration, + 'google-services.json', ); + testAndroidServiceFileValues(androidServiceFilePath); - await testAppleServiceFileValues(iosPath); - await testAppleServiceFileValues( - macosPath, - platform: kMacos, - ); - - // check default "firebase.json" was created and has correct content - final firebaseJsonFile = p.join(projectPath!, 'firebase.json'); - final firebaseJsonFileContent = - await File(firebaseJsonFile).readAsString(); + // Check android "android/settings.gradle" & "android/app/build.gradle" were updated + await checkBuildGradleFileUpdated(projectPath!); - final decodedFirebaseJson = - jsonDecode(firebaseJsonFileContent) as Map; + // check "firebase_options.dart" file is created in lib directory + final firebaseOptions = + p.join(projectPath!, 'lib', 'firebase_options.dart'); - checkAppleFirebaseJsonValues( - decodedFirebaseJson, + await testFirebaseOptionsFileValues(firebaseOptions); + }, + timeout: const Timeout( + Duration(minutes: 2), + ), + ); + test( + 'flutterfire configure: rewrite service files when rerunning "flutterfire configure" with different apps', + () async { + const defaultTarget = 'Runner'; + // The initial configuration + final result = Process.runSync( + 'flutterfire', [ - kFlutter, - kPlatforms, - kIos, - kBuildConfiguration, - appleBuildConfiguration, + 'configure', + '--yes', + '--project=$firebaseProjectId', + '--platforms=android,ios,macos,web', + '--web-app-id=$webAppId', + '--windows-app-id=$windowsAppId', ], - 'ios/$buildType/GoogleService-Info.plist', + workingDirectory: projectPath, + runInShell: true, ); - checkAppleFirebaseJsonValues( - decodedFirebaseJson, - [ - kFlutter, - kPlatforms, - kMacos, - kBuildConfiguration, - appleBuildConfiguration, - ], - 'macos/$buildType/GoogleService-Info.plist', - ); + if (result.exitCode != 0) { + fail(result.stderr as String); + } - checkAndroidFirebaseJsonValues( - decodedFirebaseJson, + // The second configuration with different bundle ids which we need to check + final result2 = Process.runSync( + 'flutterfire', [ - kFlutter, - kPlatforms, - kAndroid, - kBuildConfiguration, - buildType, + 'configure', + '--yes', + '--project=$firebaseProjectId', + '--platforms=android,ios,macos,web,windows', + '--ios-bundle-id=com.example.secondApp', + '--android-package-name=com.example.second_app', + '--macos-bundle-id=com.example.secondApp', + '--web-app-id=$secondWebAppId', + '--windows-app-id=$secondWindowsAppId', ], - 'android/app/$buildType/google-services.json', + workingDirectory: projectPath, + runInShell: true, ); - const defaultFilePath = 'lib/firebase_options.dart'; - final keysToMapDart = [kFlutter, kPlatforms, kDart, defaultFilePath]; - checkDartFirebaseJsonValues( - decodedFirebaseJson, - keysToMapDart, - ); + if (result2.exitCode != 0) { + fail(result2.stderr as String); + } - final scriptToCheckIosPbxprojFile = - rubyScriptForCheckingBundleResourcesScript( - projectPath!, - kIos, - ); + if (Platform.isMacOS) { + // check Apple service files were created and have correct content + final iosPath = + p.join(projectPath!, kIos, defaultTarget, appleServiceFileName); + final macosPath = p.join(projectPath!, kMacos, defaultTarget); + + await testAppleServiceFileValues( + iosPath, + appId: secondAppleAppId, + bundleId: secondAppleBundleId, + ); + await testAppleServiceFileValues( + macosPath, + platform: kMacos, + appId: secondAppleAppId, + bundleId: secondAppleBundleId, + ); + + // check default "firebase.json" was created and has correct content + final firebaseJsonFile = p.join(projectPath!, 'firebase.json'); + final firebaseJsonFileContent = + await File(firebaseJsonFile).readAsString(); + + final decodedFirebaseJson = + jsonDecode(firebaseJsonFileContent) as Map; + + checkAppleFirebaseJsonValues( + decodedFirebaseJson, + [kFlutter, kPlatforms, kIos, kDefaultConfig], + '$kIos/$defaultTarget/$appleServiceFileName', + appId: secondAppleAppId, + ); + checkAppleFirebaseJsonValues( + decodedFirebaseJson, + [ + kFlutter, + kPlatforms, + kMacos, + kDefaultConfig, + ], + '$kMacos/$defaultTarget/$appleServiceFileName', + appId: secondAppleAppId, + ); + + checkAndroidFirebaseJsonValues( + decodedFirebaseJson, + [ + kFlutter, + kPlatforms, + kAndroid, + kDefaultConfig, + ], + 'android/app/$androidServiceFileName', + appId: secondAndroidAppId, + ); + + const defaultFilePath = 'lib/firebase_options.dart'; + final keysToMapDart = [kFlutter, kPlatforms, kDart, defaultFilePath]; + + checkDartFirebaseJsonValues( + decodedFirebaseJson, + keysToMapDart, + androidAppId: secondAndroidAppId, + appleAppId: secondAppleAppId, + webAppId: secondWebAppId, + ); + + // check GoogleService-Info.plist file is included & debug symbols script (until firebase crashlytics is a dependency) is not included in Apple "project.pbxproj" files + final iosXcodeProject = p.join( + projectPath!, + kIos, + 'Runner.xcodeproj', + ); - final iosResult = Process.runSync( - 'ruby', - [ - '-e', - scriptToCheckIosPbxprojFile, - ], - runInShell: true, - ); + final scriptToCheckIosPbxprojFile = + rubyScriptForTestingDefaultConfigure(iosXcodeProject); - if (iosResult.exitCode != 0) { - fail(iosResult.stderr as String); - } + final iosResult = Process.runSync( + 'ruby', + [ + '-e', + scriptToCheckIosPbxprojFile, + ], + ); - expect(iosResult.stdout, 'success'); + if (iosResult.exitCode != 0) { + fail(iosResult.stderr as String); + } - final scriptToCheckMacosPbxprojFile = - rubyScriptForCheckingBundleResourcesScript( + expect(iosResult.stdout, 'success'); + + final macosXcodeProject = p.join( + projectPath!, + kMacos, + 'Runner.xcodeproj', + ); + + final scriptToCheckMacosPbxprojFile = + rubyScriptForTestingDefaultConfigure( + macosXcodeProject, + ); + + final macosResult = Process.runSync( + 'ruby', + [ + '-e', + scriptToCheckMacosPbxprojFile, + ], + ); + + if (macosResult.exitCode != 0) { + fail(macosResult.stderr as String); + } + + expect(macosResult.stdout, 'success'); + } + + // check google-services.json was created and has correct content + final androidServiceFilePath = p.join( projectPath!, - kMacos, + 'android', + 'app', + androidServiceFileName, + ); + testAndroidServiceFileValues( + androidServiceFilePath, + appId: secondAndroidAppId, + ); + + // check "firebase_options.dart" file is created in lib directory + final firebaseOptions = + p.join(projectPath!, 'lib', 'firebase_options.dart'); + + final firebaseOptionsContent = + await File(firebaseOptions).readAsString(); + + expect( + firebaseOptionsContent.split('\n'), + containsAll([ + contains(secondAppleAppId), + contains(secondAppleBundleId), + contains(secondAndroidAppId), + contains(secondWebAppId), + contains(secondWindowsAppId), + contains('static const FirebaseOptions web = FirebaseOptions'), + contains('static const FirebaseOptions android = FirebaseOptions'), + contains('static const FirebaseOptions ios = FirebaseOptions'), + contains('static const FirebaseOptions macos = FirebaseOptions'), + contains('static const FirebaseOptions windows = FirebaseOptions'), + ]), ); + }, + timeout: const Timeout( + Duration(minutes: 2), + ), + ); + + test( + 'flutterfire configure: test when only two platforms are selected, not including "web" platform', + () async { + const defaultTarget = 'Runner'; - final macosResult = Process.runSync( - 'ruby', + final result = Process.runSync( + 'flutterfire', [ - '-e', - scriptToCheckMacosPbxprojFile, + 'configure', + '--yes', + '--platforms=ios,android', + '--project=$firebaseProjectId', ], + workingDirectory: projectPath, runInShell: true, ); - if (macosResult.exitCode != 0) { - fail(macosResult.stderr as String); + if (result.exitCode != 0) { + fail(result.stderr as String); } - expect(macosResult.stdout, 'success'); - } - - // check google-services.json was created and has correct content - final androidServiceFilePath = p.join( - projectPath!, - 'android', - 'app', - buildType, - 'google-services.json', - ); - testAndroidServiceFileValues(androidServiceFilePath); - - // Check android "android/settings.gradle" & "android/app/build.gradle" were updated - await checkBuildGradleFileUpdated(projectPath!); + if (Platform.isMacOS) { + // check iOS service files were created and have correct content + final iosPath = + p.join(projectPath!, kIos, defaultTarget, appleServiceFileName); + + await testAppleServiceFileValues( + iosPath, + ); + + // check default "firebase.json" was created and has correct content + final firebaseJsonFile = p.join(projectPath!, 'firebase.json'); + final firebaseJsonFileContent = + await File(firebaseJsonFile).readAsString(); + + final decodedFirebaseJson = + jsonDecode(firebaseJsonFileContent) as Map; + + checkAppleFirebaseJsonValues( + decodedFirebaseJson, + [kFlutter, kPlatforms, kIos, kDefaultConfig], + '$kIos/$defaultTarget/$appleServiceFileName', + ); + + checkAndroidFirebaseJsonValues( + decodedFirebaseJson, + [ + kFlutter, + kPlatforms, + kAndroid, + kDefaultConfig, + ], + 'android/app/$androidServiceFileName', + ); + + const defaultFilePath = 'lib/firebase_options.dart'; + final keysToMapDart = [kFlutter, kPlatforms, kDart, defaultFilePath]; + + checkDartFirebaseJsonValues( + decodedFirebaseJson, + keysToMapDart, + checkMacos: false, + checkWeb: false, + ); + + // check GoogleService-Info.plist file is included & debug symbols script (until firebase crashlytics is a dependency) is not included in Apple "project.pbxproj" files + final iosXcodeProject = p.join( + projectPath!, + kIos, + 'Runner.xcodeproj', + ); - // check "firebase_options.dart" file is created in lib directory - final firebaseOptions = - p.join(projectPath!, 'lib', 'firebase_options.dart'); + final scriptToCheckIosPbxprojFile = + rubyScriptForTestingDefaultConfigure(iosXcodeProject); - await testFirebaseOptionsFileValues(firebaseOptions); - }, - timeout: const Timeout( - Duration(minutes: 2), - ), - ); - - test( - 'flutterfire configure: android - "default" Apple - "target"', - () async { - const targetType = 'Runner'; - const applePath = 'staging/target'; - const androidBuildConfiguration = 'development'; - final result = Process.runSync( - 'flutterfire', - [ - 'configure', - '--yes', - '--project=$firebaseProjectId', - // The below args needed for CI - '--platforms=android,ios,macos,web,windows', - '--web-app-id=$webAppId', - '--windows-app-id=$windowsAppId', - // Android just requires the `--android-out` flag to be set - '--android-out=android/app/$androidBuildConfiguration', - // Apple required the `--ios-out` and `--macos-out` flags to be set & the build type, - // We're using `Runner` target for both which is the standard target for an apple Flutter app - '--ios-out=ios/$applePath', - '--ios-target=$targetType', - '--macos-out=macos/$applePath', - '--macos-target=$targetType', - ], - workingDirectory: projectPath, - runInShell: true, - ); + final iosResult = Process.runSync( + 'ruby', + [ + '-e', + scriptToCheckIosPbxprojFile, + ], + ); - if (result.exitCode != 0) { - fail(result.stderr as String); - } + if (iosResult.exitCode != 0) { + fail(iosResult.stderr as String); + } - if (Platform.isMacOS) { - // check Apple service files were created and have correct content - final iosPath = - p.join(projectPath!, kIos, applePath, appleServiceFileName); - final macosPath = p.join(projectPath!, kMacos, applePath); + expect(iosResult.stdout, 'success'); + } - await testAppleServiceFileValues(iosPath); - await testAppleServiceFileValues( - macosPath, - platform: kMacos, + // check google-services.json was created and has correct content + final androidServiceFilePath = p.join( + projectPath!, + 'android', + 'app', + androidServiceFileName, ); - - // check default "firebase.json" was created and has correct content - final firebaseJsonFile = p.join(projectPath!, 'firebase.json'); - final firebaseJsonFileContent = - await File(firebaseJsonFile).readAsString(); - - final decodedFirebaseJson = - jsonDecode(firebaseJsonFileContent) as Map; - - checkAppleFirebaseJsonValues( - decodedFirebaseJson, - [ - kFlutter, - kPlatforms, - kIos, - kTargets, - targetType, - ], - 'ios/$applePath/GoogleService-Info.plist', + testAndroidServiceFileValues( + androidServiceFilePath, ); - checkAppleFirebaseJsonValues( - decodedFirebaseJson, - [kFlutter, kPlatforms, kMacos, kTargets, targetType], - 'macos/$applePath/GoogleService-Info.plist', + // check "firebase_options.dart" file is created in lib directory + final firebaseOptions = + p.join(projectPath!, 'lib', 'firebase_options.dart'); + + final firebaseOptionsContent = + await File(firebaseOptions).readAsString(); + + final listOfStrings = firebaseOptionsContent.split('\n'); + expect( + listOfStrings, + containsAll([ + contains(appleAppId), + contains(appleBundleId), + contains(androidAppId), + contains('static const FirebaseOptions android = FirebaseOptions'), + contains('static const FirebaseOptions ios = FirebaseOptions'), + ]), + ); + expect( + firebaseOptionsContent + .contains('static const FirebaseOptions web = FirebaseOptions'), + isFalse, + ); + expect( + firebaseOptionsContent.contains( + 'static const FirebaseOptions windows = FirebaseOptions'), + isFalse, ); + }, + timeout: const Timeout( + Duration(minutes: 2), + ), + ); - checkAndroidFirebaseJsonValues( - decodedFirebaseJson, + test( + 'flutterfire configure: test will reconfigure project if no args and `firebase.json` is present', + () async { + const defaultTarget = 'Runner'; + // Set up initial configuration + final result = Process.runSync( + 'flutterfire', [ - kFlutter, - kPlatforms, - kAndroid, - kBuildConfiguration, - androidBuildConfiguration, + 'configure', + '--yes', + '--project=$firebaseProjectId', + '--platforms=android,ios', ], - 'android/app/$androidBuildConfiguration/google-services.json', + workingDirectory: projectPath, + runInShell: true, ); - // Check dart map is correct - const defaultFilePath = 'lib/firebase_options.dart'; - final keysToMapDart = [kFlutter, kPlatforms, kDart, defaultFilePath]; - checkDartFirebaseJsonValues(decodedFirebaseJson, keysToMapDart); + if (result.exitCode != 0) { + fail(result.stderr as String); + } + + // Now firebase.json file has been written, change values in service files to test they are rewritten + if (Platform.isMacOS) { + final iosPath = + p.join(projectPath!, kIos, defaultTarget, appleServiceFileName); + + // Clean out file to test it was recreated + await File(iosPath).writeAsString(''); + } - // check GoogleService-Info.plist file is included & debug symbols script (until firebase crashlytics is a dependency) is not included in Apple "project.pbxproj" files - final iosXcodeProject = p.join( + final androidServiceFilePath = p.join( projectPath!, - kIos, - 'Runner.xcodeproj', + 'android', + 'app', + androidServiceFileName, ); + // Clean out file to test it was recreated + await File(androidServiceFilePath).writeAsString(''); - final scriptToCheckIosPbxprojFile = - rubyScriptForTestingDefaultConfigure(iosXcodeProject); + final firebaseOptions = + p.join(projectPath!, 'lib', 'firebase_options.dart'); - final iosResult = Process.runSync( - 'ruby', + final accessToken = await generateAccessTokenCI(); + // Perform `flutterfire configure` without args to use `flutterfire reconfigure`. + final result2 = Process.runSync( + 'flutterfire', [ - '-e', - scriptToCheckIosPbxprojFile, + 'configure', + if (accessToken != null) '--test-access-token=$accessToken', ], + workingDirectory: projectPath, runInShell: true, + environment: {'TEST_ENVIRONMENT': 'true'}, ); - if (iosResult.exitCode != 0) { - fail(iosResult.stderr as String); + if (result2.exitCode != 0) { + fail(result2.stderr as String); } - expect(iosResult.stdout, 'success'); + if (Platform.isMacOS) { + // check iOS service file was recreated and has correct content + final iosPath = + p.join(projectPath!, kIos, defaultTarget, appleServiceFileName); - final macosXcodeProject = p.join( - projectPath!, - kMacos, - 'Runner.xcodeproj', + await testAppleServiceFileValues( + iosPath, + ); + } + + // check google-services.json was recreated and has correct content + testAndroidServiceFileValues( + androidServiceFilePath, ); - final scriptToCheckMacosPbxprojFile = - rubyScriptForTestingDefaultConfigure( - macosXcodeProject, + // check "firebase_options.dart" file was recreated in lib directory + final firebaseOptionsContent = + await File(firebaseOptions).readAsString(); + + final listOfStrings = firebaseOptionsContent.split('\n'); + expect( + listOfStrings, + containsAll([ + contains(appleAppId), + contains(appleBundleId), + contains(androidAppId), + contains('static const FirebaseOptions android = FirebaseOptions'), + contains('static const FirebaseOptions ios = FirebaseOptions'), + ]), ); + expect( + firebaseOptionsContent + .contains('static const FirebaseOptions web = FirebaseOptions'), + isFalse, + ); + expect( + firebaseOptionsContent.contains( + 'static const FirebaseOptions windows = FirebaseOptions'), + isFalse, + ); + }, + timeout: const Timeout( + Duration(minutes: 2), + ), + ); - final macosResult = Process.runSync( - 'ruby', + test( + 'flutterfire configure: write Dart configuration file to different output', + () async { + const configurationFileName = 'different_firebase_options.dart'; + // Set up initial configuration + final result = Process.runSync( + 'flutterfire', [ - '-e', - scriptToCheckMacosPbxprojFile, + 'configure', + '--yes', + '--project=$firebaseProjectId', + '--platforms=android,ios', + // The below args aren't needed unless running from CI. We need for Github actions to run command. + '--platforms=android,ios,macos,web,windows', + '--web-app-id=$webAppId', + '--windows-app-id=$windowsAppId', + // Output to different file + '--out=lib/$configurationFileName', ], + workingDirectory: projectPath, runInShell: true, ); - if (macosResult.exitCode != 0) { - fail(macosResult.stderr as String); + if (result.exitCode != 0) { + fail(result.stderr as String); } - expect(macosResult.stdout, 'success'); - } - - // check google-services.json was created and has correct content - final androidServiceFilePath = p.join( - projectPath!, - 'android', - 'app', - androidBuildConfiguration, - 'google-services.json', - ); - testAndroidServiceFileValues(androidServiceFilePath); - - // Check android "android/settings.gradle" & "android/app/build.gradle" were updated - await checkBuildGradleFileUpdated(projectPath!); - - // check "firebase_options.dart" file is created in lib directory - final firebaseOptions = - p.join(projectPath!, 'lib', 'firebase_options.dart'); - - await testFirebaseOptionsFileValues(firebaseOptions); - }, - timeout: const Timeout( - Duration(minutes: 2), - ), - ); + final firebaseOptions = + p.join(projectPath!, 'lib', configurationFileName); + + // check "different_firebase_options.dart" file was recreated in lib directory + final firebaseOptionsContent = + await File(firebaseOptions).readAsString(); + + final listOfStrings = firebaseOptionsContent.split('\n'); + expect( + listOfStrings, + containsAll([ + contains(appleAppId), + contains(appleBundleId), + contains(androidAppId), + contains(webAppId), + contains(windowsAppId), + contains('static const FirebaseOptions android = FirebaseOptions'), + contains('static const FirebaseOptions ios = FirebaseOptions'), + contains('static const FirebaseOptions web = FirebaseOptions'), + contains('static const FirebaseOptions windows = FirebaseOptions'), + ]), + ); + }, + timeout: const Timeout( + Duration(minutes: 2), + ), + ); - test( - 'Validate `flutterfire upload-crashlytics-symbols` script is ran when building app', - () async { + test( + 'flutterfire configure: incorrect `--web-app-id` should throw exception', + () async { final result = Process.runSync( - 'flutter', - ['pub', 'add', 'firebase_crashlytics'], - workingDirectory: projectPath, - ); - - if (result.exitCode != 0) { - fail(result.stderr as String); - } - - final result2 = Process.runSync( 'flutterfire', [ 'configure', '--yes', '--project=$firebaseProjectId', - '--platforms=android,ios,macos,web,windows', - '--web-app-id=$webAppId', - '--windows-app-id=$windowsAppId', - ], - workingDirectory: projectPath, - runInShell: true, - ); - - if (result2.exitCode != 0) { - fail(result2.stderr as String); - } - - const iosVersion = '13.0'; - // Update project.pbxproj - final pbxprojResult = Process.runSync( - 'sed', - [ - '-i', - '', - 's/IPHONEOS_DEPLOYMENT_TARGET = [0-9.]*;/IPHONEOS_DEPLOYMENT_TARGET = $iosVersion;/', - 'ios/Runner.xcodeproj/project.pbxproj', - ], - workingDirectory: projectPath, - ); - - if (pbxprojResult.exitCode != 0) { - fail(pbxprojResult.stderr as String); - } - - final buildApp = Process.runSync( - 'flutter', - [ - 'build', - 'ios', - '--no-codesign', - '--simulator', - '--debug', - '--verbose', + '--platforms=web', + '--web-app-id=a-non-existent-web-app-id', ], workingDirectory: projectPath, runInShell: true, ); - if (buildApp.exitCode != 0) { - fail(buildApp.stderr as String); - } + final output = result.stderr as String; + final expectedOutput = [ + 'does not match the web app id of any existing Firebase app', + 'Exception', + ]; + final expected = expectedOutput.every(output.contains); + expect(result.exitCode != 0, isTrue); expect( - buildApp.stdout, - // Check symbols are uploaded in the background - contains('Symbol uploading will proceed in the background'), - ); - - Process.runSync( - 'flutter', - ['config', '--enable-swift-package-manager'], - workingDirectory: projectPath, - ); - - final iosDirectory = p.join(projectPath!, 'ios'); - - Process.runSync( - 'bash', - [ - '-c', - '[ -f Podfile ] && rm Podfile && pod deintegrate && rm -rf Pods/', - ], - workingDirectory: iosDirectory, + expected, + isTrue, ); + }); - final buildAppSPM = Process.runSync( - 'flutter', + test( + 'flutterfire configure: incorrect `--windows-app-id` should throw exception', + () async { + final result = Process.runSync( + 'flutterfire', [ - 'build', - 'ios', - '--no-codesign', - '--simulator', - '--debug', - '--verbose', + 'configure', + '--yes', + '--project=$firebaseProjectId', + '--platforms=windows', + // Trigger the exception + '--windows-app-id=a-non-existent-windows-app-id', ], workingDirectory: projectPath, runInShell: true, ); - if (buildAppSPM.exitCode != 0) { - fail(buildAppSPM.stderr as String); - } + final output = result.stderr as String; + final expectedOutput = [ + 'does not match the web app id of any existing Firebase app', + 'Exception', + ]; + final expected = expectedOutput.every(output.contains); + + expect(result.exitCode != 0, isTrue); expect( - buildAppSPM.stdout, - // Check symbols are uploaded in the background - contains('Symbol uploading will proceed in the background'), + expected, + isTrue, ); - }, - skip: !Platform.isMacOS, - timeout: const Timeout( - Duration(minutes: 3), - ), - ); - - test( - 'flutterfire configure: rewrite service files when rerunning "flutterfire configure" with different apps', - () async { - const defaultTarget = 'Runner'; - // The initial configuration + }); + + test( + 'flutterfire configure: get correct Firebase App with manually created Firebase web app via `--web-app-id`', + () async { final result = Process.runSync( 'flutterfire', [ 'configure', '--yes', '--project=$firebaseProjectId', - '--platforms=android,ios,macos,web', - '--web-app-id=$webAppId', - '--windows-app-id=$windowsAppId', + '--platforms=web', + '--web-app-id=$secondWebAppId', ], workingDirectory: projectPath, runInShell: true, ); - if (result.exitCode != 0) { - fail(result.stderr as String); - } + expect(result.exitCode, 0); + // Console out put looks like this on success: "web 1:262904632156:web:cb3a00412ed430ca2f2799" + expect( + (result.stdout as String).contains( + 'web $secondWebAppId', + ), + isTrue, + ); - // The second configuration with different bundle ids which we need to check - final result2 = Process.runSync( + // check "firebase_options.dart" file is created in lib directory + final firebaseOptions = + p.join(projectPath!, 'lib', 'firebase_options.dart'); + + await testFirebaseOptionsFileValues( + firebaseOptions, + selectedPlatform: kWeb, + ); + }); + + test( + 'flutterfire configure: get correct Firebase App with manually created Firebase web app via `--windows-app-id`', + () async { + final result = Process.runSync( 'flutterfire', [ 'configure', '--yes', '--project=$firebaseProjectId', - '--platforms=android,ios,macos,web,windows', - '--ios-bundle-id=com.example.secondApp', - '--android-package-name=com.example.second_app', - '--macos-bundle-id=com.example.secondApp', - '--web-app-id=$secondWebAppId', + '--platforms=windows', '--windows-app-id=$secondWindowsAppId', ], workingDirectory: projectPath, runInShell: true, ); - if (result2.exitCode != 0) { - fail(result2.stderr as String); - } - - if (Platform.isMacOS) { - // check Apple service files were created and have correct content - final iosPath = - p.join(projectPath!, kIos, defaultTarget, appleServiceFileName); - final macosPath = p.join(projectPath!, kMacos, defaultTarget); - - await testAppleServiceFileValues( - iosPath, - appId: secondAppleAppId, - bundleId: secondAppleBundleId, - ); - await testAppleServiceFileValues( - macosPath, - platform: kMacos, - appId: secondAppleAppId, - bundleId: secondAppleBundleId, - ); + expect(result.exitCode, 0); - // check default "firebase.json" was created and has correct content - final firebaseJsonFile = p.join(projectPath!, 'firebase.json'); - final firebaseJsonFileContent = - await File(firebaseJsonFile).readAsString(); + // check "firebase_options.dart" file is created in lib directory + final firebaseOptions = + p.join(projectPath!, 'lib', 'firebase_options.dart'); - final decodedFirebaseJson = - jsonDecode(firebaseJsonFileContent) as Map; + await testFirebaseOptionsFileValues( + firebaseOptions, + selectedPlatform: kWindows, + ); + }); - checkAppleFirebaseJsonValues( - decodedFirebaseJson, - [kFlutter, kPlatforms, kIos, kDefaultConfig], - '$kIos/$defaultTarget/$appleServiceFileName', - appId: secondAppleAppId, - ); - checkAppleFirebaseJsonValues( - decodedFirebaseJson, + test( + 'flutterfire configure: check Dart file configuration is updated correctly', + () async { + // Set up initial configuration + final result = Process.runSync( + 'flutterfire', [ - kFlutter, - kPlatforms, - kMacos, - kDefaultConfig, + 'configure', + '--yes', + '--project=$firebaseProjectId', + // Only configuring android initially + '--platforms=android', ], - '$kMacos/$defaultTarget/$appleServiceFileName', - appId: secondAppleAppId, - ); - - checkAndroidFirebaseJsonValues( - decodedFirebaseJson, - [ - kFlutter, - kPlatforms, - kAndroid, - kDefaultConfig, - ], - 'android/app/$androidServiceFileName', - appId: secondAndroidAppId, + workingDirectory: projectPath, + runInShell: true, ); - const defaultFilePath = 'lib/firebase_options.dart'; - final keysToMapDart = [kFlutter, kPlatforms, kDart, defaultFilePath]; - - checkDartFirebaseJsonValues( - decodedFirebaseJson, - keysToMapDart, - androidAppId: secondAndroidAppId, - appleAppId: secondAppleAppId, - webAppId: secondWebAppId, - ); + if (result.exitCode != 0) { + fail(result.stderr as String); + } - // check GoogleService-Info.plist file is included & debug symbols script (until firebase crashlytics is a dependency) is not included in Apple "project.pbxproj" files - final iosXcodeProject = p.join( + final firebaseOptions = p.join( projectPath!, - kIos, - 'Runner.xcodeproj', + 'lib', + 'firebase_options.dart', ); - final scriptToCheckIosPbxprojFile = - rubyScriptForTestingDefaultConfigure(iosXcodeProject); - - final iosResult = Process.runSync( - 'ruby', - [ - '-e', - scriptToCheckIosPbxprojFile, - ], + // check "firebase_options.dart" file was created and updates android only + final firebaseOptionsContent = + await File(firebaseOptions).readAsString(); + + // only configure android initially + final listOfStrings = firebaseOptionsContent.split('\n'); + expect( + listOfStrings, + containsAll([ + contains(androidAppId), + contains('static const FirebaseOptions android = FirebaseOptions'), + ]), ); - - if (iosResult.exitCode != 0) { - fail(iosResult.stderr as String); - } - - expect(iosResult.stdout, 'success'); - - final macosXcodeProject = p.join( - projectPath!, - kMacos, - 'Runner.xcodeproj', + expect( + firebaseOptionsContent + .contains('static const FirebaseOptions web = FirebaseOptions'), + isFalse, ); - - final scriptToCheckMacosPbxprojFile = - rubyScriptForTestingDefaultConfigure( - macosXcodeProject, + expect( + firebaseOptionsContent + .contains('static const FirebaseOptions ios = FirebaseOptions'), + isFalse, ); - final macosResult = Process.runSync( - 'ruby', + // Now reconfigure with ios, macos & web platforms and check Dart file is updated correctly + final result2 = Process.runSync( + 'flutterfire', [ - '-e', - scriptToCheckMacosPbxprojFile, + 'configure', + '--yes', + '--project=$firebaseProjectId', + // Configure the rest of the platforms + '--platforms=ios,macos,web', + '--web-app-id=$webAppId', ], + workingDirectory: projectPath, + runInShell: true, ); - if (macosResult.exitCode != 0) { - fail(macosResult.stderr as String); + if (result2.exitCode != 0) { + fail(result.stderr as String); } - expect(macosResult.stdout, 'success'); - } - - // check google-services.json was created and has correct content - final androidServiceFilePath = p.join( - projectPath!, - 'android', - 'app', - androidServiceFileName, - ); - testAndroidServiceFileValues( - androidServiceFilePath, - appId: secondAndroidAppId, - ); - - // check "firebase_options.dart" file is created in lib directory - final firebaseOptions = - p.join(projectPath!, 'lib', 'firebase_options.dart'); - - final firebaseOptionsContent = await File(firebaseOptions).readAsString(); - - expect( - firebaseOptionsContent.split('\n'), - containsAll([ - contains(secondAppleAppId), - contains(secondAppleBundleId), - contains(secondAndroidAppId), - contains(secondWebAppId), - contains(secondWindowsAppId), - contains('static const FirebaseOptions web = FirebaseOptions'), - contains('static const FirebaseOptions android = FirebaseOptions'), - contains('static const FirebaseOptions ios = FirebaseOptions'), - contains('static const FirebaseOptions macos = FirebaseOptions'), - contains('static const FirebaseOptions windows = FirebaseOptions'), - ]), - ); - }, - timeout: const Timeout( - Duration(minutes: 2), - ), - ); - - test( - 'flutterfire configure: test when only two platforms are selected, not including "web" platform', - () async { - const defaultTarget = 'Runner'; - - final result = Process.runSync( - 'flutterfire', - [ - 'configure', - '--yes', - '--platforms=ios,android', - '--project=$firebaseProjectId', - ], - workingDirectory: projectPath, - runInShell: true, - ); - - if (result.exitCode != 0) { - fail(result.stderr as String); - } - - if (Platform.isMacOS) { - // check iOS service files were created and have correct content - final iosPath = - p.join(projectPath!, kIos, defaultTarget, appleServiceFileName); - - await testAppleServiceFileValues( - iosPath, + final firebaseOptionsContent2 = + await File(firebaseOptions).readAsString(); + final listOfStrings2 = firebaseOptionsContent2.split('\n'); + + expect( + listOfStrings2, + containsAll([ + contains(androidAppId), + contains('static const FirebaseOptions web = FirebaseOptions'), + contains('static const FirebaseOptions android = FirebaseOptions'), + contains('static const FirebaseOptions ios = FirebaseOptions'), + ]), ); - // check default "firebase.json" was created and has correct content - final firebaseJsonFile = p.join(projectPath!, 'firebase.json'); - final firebaseJsonFileContent = - await File(firebaseJsonFile).readAsString(); + final startIndexWeb = listOfStrings2.indexWhere( + (line) => line.contains( + 'if (kIsWeb)', + ), + ); - final decodedFirebaseJson = - jsonDecode(firebaseJsonFileContent) as Map; + listOfStrings2[startIndexWeb + 1].contains( + 'return web;', + ); - checkAppleFirebaseJsonValues( - decodedFirebaseJson, - [kFlutter, kPlatforms, kIos, kDefaultConfig], - '$kIos/$defaultTarget/$appleServiceFileName', + final startIndexAndroid = listOfStrings2.indexWhere( + (line) => line.contains( + 'case TargetPlatform.android:', + ), ); - checkAndroidFirebaseJsonValues( - decodedFirebaseJson, - [ - kFlutter, - kPlatforms, - kAndroid, - kDefaultConfig, - ], - 'android/app/$androidServiceFileName', + listOfStrings2[startIndexAndroid + 1].contains( + 'return android;', ); - const defaultFilePath = 'lib/firebase_options.dart'; - final keysToMapDart = [kFlutter, kPlatforms, kDart, defaultFilePath]; + final startIndexIos = listOfStrings2.indexWhere( + (line) => line.contains( + 'case TargetPlatform.iOS:', + ), + ); - checkDartFirebaseJsonValues( - decodedFirebaseJson, - keysToMapDart, - checkMacos: false, - checkWeb: false, + listOfStrings2[startIndexIos + 1].contains( + 'return ios;', ); - // check GoogleService-Info.plist file is included & debug symbols script (until firebase crashlytics is a dependency) is not included in Apple "project.pbxproj" files - final iosXcodeProject = p.join( - projectPath!, - kIos, - 'Runner.xcodeproj', + final startIndexMacos = listOfStrings2.indexWhere( + (line) => line.contains( + 'case TargetPlatform.macOS:', + ), ); - final scriptToCheckIosPbxprojFile = - rubyScriptForTestingDefaultConfigure(iosXcodeProject); + listOfStrings2[startIndexMacos + 1].contains( + 'return macos;', + ); - final iosResult = Process.runSync( - 'ruby', + // Now reconfigure with different apps across platforms and check Dart file is updated correctly + final result3 = Process.runSync( + 'flutterfire', [ - '-e', - scriptToCheckIosPbxprojFile, + 'configure', + '--yes', + '--project=$firebaseProjectId', + '--platforms=ios,macos,web,android,windows', + '--ios-bundle-id=$secondAppleBundleId', + '--android-package-name=$secondAndroidApplicationId', + '--macos-bundle-id=$secondAppleBundleId', + '--web-app-id=$secondWebAppId', + '--windows-app-id=$secondWindowsAppId', ], + workingDirectory: projectPath, + runInShell: true, ); - if (iosResult.exitCode != 0) { - fail(iosResult.stderr as String); + if (result3.exitCode != 0) { + fail(result.stderr as String); } - expect(iosResult.stdout, 'success'); - } - - // check google-services.json was created and has correct content - final androidServiceFilePath = p.join( - projectPath!, - 'android', - 'app', - androidServiceFileName, - ); - testAndroidServiceFileValues( - androidServiceFilePath, - ); - - // check "firebase_options.dart" file is created in lib directory - final firebaseOptions = - p.join(projectPath!, 'lib', 'firebase_options.dart'); - - final firebaseOptionsContent = await File(firebaseOptions).readAsString(); - - final listOfStrings = firebaseOptionsContent.split('\n'); - expect( - listOfStrings, - containsAll([ - contains(appleAppId), - contains(appleBundleId), - contains(androidAppId), - contains('static const FirebaseOptions android = FirebaseOptions'), - contains('static const FirebaseOptions ios = FirebaseOptions'), - ]), - ); - expect( - firebaseOptionsContent - .contains('static const FirebaseOptions web = FirebaseOptions'), - isFalse, - ); - expect( - firebaseOptionsContent - .contains('static const FirebaseOptions windows = FirebaseOptions'), - isFalse, - ); - }, - timeout: const Timeout( - Duration(minutes: 2), - ), - ); - - test( - 'flutterfire configure: test will reconfigure project if no args and `firebase.json` is present', - () async { - const defaultTarget = 'Runner'; - // Set up initial configuration - final result = Process.runSync( - 'flutterfire', - [ - 'configure', - '--yes', - '--project=$firebaseProjectId', - '--platforms=android,ios', - ], - workingDirectory: projectPath, - runInShell: true, - ); - - if (result.exitCode != 0) { - fail(result.stderr as String); - } - - // Now firebase.json file has been written, change values in service files to test they are rewritten - if (Platform.isMacOS) { - final iosPath = - p.join(projectPath!, kIos, defaultTarget, appleServiceFileName); - - // Clean out file to test it was recreated - await File(iosPath).writeAsString(''); - } - - final androidServiceFilePath = p.join( - projectPath!, - 'android', - 'app', - androidServiceFileName, - ); - // Clean out file to test it was recreated - await File(androidServiceFilePath).writeAsString(''); - - final firebaseOptions = - p.join(projectPath!, 'lib', 'firebase_options.dart'); - - final accessToken = await generateAccessTokenCI(); - // Perform `flutterfire configure` without args to use `flutterfire reconfigure`. - final result2 = Process.runSync( - 'flutterfire', - [ - 'configure', - if (accessToken != null) '--test-access-token=$accessToken', - ], - workingDirectory: projectPath, - runInShell: true, - environment: {'TEST_ENVIRONMENT': 'true'}, - ); - - if (result2.exitCode != 0) { - fail(result2.stderr as String); - } - - if (Platform.isMacOS) { - // check iOS service file was recreated and has correct content - final iosPath = - p.join(projectPath!, kIos, defaultTarget, appleServiceFileName); - - await testAppleServiceFileValues( - iosPath, + final firebaseOptionsContent3 = + await File(firebaseOptions).readAsString(); + final listOfStrings3 = firebaseOptionsContent3.split('\n'); + + expect( + listOfStrings3, + containsAll([ + contains(secondAndroidAppId), + contains(secondAppleAppId), + contains(secondWebAppId), + contains(secondWindowsAppId), + ]), ); - } - - // check google-services.json was recreated and has correct content - testAndroidServiceFileValues( - androidServiceFilePath, - ); - - // check "firebase_options.dart" file was recreated in lib directory - final firebaseOptionsContent = await File(firebaseOptions).readAsString(); + }, + timeout: const Timeout( + Duration(minutes: 2), + ), + ); - final listOfStrings = firebaseOptionsContent.split('\n'); - expect( - listOfStrings, - containsAll([ - contains(appleAppId), - contains(appleBundleId), - contains(androidAppId), - contains('static const FirebaseOptions android = FirebaseOptions'), - contains('static const FirebaseOptions ios = FirebaseOptions'), - ]), - ); - expect( - firebaseOptionsContent - .contains('static const FirebaseOptions web = FirebaseOptions'), - isFalse, - ); - expect( - firebaseOptionsContent - .contains('static const FirebaseOptions windows = FirebaseOptions'), - isFalse, - ); - }, - timeout: const Timeout( - Duration(minutes: 2), - ), - ); - - test( - 'flutterfire configure: write Dart configuration file to different output', - () async { - const configurationFileName = 'different_firebase_options.dart'; - // Set up initial configuration + test( + 'flutterfire configure: ensure android build.gradle files are only updated once', + () async { + // Add crashlytics and performance to check they are only created once final result = Process.runSync( - 'flutterfire', - [ - 'configure', - '--yes', - '--project=$firebaseProjectId', - '--platforms=android,ios', - // The below args aren't needed unless running from CI. We need for Github actions to run command. - '--platforms=android,ios,macos,web,windows', - '--web-app-id=$webAppId', - '--windows-app-id=$windowsAppId', - // Output to different file - '--out=lib/$configurationFileName', - ], + 'flutter', + ['pub', 'add', 'firebase_crashlytics', 'firebase_performance'], workingDirectory: projectPath, - runInShell: true, ); if (result.exitCode != 0) { fail(result.stderr as String); } - - final firebaseOptions = - p.join(projectPath!, 'lib', configurationFileName); - - // check "different_firebase_options.dart" file was recreated in lib directory - final firebaseOptionsContent = await File(firebaseOptions).readAsString(); - - final listOfStrings = firebaseOptionsContent.split('\n'); - expect( - listOfStrings, - containsAll([ - contains(appleAppId), - contains(appleBundleId), - contains(androidAppId), - contains(webAppId), - contains(windowsAppId), - contains('static const FirebaseOptions android = FirebaseOptions'), - contains('static const FirebaseOptions ios = FirebaseOptions'), - contains('static const FirebaseOptions web = FirebaseOptions'), - contains('static const FirebaseOptions windows = FirebaseOptions'), - ]), - ); - }, - timeout: const Timeout( - Duration(minutes: 2), - ), - ); - - test('flutterfire configure: incorrect `--web-app-id` should throw exception', - () async { - final result = Process.runSync( - 'flutterfire', - [ - 'configure', - '--yes', - '--project=$firebaseProjectId', - '--platforms=web', - '--web-app-id=a-non-existent-web-app-id', - ], - workingDirectory: projectPath, - runInShell: true, - ); - - final output = result.stderr as String; - final expectedOutput = [ - 'does not match the web app id of any existing Firebase app', - 'Exception', - ]; - - final expected = expectedOutput.every(output.contains); - expect(result.exitCode != 0, isTrue); - expect( - expected, - isTrue, - ); - }); - - test( - 'flutterfire configure: incorrect `--windows-app-id` should throw exception', - () async { - final result = Process.runSync( - 'flutterfire', - [ - 'configure', - '--yes', - '--project=$firebaseProjectId', - '--platforms=windows', - // Trigger the exception - '--windows-app-id=a-non-existent-windows-app-id', - ], - workingDirectory: projectPath, - runInShell: true, - ); - - final output = result.stderr as String; - final expectedOutput = [ - 'does not match the web app id of any existing Firebase app', - 'Exception', - ]; - - final expected = expectedOutput.every(output.contains); - - expect(result.exitCode != 0, isTrue); - expect( - expected, - isTrue, - ); - }); - - test( - 'flutterfire configure: get correct Firebase App with manually created Firebase web app via `--web-app-id`', - () async { - final result = Process.runSync( - 'flutterfire', - [ - 'configure', - '--yes', - '--project=$firebaseProjectId', - '--platforms=web', - '--web-app-id=$secondWebAppId', - ], - workingDirectory: projectPath, - runInShell: true, - ); - - expect(result.exitCode, 0); - // Console out put looks like this on success: "web 1:262904632156:web:cb3a00412ed430ca2f2799" - expect( - (result.stdout as String).contains( - 'web $secondWebAppId', - ), - isTrue, - ); - - // check "firebase_options.dart" file is created in lib directory - final firebaseOptions = - p.join(projectPath!, 'lib', 'firebase_options.dart'); - - await testFirebaseOptionsFileValues( - firebaseOptions, - selectedPlatform: kWeb, - ); - }); - - test( - 'flutterfire configure: get correct Firebase App with manually created Firebase web app via `--windows-app-id`', - () async { - final result = Process.runSync( - 'flutterfire', - [ - 'configure', - '--yes', - '--project=$firebaseProjectId', - '--platforms=windows', - '--windows-app-id=$secondWindowsAppId', - ], - workingDirectory: projectPath, - runInShell: true, - ); - - expect(result.exitCode, 0); - - // check "firebase_options.dart" file is created in lib directory - final firebaseOptions = - p.join(projectPath!, 'lib', 'firebase_options.dart'); - - await testFirebaseOptionsFileValues( - firebaseOptions, - selectedPlatform: kWindows, - ); - }); - - test( - 'flutterfire configure: check Dart file configuration is updated correctly', - () async { - // Set up initial configuration - final result = Process.runSync( + // Run first time to update + Process.runSync( 'flutterfire', [ 'configure', '--yes', - '--project=$firebaseProjectId', - // Only configuring android initially + // Only android '--platforms=android', ], workingDirectory: projectPath, runInShell: true, ); - - if (result.exitCode != 0) { - fail(result.stderr as String); - } - - final firebaseOptions = p.join( - projectPath!, - 'lib', - 'firebase_options.dart', - ); - - // check "firebase_options.dart" file was created and updates android only - final firebaseOptionsContent = await File(firebaseOptions).readAsString(); - - // only configure android initially - final listOfStrings = firebaseOptionsContent.split('\n'); - expect( - listOfStrings, - containsAll([ - contains(androidAppId), - contains('static const FirebaseOptions android = FirebaseOptions'), - ]), - ); - expect( - firebaseOptionsContent - .contains('static const FirebaseOptions web = FirebaseOptions'), - isFalse, - ); - expect( - firebaseOptionsContent - .contains('static const FirebaseOptions ios = FirebaseOptions'), - isFalse, - ); - - // Now reconfigure with ios, macos & web platforms and check Dart file is updated correctly + // Run second time and check it was only updated once final result2 = Process.runSync( 'flutterfire', [ 'configure', '--yes', '--project=$firebaseProjectId', - // Configure the rest of the platforms - '--platforms=ios,macos,web', - '--web-app-id=$webAppId', + // Only android + '--platforms=android', ], workingDirectory: projectPath, runInShell: true, ); if (result2.exitCode != 0) { - fail(result.stderr as String); + fail(result2.stderr as String); } - final firebaseOptionsContent2 = - await File(firebaseOptions).readAsString(); - final listOfStrings2 = firebaseOptionsContent2.split('\n'); - - expect( - listOfStrings2, - containsAll([ - contains(androidAppId), - contains('static const FirebaseOptions web = FirebaseOptions'), - contains('static const FirebaseOptions android = FirebaseOptions'), - contains('static const FirebaseOptions ios = FirebaseOptions'), - ]), + await checkBuildGradleFileUpdated( + projectPath!, + checkCrashlytics: true, + checkPerf: true, ); + }); - final startIndexWeb = listOfStrings2.indexWhere( - (line) => line.contains( - 'if (kIsWeb)', - ), - ); + test( + 'flutterfire configure: path with spaces should not break the configuration for iOS/macOS', + () async { + // Regression test for https://github.com/invertase/flutterfire_cli/pull/228 + const targetType = 'Runner'; + const iOSPath = 'ios path with spaces/target'; + const macOSPath = 'macos path with spaces/target'; + final result = Process.runSync( + 'flutterfire', + [ + 'configure', + '--yes', + '--project=$firebaseProjectId', + // The below args aren't needed unless running from CI. We need for Github actions to run command. + '--platforms=ios,macos', + // Apple required the `--ios-out` and `--macos-out` flags to be set & the build type, + // We're using `Runner` target for both which is the standard target for an apple Flutter app + '--ios-out=ios/$iOSPath', + '--ios-target=$targetType', + '--macos-out=macos/$macOSPath', + '--macos-target=$targetType', + ], + workingDirectory: projectPath, + runInShell: true, + ); - listOfStrings2[startIndexWeb + 1].contains( - 'return web;', - ); + if (result.exitCode != 0) { + fail(result.stderr as String); + } - final startIndexAndroid = listOfStrings2.indexWhere( - (line) => line.contains( - 'case TargetPlatform.android:', - ), - ); + if (Platform.isMacOS) { + // check Apple service files were created and have correct content + final iosPath = + p.join(projectPath!, kIos, iOSPath, appleServiceFileName); + final macosPath = p.join(projectPath!, kMacos, macOSPath); + + await testAppleServiceFileValues(iosPath); + await testAppleServiceFileValues( + macosPath, + platform: kMacos, + ); + } + }, + skip: !Platform.isMacOS, + timeout: const Timeout( + Duration(minutes: 2), + ), + ); + }); - listOfStrings2[startIndexAndroid + 1].contains( - 'return android;', - ); + // Separate group for tests that need iOS 15.0 + group('FlutterFire configure tests (iOS 15.0)', () { + String? projectPath; + setUp(() async { + projectPath = await createFlutterProject(updateiOSBuildTarget: true); + }); - final startIndexIos = listOfStrings2.indexWhere( - (line) => line.contains( - 'case TargetPlatform.iOS:', - ), - ); + tearDown(() { + Directory(p.dirname(projectPath!)).delete(recursive: true); + }); - listOfStrings2[startIndexIos + 1].contains( - 'return ios;', - ); + test( + 'Validate dev dependency works as normal & `flutterfire upload-crashlytics-symbols` works via dev dependency', + () async { + final result = Process.runSync( + 'flutter', + ['pub', 'add', 'firebase_crashlytics'], + workingDirectory: projectPath, + ); - final startIndexMacos = listOfStrings2.indexWhere( - (line) => line.contains( - 'case TargetPlatform.macOS:', - ), - ); + if (result.exitCode != 0) { + fail(result.stderr as String); + } - listOfStrings2[startIndexMacos + 1].contains( - 'return macos;', - ); + final installDevDependency = Process.runSync( + 'flutter', + [ + 'pub', + 'add', + '--dev', + 'flutterfire_cli', + '--path=${Directory.current.path}', + ], + workingDirectory: projectPath, + ); - // Now reconfigure with different apps across platforms and check Dart file is updated correctly - final result3 = Process.runSync( - 'flutterfire', - [ - 'configure', - '--yes', - '--project=$firebaseProjectId', - '--platforms=ios,macos,web,android,windows', - '--ios-bundle-id=$secondAppleBundleId', - '--android-package-name=$secondAndroidApplicationId', - '--macos-bundle-id=$secondAppleBundleId', - '--web-app-id=$secondWebAppId', - '--windows-app-id=$secondWindowsAppId', - ], - workingDirectory: projectPath, - runInShell: true, - ); + if (installDevDependency.exitCode != 0) { + fail(installDevDependency.stderr as String); + } - if (result3.exitCode != 0) { - fail(result.stderr as String); - } + final result2 = Process.runSync( + 'dart', + [ + 'run', + 'flutterfire_cli:flutterfire', + 'configure', + '--yes', + '--project=$firebaseProjectId', + '--platforms=ios,macos', + // Need to test bundle script is written correctly for both build configurations + '--ios-build-config=$appleBuildConfiguration', + '--ios-out=ios/$buildType', + '--macos-build-config=$appleBuildConfiguration', + '--macos-out=macos/$buildType', + ], + workingDirectory: projectPath, + runInShell: true, + ); - final firebaseOptionsContent3 = - await File(firebaseOptions).readAsString(); - final listOfStrings3 = firebaseOptionsContent3.split('\n'); + if (result2.exitCode != 0) { + fail(result2.stderr as String); + } - expect( - listOfStrings3, - containsAll([ - contains(secondAndroidAppId), - contains(secondAppleAppId), - contains(secondWebAppId), - contains(secondWindowsAppId), - ]), - ); - }, - timeout: const Timeout( - Duration(minutes: 2), - ), - ); - - test( - 'flutterfire configure: ensure android build.gradle files are only updated once', - () async { - // Add crashlytics and performance to check they are only created once - final result = Process.runSync( - 'flutter', - ['pub', 'add', 'firebase_crashlytics', 'firebase_performance'], - workingDirectory: projectPath, - ); + // Run grep to check for both strings + final grepDevDependencyScriptsAdded = Process.runSync( + 'grep', + [ + '-q', + '-E', + 'dart run flutterfire_cli:flutterfire (upload-crashlytics-symbols|bundle-service-file)', + 'ios/Runner.xcodeproj/project.pbxproj', + ], + workingDirectory: projectPath, + ); - if (result.exitCode != 0) { - fail(result.stderr as String); - } - // Run first time to update - Process.runSync( - 'flutterfire', - [ - 'configure', - '--yes', - // Only android - '--platforms=android', - ], - workingDirectory: projectPath, - runInShell: true, - ); - // Run second time and check it was only updated once - final result2 = Process.runSync( - 'flutterfire', - [ - 'configure', - '--yes', - '--project=$firebaseProjectId', - // Only android - '--platforms=android', - ], - workingDirectory: projectPath, - runInShell: true, - ); + // Exit code 0 means both strings were found + expect( + grepDevDependencyScriptsAdded.exitCode, + 0, + reason: 'Required FlutterFire scripts not found in project.pbxproj', + ); - if (result2.exitCode != 0) { - fail(result2.stderr as String); - } + const iosVersion = '13.0'; + // Update project.pbxproj + final pbxprojResult = Process.runSync( + 'sed', + [ + '-i', + '', + 's/IPHONEOS_DEPLOYMENT_TARGET = [0-9.]*;/IPHONEOS_DEPLOYMENT_TARGET = $iosVersion;/', + 'ios/Runner.xcodeproj/project.pbxproj', + ], + workingDirectory: projectPath, + ); - await checkBuildGradleFileUpdated( - projectPath!, - checkCrashlytics: true, - checkPerf: true, - ); - }); + if (pbxprojResult.exitCode != 0) { + fail(pbxprojResult.stderr as String); + } - test( - 'flutterfire configure: path with spaces should not break the configuration for iOS/macOS', - () async { - // Regression test for https://github.com/invertase/flutterfire_cli/pull/228 - const targetType = 'Runner'; - const iOSPath = 'ios path with spaces/target'; - const macOSPath = 'macos path with spaces/target'; - final result = Process.runSync( - 'flutterfire', - [ - 'configure', - '--yes', - '--project=$firebaseProjectId', - // The below args aren't needed unless running from CI. We need for Github actions to run command. - '--platforms=ios,macos', - // Apple required the `--ios-out` and `--macos-out` flags to be set & the build type, - // We're using `Runner` target for both which is the standard target for an apple Flutter app - '--ios-out=ios/$iOSPath', - '--ios-target=$targetType', - '--macos-out=macos/$macOSPath', - '--macos-target=$targetType', - ], - workingDirectory: projectPath, - runInShell: true, - ); + final buildApp = Process.runSync( + 'flutter', + [ + 'build', + 'ios', + '--no-codesign', + '--simulator', + '--debug', + '--verbose', + ], + workingDirectory: projectPath, + runInShell: true, + ); - if (result.exitCode != 0) { - fail(result.stderr as String); - } + if (buildApp.exitCode != 0) { + fail(buildApp.stderr as String); + } - if (Platform.isMacOS) { - // check Apple service files were created and have correct content - final iosPath = - p.join(projectPath!, kIos, iOSPath, appleServiceFileName); - final macosPath = p.join(projectPath!, kMacos, macOSPath); + expect( + buildApp.stdout, + // Check symbols are uploaded in the background + contains('Symbol uploading will proceed in the background'), + ); - await testAppleServiceFileValues(iosPath); - await testAppleServiceFileValues( - macosPath, - platform: kMacos, + Process.runSync( + 'flutter', + ['config', '--enable-swift-package-manager'], + workingDirectory: projectPath, ); - } - }, - skip: !Platform.isMacOS, - timeout: const Timeout( - Duration(minutes: 2), - ), - ); - - test( - 'Validate dev dependency works as normal & `flutterfire upload-crashlytics-symbols` works via dev dependency', - () async { - final result = Process.runSync( - 'flutter', - ['pub', 'add', 'firebase_crashlytics'], - workingDirectory: projectPath, - ); - if (result.exitCode != 0) { - fail(result.stderr as String); - } + final iosDirectory = p.join(projectPath!, 'ios'); - final installDevDependency = Process.runSync( - 'flutter', - [ - 'pub', - 'add', - '--dev', - 'flutterfire_cli', - '--path=${Directory.current.path}', - ], - workingDirectory: projectPath, - ); + Process.runSync( + 'bash', + [ + '-c', + '[ -f Podfile ] && rm Podfile && pod deintegrate && rm -rf Pods/', + ], + workingDirectory: iosDirectory, + ); - if (installDevDependency.exitCode != 0) { - fail(installDevDependency.stderr as String); - } + final buildAppSPM = Process.runSync( + 'flutter', + [ + 'build', + 'ios', + '--no-codesign', + '--simulator', + '--debug', + '--verbose', + ], + workingDirectory: projectPath, + runInShell: true, + ); - final result2 = Process.runSync( - 'dart', - [ - 'run', - 'flutterfire_cli:flutterfire', - 'configure', - '--yes', - '--project=$firebaseProjectId', - '--platforms=ios,macos', - // Need to test bundle script is written correctly for both build configurations - '--ios-build-config=$appleBuildConfiguration', - '--ios-out=ios/$buildType', - '--macos-build-config=$appleBuildConfiguration', - '--macos-out=macos/$buildType', - ], - workingDirectory: projectPath, - runInShell: true, - ); + if (buildAppSPM.exitCode != 0) { + fail(buildAppSPM.stderr as String); + } - if (result2.exitCode != 0) { - fail(result2.stderr as String); - } + expect( + buildAppSPM.stdout, + // Check symbols are uploaded in the background + contains('Symbol uploading will proceed in the background'), + ); + }, + skip: !Platform.isMacOS, + timeout: const Timeout( + Duration(minutes: 3), + ), + ); - // Run grep to check for both strings - final grepDevDependencyScriptsAdded = Process.runSync( - 'grep', - [ - '-q', - '-E', - 'dart run flutterfire_cli:flutterfire (upload-crashlytics-symbols|bundle-service-file)', - 'ios/Runner.xcodeproj/project.pbxproj', - ], - workingDirectory: projectPath, - ); + test( + 'Validate `flutterfire upload-crashlytics-symbols` script is ran when building app', + () async { + final result = Process.runSync( + 'flutter', + ['pub', 'add', 'firebase_crashlytics'], + workingDirectory: projectPath, + ); - // Exit code 0 means both strings were found - expect( - grepDevDependencyScriptsAdded.exitCode, - 0, - reason: 'Required FlutterFire scripts not found in project.pbxproj', - ); + if (result.exitCode != 0) { + fail(result.stderr as String); + } - const iosVersion = '13.0'; - // Update project.pbxproj - final pbxprojResult = Process.runSync( - 'sed', - [ - '-i', - '', - 's/IPHONEOS_DEPLOYMENT_TARGET = [0-9.]*;/IPHONEOS_DEPLOYMENT_TARGET = $iosVersion;/', - 'ios/Runner.xcodeproj/project.pbxproj', - ], - workingDirectory: projectPath, - ); + final result2 = Process.runSync( + 'flutterfire', + [ + 'configure', + '--yes', + '--project=$firebaseProjectId', + '--platforms=android,ios,macos,web,windows', + '--web-app-id=$webAppId', + '--windows-app-id=$windowsAppId', + ], + workingDirectory: projectPath, + runInShell: true, + ); - if (pbxprojResult.exitCode != 0) { - fail(pbxprojResult.stderr as String); - } + if (result2.exitCode != 0) { + fail(result2.stderr as String); + } - final buildApp = Process.runSync( - 'flutter', - [ - 'build', - 'ios', - '--no-codesign', - '--simulator', - '--debug', - '--verbose', - ], - workingDirectory: projectPath, - runInShell: true, - ); + // Project is created with iOS 15.0 deployment target, no manual update needed + final buildApp = Process.runSync( + 'flutter', + [ + 'build', + 'ios', + '--no-codesign', + '--simulator', + '--debug', + '--verbose', + ], + workingDirectory: projectPath, + runInShell: true, + ); - if (buildApp.exitCode != 0) { - fail(buildApp.stderr as String); - } + if (buildApp.exitCode != 0) { + fail(buildApp.stderr as String); + } - expect( - buildApp.stdout, - // Check symbols are uploaded in the background - contains('Symbol uploading will proceed in the background'), - ); + expect( + buildApp.stdout, + // Check symbols are uploaded in the background + contains('Symbol uploading will proceed in the background'), + ); - Process.runSync( - 'flutter', - ['config', '--enable-swift-package-manager'], - workingDirectory: projectPath, - ); + Process.runSync( + 'flutter', + ['config', '--enable-swift-package-manager'], + workingDirectory: projectPath, + ); - final iosDirectory = p.join(projectPath!, 'ios'); + final iosDirectory = p.join(projectPath!, 'ios'); - Process.runSync( - 'bash', - [ - '-c', - '[ -f Podfile ] && rm Podfile && pod deintegrate && rm -rf Pods/', - ], - workingDirectory: iosDirectory, - ); + Process.runSync( + 'bash', + [ + '-c', + '[ -f Podfile ] && rm Podfile && pod deintegrate && rm -rf Pods/', + ], + workingDirectory: iosDirectory, + ); - final buildAppSPM = Process.runSync( - 'flutter', - [ - 'build', - 'ios', - '--no-codesign', - '--simulator', - '--debug', - '--verbose', - ], - workingDirectory: projectPath, - runInShell: true, - ); + final buildAppSPM = Process.runSync( + 'flutter', + [ + 'build', + 'ios', + '--no-codesign', + '--simulator', + '--debug', + '--verbose', + ], + workingDirectory: projectPath, + runInShell: true, + ); - if (buildAppSPM.exitCode != 0) { - fail(buildAppSPM.stderr as String); - } + if (buildAppSPM.exitCode != 0) { + fail(buildAppSPM.stderr as String); + } - expect( - buildAppSPM.stdout, - // Check symbols are uploaded in the background - contains('Symbol uploading will proceed in the background'), - ); - }, - skip: !Platform.isMacOS, - timeout: const Timeout( - Duration(minutes: 3), - ), - ); + expect( + buildAppSPM.stdout, + // Check symbols are uploaded in the background + contains('Symbol uploading will proceed in the background'), + ); + }, + skip: !Platform.isMacOS, + timeout: const Timeout( + Duration(minutes: 3), + ), + ); + }); } diff --git a/packages/flutterfire_cli/test/test_utils.dart b/packages/flutterfire_cli/test/test_utils.dart index 5a31e9f6..a7fdb140 100644 --- a/packages/flutterfire_cli/test/test_utils.dart +++ b/packages/flutterfire_cli/test/test_utils.dart @@ -43,7 +43,7 @@ const androidAppGradleUpdate = ''' // END: FlutterFire Configuration '''; -Future createFlutterProject() async { +Future createFlutterProject({bool updateiOSBuildTarget = false}) async { final tempDir = Directory.systemTemp.createTempSync(); const flutterProject = 'flutter_test_cli'; await Process.run( @@ -55,37 +55,39 @@ Future createFlutterProject() async { final flutterProjectPath = p.join(tempDir.path, flutterProject); - // Set iOS minimum deployment target to 15.0 - const iosVersion = '15.0'; - - // Update project.pbxproj - final pbxprojResult = await Process.run( - 'sed', - [ - '-i', - '', - 's/IPHONEOS_DEPLOYMENT_TARGET = [0-9.]*;/IPHONEOS_DEPLOYMENT_TARGET = $iosVersion;/', - 'ios/Runner.xcodeproj/project.pbxproj', - ], - workingDirectory: flutterProjectPath, - ); - - if (pbxprojResult.exitCode != 0) { - throw Exception( - 'Failed to set iOS deployment target: ${pbxprojResult.stderr}', + if (updateiOSBuildTarget && Platform.isMacOS) { + // Set iOS minimum deployment target to 15.0 + const iosVersion = '15.0'; + + // Update project.pbxproj + final pbxprojResult = await Process.run( + 'sed', + [ + '-i', + '', + 's/IPHONEOS_DEPLOYMENT_TARGET = [0-9.]*;/IPHONEOS_DEPLOYMENT_TARGET = $iosVersion;/', + 'ios/Runner.xcodeproj/project.pbxproj', + ], + workingDirectory: flutterProjectPath, ); - } - // Update Podfile if it exists - final podfilePath = p.join(flutterProjectPath, 'ios', 'Podfile'); - final podfile = File(podfilePath); - if (podfile.existsSync()) { - var podfileContent = podfile.readAsStringSync(); - podfileContent = podfileContent.replaceFirst( - RegExp("platform :ios, '[0-9.]+'"), - "platform :ios, '$iosVersion'", - ); - podfile.writeAsStringSync(podfileContent); + if (pbxprojResult.exitCode != 0) { + throw Exception( + 'Failed to set iOS deployment target: ${pbxprojResult.stderr}', + ); + } + + // Update Podfile if it exists + final podfilePath = p.join(flutterProjectPath, 'ios', 'Podfile'); + final podfile = File(podfilePath); + if (podfile.existsSync()) { + var podfileContent = podfile.readAsStringSync(); + podfileContent = podfileContent.replaceFirst( + RegExp("platform :ios, '[0-9.]+'"), + "platform :ios, '$iosVersion'", + ); + podfile.writeAsStringSync(podfileContent); + } } return flutterProjectPath; From a4bb864799ee392cf1ece11607c6f028dc9fc98c Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Wed, 17 Dec 2025 12:58:26 +0000 Subject: [PATCH 13/19] test: update deployment target to 15 for tests that build --- .../flutterfire_cli/test/configure_test.dart | 21 ++-------------- packages/flutterfire_cli/test/test_utils.dart | 24 +++++++------------ 2 files changed, 11 insertions(+), 34 deletions(-) diff --git a/packages/flutterfire_cli/test/configure_test.dart b/packages/flutterfire_cli/test/configure_test.dart index ae75057c..c924fc2f 100644 --- a/packages/flutterfire_cli/test/configure_test.dart +++ b/packages/flutterfire_cli/test/configure_test.dart @@ -10,7 +10,7 @@ import 'test_utils.dart'; void main() { // Default group for tests that don't need iOS 15.0 - group('FlutterFire configure tests', () { + group('FlutterFire configure tests - no build', () { String? projectPath; setUp(() async { projectPath = await createFlutterProject(); @@ -1391,7 +1391,7 @@ void main() { }); // Separate group for tests that need iOS 15.0 - group('FlutterFire configure tests (iOS 15.0)', () { + group('FlutterFire configure tests - build target (iOS 15.0)', () { String? projectPath; setUp(() async { projectPath = await createFlutterProject(updateiOSBuildTarget: true); @@ -1472,23 +1472,6 @@ void main() { reason: 'Required FlutterFire scripts not found in project.pbxproj', ); - const iosVersion = '13.0'; - // Update project.pbxproj - final pbxprojResult = Process.runSync( - 'sed', - [ - '-i', - '', - 's/IPHONEOS_DEPLOYMENT_TARGET = [0-9.]*;/IPHONEOS_DEPLOYMENT_TARGET = $iosVersion;/', - 'ios/Runner.xcodeproj/project.pbxproj', - ], - workingDirectory: projectPath, - ); - - if (pbxprojResult.exitCode != 0) { - fail(pbxprojResult.stderr as String); - } - final buildApp = Process.runSync( 'flutter', [ diff --git a/packages/flutterfire_cli/test/test_utils.dart b/packages/flutterfire_cli/test/test_utils.dart index a7fdb140..9ebab701 100644 --- a/packages/flutterfire_cli/test/test_utils.dart +++ b/packages/flutterfire_cli/test/test_utils.dart @@ -58,15 +58,21 @@ Future createFlutterProject({bool updateiOSBuildTarget = false}) async { if (updateiOSBuildTarget && Platform.isMacOS) { // Set iOS minimum deployment target to 15.0 const iosVersion = '15.0'; + final pbxprojPath = p.join( + flutterProjectPath, + 'ios', + 'Runner.xcodeproj', + 'project.pbxproj', + ); - // Update project.pbxproj + // Update project.pbxproj - use 'g' flag to replace ALL occurrences final pbxprojResult = await Process.run( 'sed', [ '-i', '', - 's/IPHONEOS_DEPLOYMENT_TARGET = [0-9.]*;/IPHONEOS_DEPLOYMENT_TARGET = $iosVersion;/', - 'ios/Runner.xcodeproj/project.pbxproj', + 's/IPHONEOS_DEPLOYMENT_TARGET = [0-9.]*;/IPHONEOS_DEPLOYMENT_TARGET = $iosVersion;/g', + pbxprojPath, ], workingDirectory: flutterProjectPath, ); @@ -76,18 +82,6 @@ Future createFlutterProject({bool updateiOSBuildTarget = false}) async { 'Failed to set iOS deployment target: ${pbxprojResult.stderr}', ); } - - // Update Podfile if it exists - final podfilePath = p.join(flutterProjectPath, 'ios', 'Podfile'); - final podfile = File(podfilePath); - if (podfile.existsSync()) { - var podfileContent = podfile.readAsStringSync(); - podfileContent = podfileContent.replaceFirst( - RegExp("platform :ios, '[0-9.]+'"), - "platform :ios, '$iosVersion'", - ); - podfile.writeAsStringSync(podfileContent); - } } return flutterProjectPath; From 416f8bbbb84bf4e3d19e5fae3e64ba380991b77d Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Wed, 17 Dec 2025 12:59:48 +0000 Subject: [PATCH 14/19] chore: format --- packages/flutterfire_cli/test/configure_test.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/flutterfire_cli/test/configure_test.dart b/packages/flutterfire_cli/test/configure_test.dart index c924fc2f..c4a4398d 100644 --- a/packages/flutterfire_cli/test/configure_test.dart +++ b/packages/flutterfire_cli/test/configure_test.dart @@ -831,7 +831,8 @@ void main() { ); expect( firebaseOptionsContent.contains( - 'static const FirebaseOptions windows = FirebaseOptions'), + 'static const FirebaseOptions windows = FirebaseOptions', + ), isFalse, ); }, @@ -936,7 +937,8 @@ void main() { ); expect( firebaseOptionsContent.contains( - 'static const FirebaseOptions windows = FirebaseOptions'), + 'static const FirebaseOptions windows = FirebaseOptions', + ), isFalse, ); }, From 73b7098bd530dbb44d1143578030e77764810f66 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Wed, 17 Dec 2025 13:31:48 +0000 Subject: [PATCH 15/19] test: use device id to run app --- .github/workflows/validate.yaml | 2 + .../flutterfire_cli/test/configure_test.dart | 46 ++++++++++------ packages/flutterfire_cli/test/test_utils.dart | 53 +++++++++++++++++++ 3 files changed, 85 insertions(+), 16 deletions(-) diff --git a/.github/workflows/validate.yaml b/.github/workflows/validate.yaml index b2eefa7a..b470dfd3 100644 --- a/.github/workflows/validate.yaml +++ b/.github/workflows/validate.yaml @@ -107,6 +107,8 @@ jobs: - uses: actions/checkout@v2 with: ref: ${{ github.event.pull_request.head.sha || github.ref }} + - name: Select Xcode version + run: sudo xcode-select -switch /Applications/Xcode_26.0.1.app/Contents/Developer - uses: subosito/flutter-action@v2 with: channel: 'stable' diff --git a/packages/flutterfire_cli/test/configure_test.dart b/packages/flutterfire_cli/test/configure_test.dart index c4a4398d..54743f9d 100644 --- a/packages/flutterfire_cli/test/configure_test.dart +++ b/packages/flutterfire_cli/test/configure_test.dart @@ -1474,16 +1474,23 @@ void main() { reason: 'Required FlutterFire scripts not found in project.pbxproj', ); + // Find iOS 26.0 simulator and use it + final deviceId = findIOS26Simulator(); + final buildArgs = [ + 'build', + 'ios', + '--no-codesign', + '--simulator', + '--debug', + '--verbose', + ]; + if (deviceId != null) { + buildArgs.add('--device-id=$deviceId'); + } + final buildApp = Process.runSync( 'flutter', - [ - 'build', - 'ios', - '--no-codesign', - '--simulator', - '--debug', - '--verbose', - ], + buildArgs, workingDirectory: projectPath, runInShell: true, ); @@ -1577,16 +1584,23 @@ void main() { } // Project is created with iOS 15.0 deployment target, no manual update needed + // Find iOS 26.0 simulator and use it + final deviceId = findIOS26Simulator(); + final buildArgs = [ + 'build', + 'ios', + '--no-codesign', + '--simulator', + '--debug', + '--verbose', + ]; + if (deviceId != null) { + buildArgs.add('--device-id=$deviceId'); + } + final buildApp = Process.runSync( 'flutter', - [ - 'build', - 'ios', - '--no-codesign', - '--simulator', - '--debug', - '--verbose', - ], + buildArgs, workingDirectory: projectPath, runInShell: true, ); diff --git a/packages/flutterfire_cli/test/test_utils.dart b/packages/flutterfire_cli/test/test_utils.dart index 9ebab701..dfaa4f96 100644 --- a/packages/flutterfire_cli/test/test_utils.dart +++ b/packages/flutterfire_cli/test/test_utils.dart @@ -91,6 +91,59 @@ String normalizeLineEndings(String content) { return content.replaceAll('\r\n', '\n'); } +/// Finds an available iPhone 16 simulator device ID. +/// Returns the device ID if found, or null otherwise. +String? findIOS26Simulator() { + if (!Platform.isMacOS) { + return null; + } + + try { + final result = Process.runSync( + 'xcrun', + ['simctl', 'list', 'devices', 'available', '--json'], + runInShell: true, + ); + + if (result.exitCode != 0) { + return null; + } + + final jsonData = + jsonDecode(result.stdout as String) as Map; + final devices = jsonData['devices'] as Map; + + // Look for iPhone 16 simulators across all iOS runtimes + for (final runtimeKey in devices.keys) { + if (runtimeKey.toLowerCase().contains('ios')) { + final runtimeDevices = devices[runtimeKey] as List; + for (final device in runtimeDevices) { + final deviceMap = device as Map; + final name = deviceMap['name'] as String?; + if (name != null && name.contains('iPhone 16')) { + final state = deviceMap['state'] as String?; + if (state == 'Booted' || state == 'Shutdown') { + return deviceMap['udid'] as String?; + } + } + } + // If no booted/shutdown device, return first iPhone 16 found + for (final device in runtimeDevices) { + final deviceMap = device as Map; + final name = deviceMap['name'] as String?; + if (name != null && name.contains('iPhone 16')) { + return deviceMap['udid'] as String?; + } + } + } + } + } catch (e) { + return null; + } + + return null; +} + bool containsInOrder(String content, List lines) { var lastIndex = 0; From a72a2974d025ef46e6266035015f723141f08621 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Wed, 17 Dec 2025 13:58:34 +0000 Subject: [PATCH 16/19] test: update timeout --- .github/workflows/validate.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/validate.yaml b/.github/workflows/validate.yaml index b470dfd3..9401d510 100644 --- a/.github/workflows/validate.yaml +++ b/.github/workflows/validate.yaml @@ -83,7 +83,7 @@ jobs: test_macos: runs-on: macos-latest - timeout-minutes: 25 + timeout-minutes: 40 environment: CI Environment env: FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }} From afec4afe9ebf077b7f162d7b2e3b88bad4cd613b Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Wed, 17 Dec 2025 15:15:28 +0000 Subject: [PATCH 17/19] test: run iphone 16 for macos --- .github/workflows/validate.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/validate.yaml b/.github/workflows/validate.yaml index 9401d510..4abc2627 100644 --- a/.github/workflows/validate.yaml +++ b/.github/workflows/validate.yaml @@ -109,6 +109,9 @@ jobs: ref: ${{ github.event.pull_request.head.sha || github.ref }} - name: Select Xcode version run: sudo xcode-select -switch /Applications/Xcode_26.0.1.app/Contents/Developer + - uses: futureware-tech/simulator-action@v4 + with: + model: 'iPhone 16' - uses: subosito/flutter-action@v2 with: channel: 'stable' From a988bccc00c771b7c72964f61647237226931f4c Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Wed, 17 Dec 2025 15:38:24 +0000 Subject: [PATCH 18/19] test: remove function --- .../flutterfire_cli/test/configure_test.dart | 11 +--- packages/flutterfire_cli/test/test_utils.dart | 53 ------------------- 2 files changed, 2 insertions(+), 62 deletions(-) diff --git a/packages/flutterfire_cli/test/configure_test.dart b/packages/flutterfire_cli/test/configure_test.dart index 54743f9d..70cb6b3b 100644 --- a/packages/flutterfire_cli/test/configure_test.dart +++ b/packages/flutterfire_cli/test/configure_test.dart @@ -1474,8 +1474,6 @@ void main() { reason: 'Required FlutterFire scripts not found in project.pbxproj', ); - // Find iOS 26.0 simulator and use it - final deviceId = findIOS26Simulator(); final buildArgs = [ 'build', 'ios', @@ -1484,9 +1482,7 @@ void main() { '--debug', '--verbose', ]; - if (deviceId != null) { - buildArgs.add('--device-id=$deviceId'); - } + buildArgs.add('--device-id=98641507-FA54-4F7C-BBE9-4970C7C0EB55'); final buildApp = Process.runSync( 'flutter', @@ -1585,7 +1581,6 @@ void main() { // Project is created with iOS 15.0 deployment target, no manual update needed // Find iOS 26.0 simulator and use it - final deviceId = findIOS26Simulator(); final buildArgs = [ 'build', 'ios', @@ -1594,9 +1589,7 @@ void main() { '--debug', '--verbose', ]; - if (deviceId != null) { - buildArgs.add('--device-id=$deviceId'); - } + buildArgs.add('--device-id=98641507-FA54-4F7C-BBE9-4970C7C0EB55'); final buildApp = Process.runSync( 'flutter', diff --git a/packages/flutterfire_cli/test/test_utils.dart b/packages/flutterfire_cli/test/test_utils.dart index dfaa4f96..9ebab701 100644 --- a/packages/flutterfire_cli/test/test_utils.dart +++ b/packages/flutterfire_cli/test/test_utils.dart @@ -91,59 +91,6 @@ String normalizeLineEndings(String content) { return content.replaceAll('\r\n', '\n'); } -/// Finds an available iPhone 16 simulator device ID. -/// Returns the device ID if found, or null otherwise. -String? findIOS26Simulator() { - if (!Platform.isMacOS) { - return null; - } - - try { - final result = Process.runSync( - 'xcrun', - ['simctl', 'list', 'devices', 'available', '--json'], - runInShell: true, - ); - - if (result.exitCode != 0) { - return null; - } - - final jsonData = - jsonDecode(result.stdout as String) as Map; - final devices = jsonData['devices'] as Map; - - // Look for iPhone 16 simulators across all iOS runtimes - for (final runtimeKey in devices.keys) { - if (runtimeKey.toLowerCase().contains('ios')) { - final runtimeDevices = devices[runtimeKey] as List; - for (final device in runtimeDevices) { - final deviceMap = device as Map; - final name = deviceMap['name'] as String?; - if (name != null && name.contains('iPhone 16')) { - final state = deviceMap['state'] as String?; - if (state == 'Booted' || state == 'Shutdown') { - return deviceMap['udid'] as String?; - } - } - } - // If no booted/shutdown device, return first iPhone 16 found - for (final device in runtimeDevices) { - final deviceMap = device as Map; - final name = deviceMap['name'] as String?; - if (name != null && name.contains('iPhone 16')) { - return deviceMap['udid'] as String?; - } - } - } - } - } catch (e) { - return null; - } - - return null; -} - bool containsInOrder(String content, List lines) { var lastIndex = 0; From d14e808fb933da863baa4ff1f9b07e674e4dfbee Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Wed, 17 Dec 2025 16:08:09 +0000 Subject: [PATCH 19/19] test: use xcode 16.4 --- .github/workflows/validate.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/validate.yaml b/.github/workflows/validate.yaml index 4abc2627..38935ed1 100644 --- a/.github/workflows/validate.yaml +++ b/.github/workflows/validate.yaml @@ -107,8 +107,8 @@ jobs: - uses: actions/checkout@v2 with: ref: ${{ github.event.pull_request.head.sha || github.ref }} - - name: Select Xcode version - run: sudo xcode-select -switch /Applications/Xcode_26.0.1.app/Contents/Developer + - name: Xcode + run: sudo xcode-select -s /Applications/Xcode_16.4.app/Contents/Developer - uses: futureware-tech/simulator-action@v4 with: model: 'iPhone 16'