diff --git a/packages/firebase_core/firebase_core_web/lib/firebase_core_web.dart b/packages/firebase_core/firebase_core_web/lib/firebase_core_web.dart index 6b9b24f0c378..faa0a0837ea1 100644 --- a/packages/firebase_core/firebase_core_web/lib/firebase_core_web.dart +++ b/packages/firebase_core/firebase_core_web/lib/firebase_core_web.dart @@ -6,15 +6,15 @@ library firebase_core_web; import 'dart:async'; -import 'dart:html'; -import 'dart:js'; -import 'dart:js_util'; +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/src/interop/js.dart'; +import 'package:firebase_core_web/src/interop/package_web_tweaks.dart'; +import 'package:firebase_core_web/src/interop/utils/es6_interop.dart'; import 'package:flutter_web_plugins/flutter_web_plugins.dart'; -import 'package:js/js_util.dart' as js_util; import 'package:meta/meta.dart'; +import 'package:web/web.dart' as web; import 'src/interop/core.dart' as firebase; @@ -46,9 +46,9 @@ FirebaseOptions _createFromJsOptions(firebase.FirebaseOptions options) { /// When the Firebase JS SDK throws an error, it contains a code which can be /// used to identify the specific type of error. This helper function is used /// to keep error messages consistent across different platforms. -String _getJSErrorCode(dynamic e) { - if (js_util.getProperty(e, 'name') == 'FirebaseError') { - return js_util.getProperty(e, 'code') ?? ''; +String _getJSErrorCode(JSError e) { + if (e.name == 'FirebaseError') { + return e.code ?? ''; } return ''; @@ -59,11 +59,10 @@ String _getJSErrorCode(dynamic e) { /// If a JavaScript error is thrown and not manually handled using the code, /// this function ensures that if the error is Firebase related, it is instead /// re-created as a [FirebaseException] with a familiar code and message. -FirebaseException _catchJSError(dynamic e) { - if (js_util.getProperty(e, 'name') == 'FirebaseError') { - String rawCode = js_util.getProperty(e, 'code'); - String code = rawCode; - String message = js_util.getProperty(e, 'message') ?? ''; +FirebaseException _catchJSError(JSError e) { + if (e.name == 'FirebaseError') { + String code = e.code ?? ''; + String message = e.message ?? ''; if (code.contains('/')) { List chunks = code.split('/'); @@ -73,7 +72,7 @@ FirebaseException _catchJSError(dynamic e) { return FirebaseException( plugin: 'core', code: code, - message: message.replaceAll(' ($rawCode)', ''), + message: message.replaceAll(' ($code)', ''), ); } 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 9d8744d51601..68cefba51d31 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 @@ -67,8 +67,9 @@ class FirebaseCoreWeb extends FirebasePlatform { /// own risk as the version might be unsupported or untested against. @visibleForTesting String get firebaseSDKVersion { - return context['flutterfire_web_sdk_version'] ?? - supportedFirebaseJsSdkVersion; + final overridedWebSDKVersion = + (globalContext['flutterfire_web_sdk_version'] as JSString?)?.toDart; + return overridedWebSDKVersion ?? supportedFirebaseJsSdkVersion; } /// Returns a list of services which won't be automatically injected on @@ -83,8 +84,8 @@ class FirebaseCoreWeb extends FirebasePlatform { /// You must ensure the Firebase script is injected before using the service. List get _ignoredServiceScripts { try { - JsObject ignored = - JsObject.fromBrowserObject(context['flutterfire_ignore_scripts']); + JSObject ignored = + globalContext.getProperty('flutterfire_ignore_scripts'.toJS); if (ignored is Iterable) { return (ignored as Iterable) @@ -104,48 +105,49 @@ class FirebaseCoreWeb extends FirebasePlatform { /// document. @visibleForTesting Future injectSrcScript(String src, String windowVar) async { - DomTrustedScriptUrl? trustedUrl; - final trustedPolicyName = _defaultTrustedPolicyName + windowVar; - if (trustedTypes != null) { - console.debug( - 'TrustedTypes available. Creating policy:', - trustedPolicyName, + web.TrustedScriptURL? trustedUrl; + final trustedTypePolicyName = _defaultTrustedPolicyName + windowVar; + if (web.window.nullableTrustedTypes != null) { + web.console.debug( + 'TrustedTypes available. Creating policy: $trustedTypePolicyName'.toJS, ); - final DomTrustedTypePolicyFactory factory = trustedTypes!; try { - final DomTrustedTypePolicy policy = factory.createPolicy( - trustedPolicyName, - DomTrustedTypePolicyOptions( - createScriptURL: allowInterop((String url) => src), + final web.TrustedTypePolicy policy = + web.window.trustedTypes.createPolicy( + trustedTypePolicyName, + web.TrustedTypePolicyOptions( + createScriptURL: ((JSString url) => src).toJS, ), ); - trustedUrl = policy.createScriptURL(src); + trustedUrl = policy.createScriptURLNoArgs(src); } catch (e) { - rethrow; + throw TrustedTypesException(e.toString()); } } - ScriptElement script = ScriptElement(); + + final web.HTMLScriptElement script = + web.document.createElement('script') as web.HTMLScriptElement; script.type = 'text/javascript'; script.crossOrigin = 'anonymous'; script.text = ''' window.ff_trigger_$windowVar = async (callback) => { console.debug("Initializing Firebase $windowVar"); - callback(await import("${trustedUrl != null ? callMethod(trustedUrl, 'toString', []) : src}")); + callback(await import("${trustedUrl != null ? trustedUrl.callMethod('toString'.toJS) : src}")); }; '''; - assert(document.head != null); - document.head!.append(script); + web.document.head!.appendChild(script); Completer completer = Completer(); - context.callMethod('ff_trigger_$windowVar', [ - (module) { - context[windowVar] = module; - context.deleteProperty('ff_trigger_$windowVar'); + globalContext.callMethod( + 'ff_trigger_$windowVar'.toJS, + (JSAny module) { + globalContext[windowVar] = module; + globalContext.delete('ff_trigger_$windowVar'.toJS); completer.complete(); - } - ]); + }.toJS, + ); await completer.future; } @@ -155,7 +157,7 @@ class FirebaseCoreWeb extends FirebasePlatform { Future _initializeCore() async { // If Firebase is already available, core has already been initialized // (or the user has added the scripts to their html file). - if (context['firebase_core'] != null) { + if (globalContext.getProperty('firebase_core'.toJS) != null) { return; } @@ -291,7 +293,7 @@ class FirebaseCoreWeb extends FirebasePlatform { measurementId: options.measurementId, ); } catch (e) { - if (_getJSErrorCode(e) == 'app/duplicate-app') { + if (_getJSErrorCode(e as JSError) == 'app/duplicate-app') { throw duplicateApp(name); } @@ -330,15 +332,14 @@ class FirebaseCoreWeb extends FirebasePlatform { try { app = guardNotInitialized(() => firebase.app(name)); + return _createFromJsApp(app); } catch (e) { - if (_getJSErrorCode(e) == 'app/no-app') { + if (_getJSErrorCode(e as JSError) == 'app/no-app') { throw noAppExists(name); } throw _catchJSError(e); } - - return _createFromJsApp(app); } } @@ -367,3 +368,15 @@ R guardNotInitialized(R Function() cb) { _handleException(error, stackTrace); } } + +/// Exception thrown if the Trusted Types feature is supported, enabled, and it +/// has prevented this loader from injecting the JS SDK. +class TrustedTypesException implements Exception { + /// + TrustedTypesException(this.message); + + /// The message of the exception + final String message; + @override + String toString() => 'TrustedTypesException: $message'; +} diff --git a/packages/firebase_core/firebase_core_web/lib/src/interop/app.dart b/packages/firebase_core/firebase_core_web/lib/src/interop/app.dart index 0eb4146d684d..651d61dcf660 100644 --- a/packages/firebase_core/firebase_core_web/lib/src/interop/app.dart +++ b/packages/firebase_core/firebase_core_web/lib/src/interop/app.dart @@ -5,6 +5,8 @@ // ignore_for_file: public_member_api_docs +import 'dart:js_interop'; + import 'package:firebase_core_web/firebase_core_web_interop.dart'; import 'core.dart' as core_interop; @@ -30,5 +32,5 @@ class App extends JsObjectWrapper { } /// Deletes the app and frees resources of all App's services. - Future delete() => handleThenable(core_interop.deleteApp(jsObject)); + Future delete() => core_interop.deleteApp(jsObject).toDart; } diff --git a/packages/firebase_core/firebase_core_web/lib/src/interop/app_interop.dart b/packages/firebase_core/firebase_core_web/lib/src/interop/app_interop.dart index 0f01b0696124..a815b464aa9d 100644 --- a/packages/firebase_core/firebase_core_web/lib/src/interop/app_interop.dart +++ b/packages/firebase_core/firebase_core_web/lib/src/interop/app_interop.dart @@ -9,10 +9,14 @@ library firebase_interop.core.app; import 'package:js/js.dart'; + import 'core_interop.dart'; @JS('FirebaseApp') -abstract class AppJsImpl { +@staticInterop +abstract class AppJsImpl {} + +extension AppJsImplExtension on AppJsImpl { external String get name; external FirebaseOptions get options; } diff --git a/packages/firebase_core/firebase_core_web/lib/src/interop/core.dart b/packages/firebase_core/firebase_core_web/lib/src/interop/core.dart index d7bce00f06ad..4b1a14b3ce16 100644 --- a/packages/firebase_core/firebase_core_web/lib/src/interop/core.dart +++ b/packages/firebase_core/firebase_core_web/lib/src/interop/core.dart @@ -5,9 +5,11 @@ // ignore_for_file: public_member_api_docs +import 'dart:js_interop'; + import 'package:firebase_core_platform_interface/firebase_core_platform_interface.dart'; +import 'package:firebase_core_web/firebase_core_web_interop.dart'; -import 'app.dart'; import 'core_interop.dart' as firebase_interop; export 'app.dart'; @@ -16,10 +18,9 @@ export 'core_interop.dart'; List get apps => firebase_interop .getApps() - // explicitly typing the param as dynamic to work-around - // https://github.com/dart-lang/sdk/issues/33537 - // ignore: unnecessary_lambdas - .map((dynamic e) => App.getInstance(e)) + .toDart + .cast() + .map(App.getInstance) .toList(); App initializeApp({ diff --git a/packages/firebase_core/firebase_core_web/lib/src/interop/core_interop.dart b/packages/firebase_core/firebase_core_web/lib/src/interop/core_interop.dart index f8bde927eb86..2b3528abad1b 100644 --- a/packages/firebase_core/firebase_core_web/lib/src/interop/core_interop.dart +++ b/packages/firebase_core/firebase_core_web/lib/src/interop/core_interop.dart @@ -8,11 +8,13 @@ @JS('firebase_core') library firebase_interop.core; +import 'dart:js_interop'; + import 'package:firebase_core_web/firebase_core_web_interop.dart'; -import 'package:js/js.dart'; @JS() -external List getApps(); +// List +external JSArray getApps(); /// The current SDK version. /// @@ -27,7 +29,7 @@ external AppJsImpl initializeApp(FirebaseOptions options, [String? name]); external AppJsImpl getApp([String? name]); @JS() -external PromiseJsImpl deleteApp(AppJsImpl app); +external JSPromise deleteApp(AppJsImpl app); /// FirebaseError is a subclass of the standard Error object. /// In addition to a message string, it contains a string-valued code. @@ -35,19 +37,23 @@ external PromiseJsImpl deleteApp(AppJsImpl app); /// See: . @JS() @anonymous -abstract class FirebaseError { +@staticInterop +abstract class FirebaseError {} + +extension FirebaseErrorExtension on FirebaseError { external String get code; external String get message; external String get name; external String get stack; /// Not part of the core JS API, but occasionally exposed in error objects. - external Object get serverResponse; + external JSAny get serverResponse; } /// A structure for options provided to Firebase. @JS() @anonymous +@staticInterop class FirebaseOptions { external factory FirebaseOptions({ String? apiKey, @@ -59,7 +65,9 @@ class FirebaseOptions { String? measurementId, String? appId, }); +} +extension FirebaseOptionsExtension on FirebaseOptions { external String get apiKey; external set apiKey(String s); external String get authDomain; diff --git a/packages/firebase_core/firebase_core_web/lib/src/interop/js.dart b/packages/firebase_core/firebase_core_web/lib/src/interop/js.dart deleted file mode 100644 index 0a720243449d..000000000000 --- a/packages/firebase_core/firebase_core_web/lib/src/interop/js.dart +++ /dev/null @@ -1,141 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -/* -// DOM shim. This file contains everything we need from the DOM API written as -// @staticInterop, so we don't need dart:html -// https://developer.mozilla.org/en-US/docs/Web/API/ -*/ - -import 'package:js/js.dart'; - -/// console interface -@JS() -@staticInterop -@anonymous -abstract class DomConsole {} - -/// The interface of window.console -extension DomConsoleExtension on DomConsole { - /// console.debug - external DomConsoleDumpFn get debug; - - /// console.info - external DomConsoleDumpFn get info; - - /// console.log - external DomConsoleDumpFn get log; - - /// console.warn - external DomConsoleDumpFn get warn; - - /// console.error - external DomConsoleDumpFn get error; -} - -/// Fakey variadic-type for console-dumping methods (like console.log or info). -typedef DomConsoleDumpFn = void Function( - Object? arg, [ - Object? arg2, - Object? arg3, - Object? arg4, - Object? arg5, - Object? arg6, - Object? arg7, - Object? arg8, - Object? arg9, - Object? arg10, -]); - -/// Error object -@JS('Error') -@staticInterop -abstract class DomError {} - -/// Methods on the error object -extension DomErrorExtension on DomError { - /// Error message. - external String? get message; - - /// Stack trace. - external String? get stack; - - /// Error name. This is determined by the constructor function. - external String get name; - - /// Error cause indicating the reason why the current error is thrown. - /// - /// This is usually another caught error, or the value provided as the `cause` - /// property of the Error constructor's second argument. - external Object? get cause; -} - -/* -// Trusted Types API (TrustedTypePolicy, TrustedScript, TrustedScriptURL) -// https://developer.mozilla.org/en-US/docs/Web/API/TrustedTypesAPI -*/ - -/// A factory to create `TrustedTypePolicy` objects. -@JS() -@staticInterop -@anonymous -abstract class DomTrustedTypePolicyFactory {} - -/// (Some) methods of the [DomTrustedTypePolicyFactory]: -extension DomTrustedTypePolicyFactoryExtension on DomTrustedTypePolicyFactory { - /// createPolicy - external DomTrustedTypePolicy createPolicy( - String policyName, - DomTrustedTypePolicyOptions? policyOptions, - ); -} - -/// Options to create a trusted type policy. -@JS() -@staticInterop -@anonymous -abstract class DomTrustedTypePolicyOptions { - /// Constructs a TrustedPolicyOptions object in JavaScript. - /// - /// The following properties need to be manually wrapped in [allowInterop] - /// before being passed to this constructor: [createScriptURL]. - external factory DomTrustedTypePolicyOptions({ - DomCreateScriptUrlOptionFn? createScriptURL, - }); -} - -/// Type of the function to configure createScriptURL -typedef DomCreateScriptUrlOptionFn = String Function(String input); - -/// An instance of a TrustedTypePolicy -@JS() -@staticInterop -@anonymous -abstract class DomTrustedTypePolicy {} - -/// (Some) methods of the [DomTrustedTypePolicy] -extension DomTrustedTypePolicyExtension on DomTrustedTypePolicy { - /// Create a `TrustedScriptURL` for the given [input]. - external DomTrustedScriptUrl createScriptURL(String input); -} - -/// An instance of a DomTrustedScriptUrl -@JS() -@staticInterop -@anonymous -abstract class DomTrustedScriptUrl {} - -// Getters - -/// window.trustedTypes (may or may not be supported by the browser) -@JS() -@staticInterop -@anonymous -external DomTrustedTypePolicyFactory? get trustedTypes; - -/// window.console -@JS() -@staticInterop -@anonymous -external DomConsole get console; diff --git a/packages/firebase_core/firebase_core_web/lib/src/interop/package_web_tweaks.dart b/packages/firebase_core/firebase_core_web/lib/src/interop/package_web_tweaks.dart new file mode 100644 index 000000000000..7d9e2bd612f7 --- /dev/null +++ b/packages/firebase_core/firebase_core_web/lib/src/interop/package_web_tweaks.dart @@ -0,0 +1,38 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Copied from https://github.com/flutter/packages/google_identity_services_web/lib/src/js_interop/package_web_tweaks.dart + +/// Provides some useful tweaks to `package:web`. +library package_web_tweaks; + +import 'dart:js_interop'; + +import 'package:web/web.dart' as web; + +/// This extension gives web.window a nullable getter to the `trustedTypes` +/// property, which needs to be used to check for feature support. +extension NullableTrustedTypesGetter on web.Window { + /// + @JS('trustedTypes') + external web.TrustedTypePolicyFactory? get nullableTrustedTypes; +} + +/// This extension allows a trusted type policy to create a script URL without +/// the `args` parameter (which in Chrome currently fails). +extension CreateScriptUrlWithoutArgs on web.TrustedTypePolicy { + /// + @JS('createScriptURL') + external web.TrustedScriptURL createScriptURLNoArgs( + String input, + ); +} + +/// This extension allows setting a TrustedScriptURL as the src of a script element, +/// which currently only accepts a string. +extension TrustedTypeSrcAttribute on web.HTMLScriptElement { + /// + @JS('src') + external set srcTT(web.TrustedScriptURL value); +} diff --git a/packages/firebase_core/firebase_core_web/lib/src/interop/utils/es6_interop.dart b/packages/firebase_core/firebase_core_web/lib/src/interop/utils/es6_interop.dart index 3e8aaecce6a2..38c582de1e74 100644 --- a/packages/firebase_core/firebase_core_web/lib/src/interop/utils/es6_interop.dart +++ b/packages/firebase_core/firebase_core_web/lib/src/interop/utils/es6_interop.dart @@ -17,3 +17,13 @@ class PromiseJsImpl { external PromiseJsImpl(Function resolver); external PromiseJsImpl then([Func1? onResolve, Func1? onReject]); } + +@JS() +@staticInterop +class JSError {} + +extension JSErrorExtension on JSError { + external String? get name; + external String? get message; + external String? get code; +} diff --git a/packages/firebase_core/firebase_core_web/lib/src/interop/utils/utils.dart b/packages/firebase_core/firebase_core_web/lib/src/interop/utils/utils.dart index d90fbcf91e8e..c0592509b5c6 100644 --- a/packages/firebase_core/firebase_core_web/lib/src/interop/utils/utils.dart +++ b/packages/firebase_core/firebase_core_web/lib/src/interop/utils/utils.dart @@ -5,6 +5,8 @@ // ignore_for_file: public_member_api_docs +// TODO(Lyokone): should be deleted once all plugins are migrated to use js_interop + import 'dart:async'; import 'package:js/js.dart'; diff --git a/packages/firebase_core/firebase_core_web/pubspec.yaml b/packages/firebase_core/firebase_core_web/pubspec.yaml index a931532f4664..d4d4837d8e6d 100644 --- a/packages/firebase_core/firebase_core_web/pubspec.yaml +++ b/packages/firebase_core/firebase_core_web/pubspec.yaml @@ -5,7 +5,7 @@ repository: https://github.com/firebase/flutterfire/tree/master/packages/firebas version: 2.10.0 environment: - sdk: '>=2.18.0 <4.0.0' + sdk: '>=3.2.0 <4.0.0' flutter: '>=3.3.0' dependencies: @@ -16,6 +16,7 @@ dependencies: sdk: flutter js: ^0.6.3 meta: ^1.8.0 + web: '>=0.3.0 <0.5.0' dev_dependencies: flutter_test: diff --git a/packages/firebase_core/firebase_core_web/test/firebase_core_web_test.dart b/packages/firebase_core/firebase_core_web/test/firebase_core_web_test.dart index fbf87ac6dd27..05126c3ad599 100644 --- a/packages/firebase_core/firebase_core_web/test/firebase_core_web_test.dart +++ b/packages/firebase_core/firebase_core_web/test/firebase_core_web_test.dart @@ -39,77 +39,5 @@ void main() { final List apps = FirebasePlatform.instance.apps; expect(apps, hasLength(0)); }); - - test('.app()', () async { - (js.context['firebase_core'] as js.JsObject)['getApp'] = - js.allowInterop((String name) { - return js.JsObject.jsify({ - 'name': name, - 'options': { - 'apiKey': 'abc', - 'appId': '123', - 'messagingSenderId': 'msg', - 'projectId': 'test', - }, - }); - }); - - final FirebaseAppPlatform app = FirebasePlatform.instance.app('foo'); - - expect(app.name, equals('foo')); - - expect(app.options.apiKey, equals('abc')); - expect(app.options.appId, equals('123')); - expect(app.options.messagingSenderId, equals('msg')); - expect(app.options.projectId, equals('test')); - }); - - test('.initializeApp()', () async { - bool appConfigured = false; - - (js.context['firebase_core'] as js.JsObject)['getApp'] = - js.allowInterop((String name) { - if (appConfigured) { - return js.JsObject.jsify({ - 'name': name, - 'options': { - 'apiKey': 'abc', - 'appId': '123', - 'messagingSenderId': 'msg', - 'projectId': 'test', - }, - }); - } else { - return null; - } - }); - - // Prevents a warning log. - (js.context['firebase_core'] as js.JsObject)['SDK_VERSION'] = - supportedFirebaseJsSdkVersion; - - (js.context['firebase_core'] as js.JsObject)['initializeApp'] = - js.allowInterop((js.JsObject options, String name) { - appConfigured = true; - return js.JsObject.jsify({ - 'name': name, - 'options': options, - }); - }); - - final FirebaseAppPlatform app = - await FirebasePlatform.instance.initializeApp( - name: 'foo', - options: const FirebaseOptions( - apiKey: 'abc', - appId: '123', - messagingSenderId: 'msg', - projectId: 'test', - ), - ); - - expect(app.name, equals('foo')); - expect(app.options.appId, equals('123')); - }); }); } diff --git a/packages/firebase_core/firebase_core_web/test/tools.dart b/packages/firebase_core/firebase_core_web/test/tools.dart index c6982ff84443..d0f21c11605d 100644 --- a/packages/firebase_core/firebase_core_web/test/tools.dart +++ b/packages/firebase_core/firebase_core_web/test/tools.dart @@ -4,7 +4,6 @@ import 'dart:html'; -import 'package:firebase_core_web/src/interop/js.dart' as dom; import 'package:js/js_util.dart' as js_util; /// Injects a `` tag with the provided [attributes] into the [dom.document].