diff --git a/packages/firebase_ai/firebase_ai/example/lib/main.dart b/packages/firebase_ai/firebase_ai/example/lib/main.dart index bb42d139229c..b91bbd282276 100644 --- a/packages/firebase_ai/firebase_ai/example/lib/main.dart +++ b/packages/firebase_ai/firebase_ai/example/lib/main.dart @@ -13,6 +13,7 @@ // limitations under the License. import 'package:firebase_ai/firebase_ai.dart'; + import 'package:firebase_auth/firebase_auth.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/material.dart'; @@ -70,10 +71,10 @@ class _GenerativeAISampleState extends State { void _initializeModel(bool useVertexBackend) { if (useVertexBackend) { - final vertexInstance = FirebaseAI.vertexAI(auth: FirebaseAuth.instance); + final vertexInstance = FirebaseAI.vertexAI(); _currentModel = vertexInstance.generativeModel(model: 'gemini-2.5-flash'); } else { - final googleAI = FirebaseAI.googleAI(auth: FirebaseAuth.instance); + final googleAI = FirebaseAI.googleAI(); _currentModel = googleAI.generativeModel(model: 'gemini-2.5-flash'); } } diff --git a/packages/firebase_ai/firebase_ai/example/lib/pages/chat_page.dart b/packages/firebase_ai/firebase_ai/example/lib/pages/chat_page.dart index 8a98241001d4..24a29b59569f 100644 --- a/packages/firebase_ai/firebase_ai/example/lib/pages/chat_page.dart +++ b/packages/firebase_ai/firebase_ai/example/lib/pages/chat_page.dart @@ -12,7 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/material.dart'; import 'package:firebase_ai/firebase_ai.dart'; import '../widgets/message_widget.dart'; @@ -57,12 +56,12 @@ class _ChatPageState extends State { : null, ); if (widget.useVertexBackend) { - _model = FirebaseAI.vertexAI(auth: FirebaseAuth.instance).generativeModel( + _model = FirebaseAI.vertexAI().generativeModel( model: 'gemini-2.5-flash', generationConfig: generationConfig, ); } else { - _model = FirebaseAI.googleAI(auth: FirebaseAuth.instance).generativeModel( + _model = FirebaseAI.googleAI().generativeModel( model: 'gemini-2.5-flash', generationConfig: generationConfig, ); diff --git a/packages/firebase_ai/firebase_ai/example/lib/pages/function_calling_page.dart b/packages/firebase_ai/firebase_ai/example/lib/pages/function_calling_page.dart index ccd09c3e1b34..64c5b39ad9b4 100644 --- a/packages/firebase_ai/firebase_ai/example/lib/pages/function_calling_page.dart +++ b/packages/firebase_ai/firebase_ai/example/lib/pages/function_calling_page.dart @@ -14,7 +14,7 @@ import 'package:flutter/material.dart'; import 'package:firebase_ai/firebase_ai.dart'; -import 'package:firebase_auth/firebase_auth.dart'; + import '../utils/function_call_utils.dart'; import '../widgets/message_widget.dart'; @@ -235,9 +235,8 @@ class _FunctionCallingPageState extends State { : null, ); - final aiClient = widget.useVertexBackend - ? FirebaseAI.vertexAI(auth: FirebaseAuth.instance) - : FirebaseAI.googleAI(auth: FirebaseAuth.instance); + final aiClient = + widget.useVertexBackend ? FirebaseAI.vertexAI() : FirebaseAI.googleAI(); _functionCallModel = aiClient.generativeModel( model: 'gemini-2.5-flash', diff --git a/packages/firebase_ai/firebase_ai/example/lib/pages/grounding_page.dart b/packages/firebase_ai/firebase_ai/example/lib/pages/grounding_page.dart index 8456be1a9e4c..e895e51c14cd 100644 --- a/packages/firebase_ai/firebase_ai/example/lib/pages/grounding_page.dart +++ b/packages/firebase_ai/firebase_ai/example/lib/pages/grounding_page.dart @@ -12,7 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/material.dart'; import 'package:firebase_ai/firebase_ai.dart'; import '../widgets/message_widget.dart'; @@ -74,9 +73,8 @@ class _GroundingPageState extends State { } } - final aiProvider = widget.useVertexBackend - ? FirebaseAI.vertexAI(auth: FirebaseAuth.instance) - : FirebaseAI.googleAI(auth: FirebaseAuth.instance); + final aiProvider = + widget.useVertexBackend ? FirebaseAI.vertexAI() : FirebaseAI.googleAI(); _model = aiProvider.generativeModel( model: 'gemini-2.5-flash', diff --git a/packages/firebase_ai/firebase_ai/example/lib/pages/image_generation_page.dart b/packages/firebase_ai/firebase_ai/example/lib/pages/image_generation_page.dart index f4e2f527d3c4..0b66b1e7f6d3 100644 --- a/packages/firebase_ai/firebase_ai/example/lib/pages/image_generation_page.dart +++ b/packages/firebase_ai/firebase_ai/example/lib/pages/image_generation_page.dart @@ -15,7 +15,7 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:firebase_ai/firebase_ai.dart'; -import 'package:firebase_auth/firebase_auth.dart'; + import '../widgets/message_widget.dart'; class ImageGenerationPage extends StatefulWidget { @@ -47,9 +47,8 @@ class _ImageGenerationPageState extends State { } void _initializeModel() { - final aiClient = widget.useVertexBackend - ? FirebaseAI.vertexAI(auth: FirebaseAuth.instance) - : FirebaseAI.googleAI(auth: FirebaseAuth.instance); + final aiClient = + widget.useVertexBackend ? FirebaseAI.vertexAI() : FirebaseAI.googleAI(); _model = aiClient.generativeModel( model: 'gemini-2.5-flash-image', diff --git a/packages/firebase_ai/firebase_ai/lib/src/base_model.dart b/packages/firebase_ai/firebase_ai/lib/src/base_model.dart index edd5a9a7c7b2..de9e8072d249 100644 --- a/packages/firebase_ai/firebase_ai/lib/src/base_model.dart +++ b/packages/firebase_ai/firebase_ai/lib/src/base_model.dart @@ -282,19 +282,23 @@ abstract class BaseModel { ) { return () async { Map headers = {}; + + final effectiveAppCheck = appCheck ?? app?.getService(); + final effectiveAuth = auth ?? app?.getService(); + // Override the client name in Google AI SDK headers['x-goog-api-client'] = 'gl-dart/$packageVersion fire/$packageVersion'; - if (appCheck != null) { + if (effectiveAppCheck != null) { final appCheckToken = useLimitedUseAppCheckTokens == true - ? await appCheck.getLimitedUseToken() - : await appCheck.getToken(); + ? await effectiveAppCheck.getLimitedUseToken() + : await effectiveAppCheck.getToken(); if (appCheckToken != null) { headers['X-Firebase-AppCheck'] = appCheckToken; } } - if (auth != null) { - final idToken = await auth.currentUser?.getIdToken(); + if (effectiveAuth != null) { + final idToken = await effectiveAuth.currentUser?.getIdToken(); if (idToken != null) { headers['Authorization'] = 'Firebase $idToken'; } diff --git a/packages/firebase_ai/firebase_ai/lib/src/firebase_ai.dart b/packages/firebase_ai/firebase_ai/lib/src/firebase_ai.dart index 3d7b22023e15..40971c347362 100644 --- a/packages/firebase_ai/firebase_ai/lib/src/firebase_ai.dart +++ b/packages/firebase_ai/firebase_ai/lib/src/firebase_ai.dart @@ -62,12 +62,18 @@ class FirebaseAI extends FirebasePluginPlatform { /// If pass in [appCheck], request session will get protected from abusing. static FirebaseAI vertexAI({ FirebaseApp? app, + @Deprecated( + 'Passing an explicit instance is deprecated, internal handling is now automatic.') FirebaseAppCheck? appCheck, + @Deprecated( + 'Passing an explicit instance is deprecated, internal handling is now automatic.') FirebaseAuth? auth, String? location, bool? useLimitedUseAppCheckTokens, }) { app ??= Firebase.app(); + appCheck ??= app.getService(); + auth ??= app.getService(); var instanceKey = '${app.name}::vertexai::$location'; if (_cachedInstances.containsKey(instanceKey)) { @@ -95,11 +101,17 @@ class FirebaseAI extends FirebasePluginPlatform { /// If pass in [appCheck], request session will get protected from abusing. static FirebaseAI googleAI({ FirebaseApp? app, + @Deprecated( + 'Passing an explicit instance is deprecated, internal handling is now automatic.') FirebaseAppCheck? appCheck, + @Deprecated( + 'Passing an explicit instance is deprecated, internal handling is now automatic.') FirebaseAuth? auth, bool? useLimitedUseAppCheckTokens, }) { app ??= Firebase.app(); + appCheck ??= app.getService(); + auth ??= app.getService(); var instanceKey = '${app.name}::googleai'; if (_cachedInstances.containsKey(instanceKey)) { diff --git a/packages/firebase_ai/firebase_ai/test/firebase_vertexai_test.dart b/packages/firebase_ai/firebase_ai/test/firebase_vertexai_test.dart index 71c347f24333..fe52b397a106 100644 --- a/packages/firebase_ai/firebase_ai/test/firebase_vertexai_test.dart +++ b/packages/firebase_ai/firebase_ai/test/firebase_vertexai_test.dart @@ -14,6 +14,7 @@ import 'package:firebase_ai/firebase_ai.dart'; import 'package:firebase_app_check/firebase_app_check.dart'; +import 'package:firebase_auth/firebase_auth.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -28,6 +29,7 @@ void main() { late FirebaseApp customApp; late FirebaseApp limitTokenApp; late FirebaseAppCheck customAppCheck; + late FirebaseAuth customAuth; late FirebaseAppCheck limitTokenAppCheck; group('FirebaseAI Tests', () { @@ -47,6 +49,7 @@ void main() { appCheck = FirebaseAppCheck.instance; customAppCheck = FirebaseAppCheck.instanceFor(app: customApp); limitTokenAppCheck = FirebaseAppCheck.instanceFor(app: limitTokenApp); + customAuth = FirebaseAuth.instanceFor(app: customApp); }); test('Singleton behavior', () { @@ -96,6 +99,20 @@ void main() { expect(vertexAIAppCheck.useLimitedUseAppCheckTokens, true); }); + test('Instance creation with auto-injected AppCheck', () { + final vertexAI = FirebaseAI.vertexAI(app: customApp); + + expect(vertexAI.app, equals(customApp)); + expect(vertexAI.appCheck, equals(customAppCheck)); + }); + + test('Instance creation with auto-injected Auth', () { + final vertexAI = FirebaseAI.vertexAI(app: customApp); + + expect(vertexAI.app, equals(customApp)); + expect(vertexAI.auth, equals(customAuth)); + }); + test('generativeModel creation with Grounding tools', () { final ai = FirebaseAI.googleAI(); diff --git a/packages/firebase_app_check/firebase_app_check/lib/src/firebase_app_check.dart b/packages/firebase_app_check/firebase_app_check/lib/src/firebase_app_check.dart index 25a1845449e8..ba48413dbddd 100644 --- a/packages/firebase_app_check/firebase_app_check/lib/src/firebase_app_check.dart +++ b/packages/firebase_app_check/firebase_app_check/lib/src/firebase_app_check.dart @@ -5,7 +5,8 @@ part of '../firebase_app_check.dart'; -class FirebaseAppCheck extends FirebasePluginPlatform { +class FirebaseAppCheck extends FirebasePluginPlatform + implements FirebaseService { static Map _firebaseAppCheckInstances = {}; FirebaseAppCheck._({required this.app}) @@ -41,7 +42,9 @@ class FirebaseAppCheck extends FirebasePluginPlatform { /// Returns an instance using a specified [FirebaseApp]. static FirebaseAppCheck instanceFor({required FirebaseApp app}) { return _firebaseAppCheckInstances.putIfAbsent(app.name, () { - return FirebaseAppCheck._(app: app); + final instance = FirebaseAppCheck._(app: app); + app.registerService(instance); + return instance; }); } diff --git a/packages/firebase_auth/firebase_auth/lib/src/firebase_auth.dart b/packages/firebase_auth/firebase_auth/lib/src/firebase_auth.dart index 6906c839d281..f4fec1522d59 100644 --- a/packages/firebase_auth/firebase_auth/lib/src/firebase_auth.dart +++ b/packages/firebase_auth/firebase_auth/lib/src/firebase_auth.dart @@ -6,7 +6,7 @@ part of '../firebase_auth.dart'; /// The entry point of the Firebase Authentication SDK. -class FirebaseAuth extends FirebasePluginPlatform { +class FirebaseAuth extends FirebasePluginPlatform implements FirebaseService { // Cached instances of [FirebaseAuth]. static Map _firebaseAuthInstances = {}; @@ -45,7 +45,9 @@ class FirebaseAuth extends FirebasePluginPlatform { required FirebaseApp app, }) { return _firebaseAuthInstances.putIfAbsent(app.name, () { - return FirebaseAuth._(app: app); + final instance = FirebaseAuth._(app: app); + app.registerService(instance); + return instance; }); } diff --git a/packages/firebase_core/firebase_core/lib/src/firebase_app.dart b/packages/firebase_core/firebase_core/lib/src/firebase_app.dart index 841255c8dd7f..b083f65c8118 100644 --- a/packages/firebase_core/firebase_core/lib/src/firebase_app.dart +++ b/packages/firebase_core/firebase_core/lib/src/firebase_app.dart @@ -28,6 +28,7 @@ class FirebaseApp { /// Deleting the default app is not possible and throws an exception. Future delete() async { await _delegate.delete(); + _registries.remove(name); } /// The name of this [FirebaseApp]. @@ -71,4 +72,24 @@ class FirebaseApp { @override String toString() => '$FirebaseApp($name)'; + + static final Map> _registries = {}; + + /// Registers a service instance for this app. + void registerService(T service) { + final registry = _registries.putIfAbsent(name, () => {}); + registry[T] = service; + } + + /// Returns a registered service instance for this app. + T? getService() { + final registry = _registries[name]; + if (registry != null) { + return registry[T] as T?; + } + return null; + } } + +/// A marker interface for Firebase services that can be registered in [FirebaseApp]. +abstract class FirebaseService {} 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 87e4abe4718b..b0770caf23a4 100755 --- a/packages/firebase_core/firebase_core/test/firebase_core_test.dart +++ b/packages/firebase_core/firebase_core/test/firebase_core_test.dart @@ -64,6 +64,15 @@ void main() { mock.app(testAppName), ]); }); + + test('.registerService() and .getService()', () { + FirebaseApp app = Firebase.app(testAppName); + + final testService = TestService(); + app.registerService(testService); + + expect(app.getService(), testService); + }); }); test('.initializeApp() with demoProjectId', () async { @@ -152,3 +161,5 @@ class MockFirebaseCore extends Mock // ignore: avoid_implementing_value_types class FakeFirebaseAppPlatform extends Fake implements FirebaseAppPlatform {} + +class TestService implements FirebaseService {}