Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,10 @@ class FirebaseAuthWeb extends FirebaseAuthPlatform {
}
}
}
await authDelegate.onWaitInitState();
await guardAuthExceptions(
authDelegate.onWaitInitState,
auth: authDelegate,
);
},
);
FirebaseAuthPlatform.instance = FirebaseAuthWeb.instance;
Expand Down
20 changes: 15 additions & 5 deletions packages/firebase_auth/firebase_auth_web/lib/src/interop/auth.dart
Original file line number Diff line number Diff line change
Expand Up @@ -383,19 +383,29 @@ class Auth extends JsObjectWrapper<auth_interop.AuthJsImpl> {
/// To preserve behavior on web and mobile we store the initial user
/// in `_initUser` and add it manually to the `_changeController`.
Future<void> onWaitInitState() async {
final completer = Completer();
final completer = Completer<void>();
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);
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

One change in the error callback: complete the wait before addError.

final errorWrapper = (JSAny e) {
  if (!completer.isCompleted) {
    completer.completeError(e);
  }
  _changeController?.addError(e);
};

addError is synchronous. If it throws, completeError never runs and await completer.future waits forever again. During plugin registration the controller is still null, so ?.addError does not throw today. Completing first keeps the wait settled if that changes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good point, thanks. Done in 37e61e0: the wait now completes with the error before addError runs, so a throwing addError can't leave completer.future pending.


final unsubscribe =
jsObject.onAuthStateChanged(nextWrapper.toJS, errorWrapper.toJS);

await completer.future;
unsubscribe.callAsFunction();
try {
await completer.future;
} finally {
unsubscribe.callAsFunction();
}
}

JSFunction? _onAuthUnsubscribe;
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Object>(
(e) => (e as JSObject)['code'].dartify() == 'auth/internal-error',
),
),
);
expect(fake.unsubscribed(), isTrue);
},
);
}
Loading