diff --git a/packages/firebase_auth/firebase_auth_web/lib/firebase_auth_web.dart b/packages/firebase_auth/firebase_auth_web/lib/firebase_auth_web.dart index e864cde65f89..e2651d89c78d 100644 --- a/packages/firebase_auth/firebase_auth_web/lib/firebase_auth_web.dart +++ b/packages/firebase_auth/firebase_auth_web/lib/firebase_auth_web.dart @@ -84,7 +84,10 @@ class FirebaseAuthWeb extends FirebaseAuthPlatform { } } } - await authDelegate.onWaitInitState(); + await guardAuthExceptions( + authDelegate.onWaitInitState, + auth: authDelegate, + ); }, ); FirebaseAuthPlatform.instance = FirebaseAuthWeb.instance; diff --git a/packages/firebase_auth/firebase_auth_web/lib/src/interop/auth.dart b/packages/firebase_auth/firebase_auth_web/lib/src/interop/auth.dart index 7e14c3d0981e..00e3413cb6f3 100644 --- a/packages/firebase_auth/firebase_auth_web/lib/src/interop/auth.dart +++ b/packages/firebase_auth/firebase_auth_web/lib/src/interop/auth.dart @@ -383,19 +383,29 @@ class Auth extends JsObjectWrapper { /// To preserve behavior on web and mobile we store the initial user /// in `_initUser` and add it manually to the `_changeController`. Future onWaitInitState() async { - final completer = Completer(); + final completer = Completer(); final nextWrapper = (auth_interop.UserJsImpl? user) { _initUser = User.getInstance(user); - completer.complete(); + if (!completer.isCompleted) { + completer.complete(); + } }; - final errorWrapper = (JSAny e) => _changeController!.addError(e); + final errorWrapper = (JSAny e) { + if (!completer.isCompleted) { + completer.completeError(e); + } + _changeController?.addError(e); + }; final unsubscribe = jsObject.onAuthStateChanged(nextWrapper.toJS, errorWrapper.toJS); - await completer.future; - unsubscribe.callAsFunction(); + try { + await completer.future; + } finally { + unsubscribe.callAsFunction(); + } } JSFunction? _onAuthUnsubscribe; diff --git a/packages/firebase_auth/firebase_auth_web/test/on_wait_init_state_test.dart b/packages/firebase_auth/firebase_auth_web/test/on_wait_init_state_test.dart new file mode 100644 index 000000000000..59bdb0fa7be9 --- /dev/null +++ b/packages/firebase_auth/firebase_auth_web/test/on_wait_init_state_test.dart @@ -0,0 +1,65 @@ +// 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_auth_web/src/interop/auth.dart' as auth_interop; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + /// Minimal stand-in for the JS `Auth` object: `onAuthStateChanged` invokes + /// either the next or the error callback once, and records unsubscribe. + ({auth_interop.Auth auth, bool Function() unsubscribed}) fakeAuth({ + JSObject? error, + }) { + var unsubscribed = false; + final jsAuth = JSObject(); + jsAuth['onAuthStateChanged'] = (JSFunction next, JSFunction? onError) { + if (error != null) { + onError!.callAsFunction(null, error); + } else { + // ignore: avoid_redundant_argument_values + next.callAsFunction(null, null); + } + return (() { + unsubscribed = true; + }).toJS; + }.toJS; + return ( + auth: auth_interop.Auth.getInstance(jsAuth as auth_interop.AuthJsImpl), + unsubscribed: () => unsubscribed, + ); + } + + test('onWaitInitState completes when the first auth state arrives', () async { + final fake = fakeAuth(); + + await fake.auth.onWaitInitState(); + + expect(fake.unsubscribed(), isTrue); + }); + + test( + 'onWaitInitState fails instead of hanging when the first auth state is an error', + () async { + final error = JSObject(); + error['code'] = 'auth/internal-error'.toJS; + final fake = fakeAuth(error: error); + + await expectLater( + fake.auth.onWaitInitState().timeout(const Duration(seconds: 2)), + throwsA( + predicate( + (e) => (e as JSObject)['code'].dartify() == 'auth/internal-error', + ), + ), + ); + expect(fake.unsubscribed(), isTrue); + }, + ); +}