From e8fbff7b6b87ad15cba0a91df0e7fe41bd5d1c0f Mon Sep 17 00:00:00 2001 From: Evan Mesterhazy Date: Mon, 8 Sep 2025 21:35:48 -0400 Subject: [PATCH 1/2] fix: a bug with the `demoProjectId` arg to `Firebase.initializeApp()` If platform specific configuration files exist for the project (i.e. GoogleService-Info.plist for iOS), the default `MethodChannelFirebaseApp` is initialized using the configuration it specifies. This is fine since multiple `FirebaseApp` instances can exist, however when `MethodChannelFirebase.initializeApp()` is called it uses the default app name (`[DEFAULT]`, from the code) if no name is passed as an argument. Since the default app is initialized using the platform specific configuration before `initializeApp()` is called, the user provided options are ignored. The solution is to ensure a non-null app name is passed to `initializeApp()` to avoid conflicts with platform specific configuration files when the user intends to use a "demo-" project for testing. Since we should be providing a distinct app name for the demo app, the allowance for mismatched options between the specified options and the existing options for demo apps is also removed. --- .../firebase_core/lib/src/firebase.dart | 34 ++++++++++------ .../test/firebase_core_test.dart | 39 +++++++++++++++++++ .../method_channel_firebase.dart | 4 +- 3 files changed, 63 insertions(+), 14 deletions(-) diff --git a/packages/firebase_core/firebase_core/lib/src/firebase.dart b/packages/firebase_core/firebase_core/lib/src/firebase.dart index d0499bc98bae..05d1c6b49194 100644 --- a/packages/firebase_core/firebase_core/lib/src/firebase.dart +++ b/packages/firebase_core/firebase_core/lib/src/firebase.dart @@ -28,8 +28,15 @@ class Firebase { return _delegate.apps.map(FirebaseApp._).toList(growable: false); } - /// Initializes a new [FirebaseApp] instance by [name] and [options] and returns - /// the created app. This method should be called before any usage of FlutterFire plugins. + /// Initializes a new [FirebaseApp] instance by [name] and [options] and + /// returns the created app. This method should be called before any usage of + /// FlutterFire plugins. + /// + /// If a [demoProjectId] is provided, a new [FirebaseApp] instance will be + /// initialized with a set of default options for demo projects, overriding + /// the [options] argument. If no [name] is provided alongside a + /// [demoProjectId], the [demoProjectId] will be used as the app name. By + /// convention, the [demoProjectId] should begin with "demo-". /// /// The default app instance can be initialized here simply by passing no "name" as an argument /// in both Dart & manual initialization flows. @@ -52,16 +59,21 @@ class Firebase { // We use 'web' as the default platform for unknown platforms. platformString = 'web'; } - FirebaseAppPlatform app = await _delegate.initializeApp( - options: FirebaseOptions( - apiKey: '', - appId: '1:1:$platformString:1', - messagingSenderId: '', - projectId: demoProjectId, - ), + // A name must be set, otherwise [DEFAULT] will be used and the options + // we've provided will be ignored if any platform specific configuration + // files exist (i.e. GoogleService-Info.plist for iOS). + name ??= demoProjectId; + // The user should not set any options if they specify a demo project + // id, but it was allowed when this API was first added, so we allow it + // for backwards compatibility and simply override the user-provided + // options. + options = FirebaseOptions( + apiKey: '', + appId: '1:1:$platformString:1', + messagingSenderId: '', + projectId: demoProjectId, ); - - return FirebaseApp._(app); + // Now fall through to the normal initialization logic. } FirebaseAppPlatform app = await _delegate.initializeApp( name: name, diff --git a/packages/firebase_core/firebase_core/test/firebase_core_test.dart b/packages/firebase_core/firebase_core/test/firebase_core_test.dart index 1921a7666ca0..f621eaa783fe 100755 --- a/packages/firebase_core/firebase_core/test/firebase_core_test.dart +++ b/packages/firebase_core/firebase_core/test/firebase_core_test.dart @@ -65,6 +65,45 @@ void main() { ]); }); }); + + test('.initializeApp() with demoProjectId', () async { + const String demoProjectId = 'demo-project-id'; + const String expectedName = demoProjectId; + const FirebaseOptions expectedOptions = FirebaseOptions( + apiKey: '', + // Flutter tests use android as the default platform. + appId: '1:1:android:1', + messagingSenderId: '', + projectId: demoProjectId, + ); + + final mock = MockFirebaseCore(); + Firebase.delegatePackingProperty = mock; + + final FirebaseAppPlatform platformApp = + FirebaseAppPlatform(expectedName, expectedOptions); + + when(mock.apps).thenReturn([platformApp]); + when(mock.app(expectedName)).thenReturn(platformApp); + when(mock.initializeApp(name: expectedName, options: expectedOptions)) + .thenAnswer((_) => Future.value(platformApp)); + + // Initialize the app with only a demo project id. The implementation will + // set the name and options accordingly. + FirebaseApp initializedApp = await Firebase.initializeApp( + demoProjectId: demoProjectId, + ); + FirebaseApp app = Firebase.app(expectedName); + + expect(initializedApp, app); + verifyInOrder([ + mock.initializeApp( + name: expectedName, + options: expectedOptions, + ), + mock.app(expectedName), + ]); + }); } class MockFirebaseCore extends Mock diff --git a/packages/firebase_core/firebase_core_platform_interface/lib/src/method_channel/method_channel_firebase.dart b/packages/firebase_core/firebase_core_platform_interface/lib/src/method_channel/method_channel_firebase.dart index 57ce1b761c86..bb5d2636000f 100644 --- a/packages/firebase_core/firebase_core_platform_interface/lib/src/method_channel/method_channel_firebase.dart +++ b/packages/firebase_core/firebase_core_platform_interface/lib/src/method_channel/method_channel_firebase.dart @@ -121,9 +121,7 @@ class MethodChannelFirebase extends FirebasePlatform { // check to see if options are roughly identical (so we don't unnecessarily // throw on minor differences such as platform specific keys missing // e.g. hot reloads/restarts). - if (defaultApp != null && - _options != null && - !_options.projectId.startsWith('demo-')) { + if (defaultApp != null && _options != null) { if (_options.apiKey != defaultApp.options.apiKey || (_options.databaseURL != null && _options.databaseURL != defaultApp.options.databaseURL) || From 54e08d12bea81f887dfb5fc93347851069bd97bd Mon Sep 17 00:00:00 2001 From: Evan Mesterhazy Date: Tue, 16 Sep 2025 22:48:28 -0400 Subject: [PATCH 2/2] fix: Set a non-null apiKey for demo projects in Dart On Android the underlying Java SDK will throw an exception if the api key is empty. Setting it to any arbitrary value is sufficient. --- packages/firebase_core/firebase_core/lib/src/firebase.dart | 2 +- .../firebase_core/firebase_core/test/firebase_core_test.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/firebase_core/firebase_core/lib/src/firebase.dart b/packages/firebase_core/firebase_core/lib/src/firebase.dart index 05d1c6b49194..7ff8a4882715 100644 --- a/packages/firebase_core/firebase_core/lib/src/firebase.dart +++ b/packages/firebase_core/firebase_core/lib/src/firebase.dart @@ -68,7 +68,7 @@ class Firebase { // for backwards compatibility and simply override the user-provided // options. options = FirebaseOptions( - apiKey: '', + apiKey: '12345', appId: '1:1:$platformString:1', messagingSenderId: '', projectId: demoProjectId, diff --git a/packages/firebase_core/firebase_core/test/firebase_core_test.dart b/packages/firebase_core/firebase_core/test/firebase_core_test.dart index f621eaa783fe..87e4abe4718b 100755 --- a/packages/firebase_core/firebase_core/test/firebase_core_test.dart +++ b/packages/firebase_core/firebase_core/test/firebase_core_test.dart @@ -70,7 +70,7 @@ void main() { const String demoProjectId = 'demo-project-id'; const String expectedName = demoProjectId; const FirebaseOptions expectedOptions = FirebaseOptions( - apiKey: '', + apiKey: '12345', // Flutter tests use android as the default platform. appId: '1:1:android:1', messagingSenderId: '',