diff --git a/packages/firebase_ai/firebase_ai/android/build.gradle b/packages/firebase_ai/firebase_ai/android/build.gradle index 278104b4b9c2..2905ef640cae 100644 --- a/packages/firebase_ai/firebase_ai/android/build.gradle +++ b/packages/firebase_ai/firebase_ai/android/build.gradle @@ -21,6 +21,24 @@ buildscript { } } +repositories { + google() + mavenCentral() +} + +def firebaseCoreProject = findProject(':firebase_core') +if (firebaseCoreProject == null) { + throw new GradleException('Could not find the firebase_core FlutterFire plugin, have you added it as a dependency in your pubspec?') +} else if (!firebaseCoreProject.properties['FirebaseSDKVersion']) { + throw new GradleException('A newer version of the firebase_core FlutterFire plugin is required, please update your firebase_core pubspec dependency.') +} + +def getRootProjectExtOrCoreProperty(name, firebaseCoreProject) { + if (!rootProject.ext.has('FlutterFire')) return firebaseCoreProject.properties[name] + if (!rootProject.ext.get('FlutterFire')[name]) return firebaseCoreProject.properties[name] + return rootProject.ext.get('FlutterFire').get(name) +} + android { if (project.android.hasProperty("namespace")) { namespace 'io.flutter.plugins.firebase.ai' @@ -41,9 +59,20 @@ android { main.java.srcDirs += "src/main/kotlin" } + buildFeatures { + buildConfig = true + } + lintOptions { disable 'InvalidPackage' } + + dependencies { + api firebaseCoreProject + implementation platform("com.google.firebase:firebase-bom:${getRootProjectExtOrCoreProperty("FirebaseSDKVersion", firebaseCoreProject)}") + implementation 'com.google.firebase:firebase-common' + implementation 'androidx.annotation:annotation:1.7.0' + } } plugins.withId("org.jetbrains.kotlin.android") { @@ -53,3 +82,5 @@ plugins.withId("org.jetbrains.kotlin.android") { } } } + +apply from: file("./user-agent.gradle") diff --git a/packages/firebase_ai/firebase_ai/android/src/main/AndroidManifest.xml b/packages/firebase_ai/firebase_ai/android/src/main/AndroidManifest.xml index db54cec9bea5..12fbf47e96eb 100644 --- a/packages/firebase_ai/firebase_ai/android/src/main/AndroidManifest.xml +++ b/packages/firebase_ai/firebase_ai/android/src/main/AndroidManifest.xml @@ -1,3 +1,9 @@ + + + + + diff --git a/packages/firebase_ai/firebase_ai/android/src/main/kotlin/io/flutter/plugins/firebase/ai/FlutterFirebaseAppRegistrar.kt b/packages/firebase_ai/firebase_ai/android/src/main/kotlin/io/flutter/plugins/firebase/ai/FlutterFirebaseAppRegistrar.kt new file mode 100644 index 000000000000..6c69cea53ef1 --- /dev/null +++ b/packages/firebase_ai/firebase_ai/android/src/main/kotlin/io/flutter/plugins/firebase/ai/FlutterFirebaseAppRegistrar.kt @@ -0,0 +1,28 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package io.flutter.plugins.firebase.ai + +import androidx.annotation.Keep +import com.google.firebase.components.Component +import com.google.firebase.components.ComponentRegistrar +import com.google.firebase.platforminfo.LibraryVersionComponent + +@Keep +class FlutterFirebaseAppRegistrar : ComponentRegistrar { + override fun getComponents(): List> { + return listOf( + LibraryVersionComponent.create(BuildConfig.LIBRARY_NAME, BuildConfig.LIBRARY_VERSION)) + } +} diff --git a/packages/firebase_ai/firebase_ai/android/user-agent.gradle b/packages/firebase_ai/firebase_ai/android/user-agent.gradle new file mode 100644 index 000000000000..863c976dcaef --- /dev/null +++ b/packages/firebase_ai/firebase_ai/android/user-agent.gradle @@ -0,0 +1,22 @@ +import java.util.regex.Matcher +import java.util.regex.Pattern + +String libraryVersionName = "UNKNOWN" +String libraryName = "flutter-fire-ai" +File pubspec = new File(project.projectDir.parentFile, 'pubspec.yaml') + +if (pubspec.exists()) { + String yaml = pubspec.text + // Using \s*['|"]?([^\n|'|"]*)['|"]? to extract version number. + Matcher versionMatcher = Pattern.compile("^version:\\s*['|\"]?([^\\n|'|\"]*)['|\"]?\$", Pattern.MULTILINE).matcher(yaml) + if (versionMatcher.find()) libraryVersionName = versionMatcher.group(1).replaceAll("\\+", "-") +} + +android { + defaultConfig { + // BuildConfig.VERSION_NAME + buildConfigField 'String', 'LIBRARY_VERSION', "\"${libraryVersionName}\"" + // BuildConfig.LIBRARY_NAME + buildConfigField 'String', 'LIBRARY_NAME', "\"${libraryName}\"" + } +} diff --git a/packages/firebase_ai/firebase_ai/ios/firebase_ai/Sources/firebase_ai/Constants.swift b/packages/firebase_ai/firebase_ai/ios/firebase_ai/Sources/firebase_ai/Constants.swift new file mode 100644 index 000000000000..f48f80600feb --- /dev/null +++ b/packages/firebase_ai/firebase_ai/ios/firebase_ai/Sources/firebase_ai/Constants.swift @@ -0,0 +1,16 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Auto-generated file. Do not edit. +public let versionNumber = "5.0.0" diff --git a/packages/firebase_ai/firebase_ai/ios/firebase_ai/Sources/firebase_ai/FirebaseAIPlugin.swift b/packages/firebase_ai/firebase_ai/ios/firebase_ai/Sources/firebase_ai/FirebaseAIPlugin.swift index a76212182bda..e0ae35823168 100644 --- a/packages/firebase_ai/firebase_ai/ios/firebase_ai/Sources/firebase_ai/FirebaseAIPlugin.swift +++ b/packages/firebase_ai/firebase_ai/ios/firebase_ai/Sources/firebase_ai/FirebaseAIPlugin.swift @@ -32,6 +32,17 @@ public class FirebaseAIPlugin: NSObject, FlutterPlugin { ) let instance = FirebaseAIPlugin() registrar.addMethodCallDelegate(instance, channel: channel) + + let selector = NSSelectorFromString("registerLibrary:withVersion:") + if let firAppClass = NSClassFromString("FIRApp") as? NSObject.Type, + firAppClass.responds(to: selector) + { + _ = firAppClass.perform( + selector, + with: "flutter-fire-ai", + with: versionNumber + ) + } } public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { 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 188e46dc015d..82a37d2e7754 100644 --- a/packages/firebase_ai/firebase_ai/lib/src/firebase_ai.dart +++ b/packages/firebase_ai/firebase_ai/lib/src/firebase_ai.dart @@ -22,6 +22,9 @@ import 'package:meta/meta.dart'; import '../firebase_ai.dart'; import 'base_model.dart'; +import 'firebaseai_version.dart'; +import 'platform_version_stub.dart' + if (dart.library.js_interop) 'platform_version_web.dart'; const _defaultVertexAILocation = 'us-central1'; const _defaultAgentPlatformLocation = 'global'; @@ -36,7 +39,9 @@ class FirebaseAI extends FirebasePlugin { this.auth, this.useLimitedUseAppCheckTokens = false, }) : _useAgentPlatform = useAgentPlatform, - super(app.name, 'plugins.flutter.io/firebase_vertexai'); + super(app.name, 'plugins.flutter.io/firebase_vertexai') { + registerWebLibraryVersion('flutter-fire-ai', packageVersion); + } /// The [FirebaseApp] for this current [FirebaseAI] instance. FirebaseApp app; diff --git a/packages/firebase_ai/firebase_ai/lib/src/platform_version_stub.dart b/packages/firebase_ai/firebase_ai/lib/src/platform_version_stub.dart new file mode 100644 index 000000000000..4c4ca73600e9 --- /dev/null +++ b/packages/firebase_ai/firebase_ai/lib/src/platform_version_stub.dart @@ -0,0 +1,16 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Stub implementation of [registerWebLibraryVersion] on non-web platforms. +void registerWebLibraryVersion(String libraryName, String version) {} diff --git a/packages/firebase_ai/firebase_ai/lib/src/platform_version_web.dart b/packages/firebase_ai/firebase_ai/lib/src/platform_version_web.dart new file mode 100644 index 000000000000..bd42f290be0c --- /dev/null +++ b/packages/firebase_ai/firebase_ai/lib/src/platform_version_web.dart @@ -0,0 +1,40 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:js_interop'; +import 'dart:js_interop_unsafe'; + +bool _registered = false; + +/// Registers [libraryName] and [version] with the Firebase Web JS SDK. +void registerWebLibraryVersion(String libraryName, String version) { + if (_registered) return; + try { + final core = globalContext.getProperty('firebase_core'.toJS) as JSObject?; + if (core != null && core.hasProperty('registerVersion'.toJS).toDart) { + final storage = + globalContext.getProperty('sessionStorage'.toJS) as JSObject?; + final key = 'flutterfire-$libraryName-$version'.toJS; + if (storage?.callMethod('getItem'.toJS, key) == null) { + storage?.callMethod('setItem'.toJS, key, version.toJS); + core.callMethod( + 'registerVersion'.toJS, + libraryName.toJS, + version.toJS, + ); + } + _registered = true; + } + } catch (_) {} +} diff --git a/packages/firebase_ai/firebase_ai/macos/firebase_ai/Sources/firebase_ai/Constants.swift b/packages/firebase_ai/firebase_ai/macos/firebase_ai/Sources/firebase_ai/Constants.swift new file mode 120000 index 000000000000..7fdbf7a4f382 --- /dev/null +++ b/packages/firebase_ai/firebase_ai/macos/firebase_ai/Sources/firebase_ai/Constants.swift @@ -0,0 +1 @@ +../../../../ios/firebase_ai/Sources/firebase_ai/Constants.swift \ No newline at end of file diff --git a/packages/firebase_core/firebase_core_web/lib/src/firebase_core_web.dart b/packages/firebase_core/firebase_core_web/lib/src/firebase_core_web.dart index ea15ec3cc17a..d006a0ccd93f 100644 --- a/packages/firebase_core/firebase_core_web/lib/src/firebase_core_web.dart +++ b/packages/firebase_core/firebase_core_web/lib/src/firebase_core_web.dart @@ -87,6 +87,9 @@ class FirebaseCoreWeb extends FirebasePlatform { static void registerLibraryVersion(String libraryName, String version) { _libraryVersions[libraryName] = version; + if (globalContext.getProperty('firebase_core'.toJS) != null) { + _registerVersionIfNeeded(libraryName, version); + } } static void _registerAllLibraryVersions() { diff --git a/packages/firebase_core/firebase_core_web/test/firebase_core_web_late_registration_test.dart b/packages/firebase_core/firebase_core_web/test/firebase_core_web_late_registration_test.dart new file mode 100644 index 000000000000..4b0b1fc85b1e --- /dev/null +++ b/packages/firebase_core/firebase_core_web/test/firebase_core_web_late_registration_test.dart @@ -0,0 +1,88 @@ +// Copyright 2026 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +@TestOn('browser') +library; + +import 'dart:js_interop'; +import 'dart:js_interop_unsafe'; + +import 'package:firebase_core_platform_interface/firebase_core_platform_interface.dart'; +import 'package:firebase_core_web/firebase_core_web.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:web/web.dart' as web; + +String _getPlatformInfoString() { + final core = globalContext.getProperty('firebase_core'.toJS); + final jsApp = core.callMethod('getApp'.toJS); + final container = jsApp.getProperty('container'.toJS); + final provider = container.callMethod( + 'getProvider'.toJS, + 'platform-logger'.toJS, + ); + final logger = provider.callMethod('getImmediate'.toJS); + return logger.callMethod('getPlatformInfoString'.toJS).toDart; +} + +void main() { + group('FirebaseCoreWeb late library version registration', () { + setUp(() { + FirebasePlatform.instance = FirebaseCoreWeb(); + web.window.sessionStorage.clear(); + }); + + test( + 'registers library version with PlatformLogger when called after initializeApp', + () async { + const options = FirebaseOptions( + apiKey: 'fake-api-key', + appId: '1:1234567890:web:fake', + messagingSenderId: '1234567890', + projectId: 'fake-project', + authDomain: 'fake-project.firebaseapp.com', + ); + + await FirebasePlatform.instance.initializeApp(options: options); + + final initialPlatformInfo = _getPlatformInfoString(); + expect(initialPlatformInfo, contains('flutter-fire-core/')); + expect( + initialPlatformInfo, + isNot(contains('flutter-fire-late-test/1.2.3')), + ); + + FirebaseCoreWeb.registerLibraryVersion( + 'flutter-fire-late-test', + '1.2.3', + ); + + expect( + web.window.sessionStorage + .getItem('flutterfire-flutter-fire-late-test-1.2.3'), + '1.2.3', + ); + expect( + _getPlatformInfoString(), + contains('flutter-fire-late-test/1.2.3'), + ); + + // Clearing sessionStorage allows re-registering a subsequent version. + web.window.sessionStorage.clear(); + FirebaseCoreWeb.registerLibraryVersion( + 'flutter-fire-late-test-2', + '2.0.0', + ); + expect( + web.window.sessionStorage + .getItem('flutterfire-flutter-fire-late-test-2-2.0.0'), + '2.0.0', + ); + expect( + _getPlatformInfoString(), + contains('flutter-fire-late-test-2/2.0.0'), + ); + }, + ); + }); +} diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/firebase_data_connect.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/firebase_data_connect.dart index 165b6f5d62fb..9690f0ccbf43 100644 --- a/packages/firebase_data_connect/firebase_data_connect/lib/src/firebase_data_connect.dart +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/firebase_data_connect.dart @@ -20,11 +20,14 @@ import 'package:firebase_data_connect/src/common/common_library.dart'; import 'package:firebase_data_connect/src/core/ref.dart'; import 'package:flutter/foundation.dart'; +import './network/hot_restart_guard_stub.dart' + if (dart.library.js_interop) './network/hot_restart_guard_web.dart'; import './network/rest_library.dart'; import './network/transport_library.dart'; import 'cache/cache_data_types.dart'; import 'cache/cache.dart'; +import 'dataconnect_version.dart'; /// DataConnect class class FirebaseDataConnect extends FirebasePlugin { @@ -48,6 +51,7 @@ class FirebaseDataConnect extends FirebasePlugin { connectorConfig.serviceId, ), super(app.name, 'plugins.flutter.io/firebase_data_connect') { + registerWebLibraryVersion('flutter-fire-dc', packageVersion); _queryManager = QueryManager(this); if (sdkType != null) { _sdkType = sdkType; diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/network/hot_restart_guard_stub.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/network/hot_restart_guard_stub.dart index 7bb61996e100..4e25f6e5e1ef 100644 --- a/packages/firebase_data_connect/firebase_data_connect/lib/src/network/hot_restart_guard_stub.dart +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/network/hot_restart_guard_stub.dart @@ -17,3 +17,5 @@ String? claimDataConnectWebSocketTransport(String key) => null; bool isCurrentDataConnectWebSocketTransport(String key, String? token) => true; void releaseDataConnectWebSocketTransport(String key, String? token) {} + +void registerWebLibraryVersion(String libraryName, String version) {} diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/network/hot_restart_guard_web.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/network/hot_restart_guard_web.dart index 1fdd9cc37718..5066522f2342 100644 --- a/packages/firebase_data_connect/firebase_data_connect/lib/src/network/hot_restart_guard_web.dart +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/network/hot_restart_guard_web.dart @@ -48,3 +48,26 @@ void releaseDataConnectWebSocketTransport(String key, String? token) { globalContext.delete(key.toJS); } } + +bool _registered = false; + +void registerWebLibraryVersion(String libraryName, String version) { + if (_registered) return; + try { + final core = globalContext.getProperty('firebase_core'.toJS) as JSObject?; + if (core != null && core.hasProperty('registerVersion'.toJS).toDart) { + final storage = + globalContext.getProperty('sessionStorage'.toJS) as JSObject?; + final key = 'flutterfire-$libraryName-$version'.toJS; + if (storage?.callMethod('getItem'.toJS, key) == null) { + storage?.callMethod('setItem'.toJS, key, version.toJS); + core.callMethod( + 'registerVersion'.toJS, + libraryName.toJS, + version.toJS, + ); + } + _registered = true; + } + } catch (_) {} +} diff --git a/scripts/generate_versions_spm.dart b/scripts/generate_versions_spm.dart index 2b7766dedcdb..66f05d871578 100644 --- a/scripts/generate_versions_spm.dart +++ b/scripts/generate_versions_spm.dart @@ -118,6 +118,7 @@ void updateLibraryVersionPureSwiftPlugins() { 'firebase_in_app_messaging', 'firebase_crashlytics', 'firebase_core', + 'firebase_ai', ]; for (final package in packages) {