From 7f49a978f3fc589c8ecfa9b1b4cab24c9c224088 Mon Sep 17 00:00:00 2001 From: Milen Marinov Date: Thu, 8 Oct 2026 23:22:42 +0300 Subject: [PATCH] fix(ui_auth): don't call setState in ForgotPasswordView after it is disposed `_ForgotPasswordViewState._submit` awaits `sendPasswordResetEmail` and then calls `setState` in a `finally` block without checking `mounted`. If the view is popped while the request is in flight (e.g. via its own "Go back" button), the `setState` runs on a defunct State. In debug builds this is the "setState() called after dispose()" assertion; in release builds the assertion is stripped and `_element!` throws "Null check operator used on a null value" from `State.setState`. Guard the `setState` with `mounted` and add a regression test that pops the view while the request is still pending. --- .../lib/src/views/forgot_password_view.dart | 4 +- .../test/views/forgot_password_view_test.dart | 43 +++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/packages/firebase_ui_auth/lib/src/views/forgot_password_view.dart b/packages/firebase_ui_auth/lib/src/views/forgot_password_view.dart index 351a90b2..a53e30d8 100644 --- a/packages/firebase_ui_auth/lib/src/views/forgot_password_view.dart +++ b/packages/firebase_ui_auth/lib/src/views/forgot_password_view.dart @@ -73,7 +73,9 @@ class _ForgotPasswordViewState extends State { } on fba.FirebaseAuthException catch (e) { exception = e; } finally { - setState(() => isLoading = false); + if (mounted) { + setState(() => isLoading = false); + } } } diff --git a/packages/firebase_ui_auth/test/views/forgot_password_view_test.dart b/packages/firebase_ui_auth/test/views/forgot_password_view_test.dart index fb9f921b..6595b0eb 100644 --- a/packages/firebase_ui_auth/test/views/forgot_password_view_test.dart +++ b/packages/firebase_ui_auth/test/views/forgot_password_view_test.dart @@ -2,6 +2,8 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. +import 'dart:async'; + import 'package:firebase_auth/firebase_auth.dart' as fba; import 'package:firebase_ui_auth/firebase_ui_auth.dart'; import 'package:firebase_ui_shared/firebase_ui_shared.dart'; @@ -95,5 +97,46 @@ void main() { expect(find.byType(ErrorText), findsNothing); verify(auth.sendPasswordResetEmail(email: 'delay@email.com')).called(1); }); + + testWidgets( + "doesn't call setState if popped while sendPasswordResetEmail is pending", + (tester) async { + final completer = Completer(); + when( + auth.sendPasswordResetEmail(email: 'pending@email.com'), + ).thenAnswer((_) => completer.future); + + await tester.pumpWidget( + MaterialApp( + home: Builder( + builder: (context) => TextButton( + onPressed: () => Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => + Scaffold(body: ForgotPasswordView(auth: auth)), + ), + ), + child: const Text('open'), + ), + ), + ), + ); + await tester.tap(find.text('open')); + await tester.pumpAndSettle(); + + await tester.enterText(find.byType(TextField), 'pending@email.com'); + await tester.tap(find.byType(LoadingButton)); + await tester.pump(); + + await tester.tap(find.text('Go back')); + await tester.pumpAndSettle(); + expect(find.byType(ForgotPasswordView), findsNothing); + + completer.complete(); + await tester.pump(); + + expect(tester.takeException(), isNull); + }, + ); }); }