Skip to content

feat(core, web): migrate web to js_interop to be compatible with WASM #12031

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Jan 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 13 additions & 14 deletions packages/firebase_core/firebase_core_web/lib/firebase_core_web.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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 '';
Expand All @@ -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<String> chunks = code.split('/');
Expand All @@ -73,7 +72,7 @@ FirebaseException _catchJSError(dynamic e) {
return FirebaseException(
plugin: 'core',
code: code,
message: message.replaceAll(' ($rawCode)', ''),
message: message.replaceAll(' ($code)', ''),
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -83,8 +84,8 @@ class FirebaseCoreWeb extends FirebasePlatform {
/// You must ensure the Firebase script is injected before using the service.
List<String> 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)
Expand All @@ -104,48 +105,49 @@ class FirebaseCoreWeb extends FirebasePlatform {
/// document.
@visibleForTesting
Future<void> 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;
}
Expand All @@ -155,7 +157,7 @@ class FirebaseCoreWeb extends FirebasePlatform {
Future<void> _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;
}

Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -367,3 +368,15 @@ R guardNotInitialized<R>(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';
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -30,5 +32,5 @@ class App extends JsObjectWrapper<AppJsImpl> {
}

/// Deletes the app and frees resources of all App's services.
Future<void> delete() => handleThenable(core_interop.deleteApp(jsObject));
Future<void> delete() => core_interop.deleteApp(jsObject).toDart;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -16,10 +18,9 @@ export 'core_interop.dart';

List<App> 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<AppJsImpl>()
.map(App.getInstance)
.toList();

App initializeApp({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<AppJsImpl> getApps();
// List<AppJsImpl>
external JSArray getApps();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once AppJsImpl is moved to an extension type that implements JSObject, this can be JSArray<AppJsImpl>, and a cast won't be needed above, but for now this is the best alternative. :/


/// The current SDK version.
///
Expand All @@ -27,27 +29,31 @@ external AppJsImpl initializeApp(FirebaseOptions options, [String? name]);
external AppJsImpl getApp([String? name]);

@JS()
external PromiseJsImpl<void> 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.
///
/// See: <https://firebase.google.com/docs/reference/js/firebase.FirebaseError>.
@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,
Expand All @@ -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;
Expand Down
Loading