From 405bbe7d04d799c80ec11fa3d5a3c4a97ae2b9e4 Mon Sep 17 00:00:00 2001 From: Fabio Carneiro Date: Fri, 23 Jan 2026 12:11:51 +0100 Subject: [PATCH 1/3] feat(auth): add providerBuilder and DI support for layout customization --- .../lib/firebase_ui_auth.dart | 3 +- .../lib/src/auth_controller.dart | 45 ++++ .../screens/email_link_sign_in_screen.dart | 11 +- .../screens/email_verification_screen.dart | 37 +-- .../src/screens/forgot_password_screen.dart | 30 ++- .../src/screens/internal/login_screen.dart | 5 + .../internal/multi_provider_screen.dart | 5 +- .../lib/src/screens/phone_input_screen.dart | 56 +++-- .../lib/src/screens/register_screen.dart | 8 + .../lib/src/screens/sign_in_screen.dart | 8 + .../src/screens/sms_code_input_screen.dart | 86 ++++--- .../lib/src/views/login_view.dart | 125 +++++++--- .../lib/src/widgets/auth_flow_builder.dart | 21 +- .../src/widgets/delete_account_button.dart | 5 +- .../widgets/editable_user_display_name.dart | 6 +- .../lib/src/widgets/email_form.dart | 7 +- .../widgets/email_link_sign_in_button.dart | 6 +- .../widgets/phone_verification_button.dart | 7 +- .../lib/src/widgets/sign_out_button.dart | 6 +- .../lib/src/widgets/user_avatar.dart | 6 +- .../flows/auth_state_change_action_test.dart | 236 ++++++++++++++++++ .../flows/auth_state_transition_test.dart | 70 ++++++ ...egister_screen_providers_builder_test.dart | 32 +++ ...sign_in_screen_providers_builder_test.dart | 32 +++ .../login_view_providers_builder_test.dart | 83 ++++++ .../lib/src/oauth_provider_button_base.dart | 9 +- .../test/flutterfire_ui_oauth_test.dart | 68 +++++ .../lib/firebase_ui_oauth_apple.dart | 34 ++- packages/firebase_ui_oauth_apple/pubspec.yaml | 1 + .../lib/firebase_ui_oauth_facebook.dart | 33 ++- .../firebase_ui_oauth_facebook/pubspec.yaml | 1 + .../lib/firebase_ui_oauth_google.dart | 34 ++- .../firebase_ui_oauth_google/pubspec.yaml | 1 + .../lib/firebase_ui_oauth_twitter.dart | 34 ++- .../firebase_ui_oauth_twitter/pubspec.yaml | 1 + 35 files changed, 953 insertions(+), 199 deletions(-) create mode 100644 packages/firebase_ui_auth/test/flows/auth_state_change_action_test.dart create mode 100644 packages/firebase_ui_auth/test/flows/auth_state_transition_test.dart create mode 100644 packages/firebase_ui_auth/test/screens/register_screen_providers_builder_test.dart create mode 100644 packages/firebase_ui_auth/test/screens/sign_in_screen_providers_builder_test.dart create mode 100644 packages/firebase_ui_auth/test/views/login_view_providers_builder_test.dart diff --git a/packages/firebase_ui_auth/lib/firebase_ui_auth.dart b/packages/firebase_ui_auth/lib/firebase_ui_auth.dart index 0d080a05..0366f045 100644 --- a/packages/firebase_ui_auth/lib/firebase_ui_auth.dart +++ b/packages/firebase_ui_auth/lib/firebase_ui_auth.dart @@ -14,7 +14,8 @@ import 'src/oauth_providers.dart'; import 'src/providers/auth_provider.dart'; export 'src/actions.dart'; -export 'src/auth_controller.dart' show AuthAction, AuthController; +export 'src/auth_controller.dart' + show AuthAction, AuthController, AuthControllerProvider, FirebaseAuthProvider; export 'src/auth_flow.dart'; export 'src/auth_state.dart' show diff --git a/packages/firebase_ui_auth/lib/src/auth_controller.dart b/packages/firebase_ui_auth/lib/src/auth_controller.dart index 9673c572..d45f48af 100644 --- a/packages/firebase_ui_auth/lib/src/auth_controller.dart +++ b/packages/firebase_ui_auth/lib/src/auth_controller.dart @@ -88,3 +88,48 @@ class AuthControllerProvider extends InheritedWidget { return ctrl != oldWidget.ctrl || action != oldWidget.action; } } + +/// A widget that provides an instance of [fba.FirebaseAuth] down the widget tree. +class FirebaseAuthProvider extends InheritedWidget { + /// An instance of [fba.FirebaseAuth] to provide. + final fba.FirebaseAuth auth; + + const FirebaseAuthProvider({ + super.key, + required this.auth, + required super.child, + }); + + @override + bool updateShouldNotify(FirebaseAuthProvider oldWidget) { + return auth != oldWidget.auth; + } + + /// Looks up an instance of [fba.FirebaseAuth] in the widget tree. + static fba.FirebaseAuth? maybeOf(BuildContext context) { + return context + .dependOnInheritedWidgetOfExactType() + ?.auth; + } + + /// Looks up an instance of [fba.FirebaseAuth] in the widget tree without + /// registering a dependency. + static fba.FirebaseAuth? findAuth(BuildContext context) { + return context.getInheritedWidgetOfExactType()?.auth; + } + + /// Looks up an instance of [fba.FirebaseAuth] in the widget tree. + /// Throws an [Exception] if no [FirebaseAuthProvider] was found. + static fba.FirebaseAuth of(BuildContext context) { + final auth = maybeOf(context); + + if (auth == null) { + throw Exception( + 'No FirebaseAuthProvider found. ' + 'Make sure to wrap your code with FirebaseAuthProvider', + ); + } + + return auth; + } +} diff --git a/packages/firebase_ui_auth/lib/src/screens/email_link_sign_in_screen.dart b/packages/firebase_ui_auth/lib/src/screens/email_link_sign_in_screen.dart index 0d31542b..efbba365 100644 --- a/packages/firebase_ui_auth/lib/src/screens/email_link_sign_in_screen.dart +++ b/packages/firebase_ui_auth/lib/src/screens/email_link_sign_in_screen.dart @@ -2,6 +2,7 @@ // 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 'package:firebase_auth/firebase_auth.dart' as fba; import 'package:firebase_ui_shared/firebase_ui_shared.dart'; import 'package:flutter/widgets.dart'; import 'package:firebase_ui_auth/firebase_ui_auth.dart'; @@ -61,6 +62,8 @@ class EmailLinkSignInScreen extends ProviderScreen { @override Widget build(BuildContext context) { + final auth = this.auth ?? FirebaseAuthProvider.maybeOf(context) ?? fba.FirebaseAuth.instance; + final child = UniversalScaffold( body: ResponsivePage( breakpoint: breakpoint, @@ -76,6 +79,12 @@ class EmailLinkSignInScreen extends ProviderScreen { ), ); - return FirebaseUIActions(actions: actions ?? const [], child: child); + return FirebaseAuthProvider( + auth: auth, + child: FirebaseUIActions( + actions: actions ?? const [], + child: child, + ), + ); } } diff --git a/packages/firebase_ui_auth/lib/src/screens/email_verification_screen.dart b/packages/firebase_ui_auth/lib/src/screens/email_verification_screen.dart index 96e993bd..debec6c7 100644 --- a/packages/firebase_ui_auth/lib/src/screens/email_verification_screen.dart +++ b/packages/firebase_ui_auth/lib/src/screens/email_verification_screen.dart @@ -84,22 +84,27 @@ class EmailVerificationScreen extends StatelessWidget { @override Widget build(BuildContext context) { - return FirebaseUIActions( - actions: actions, - child: UniversalScaffold( - body: ResponsivePage( - breakpoint: breakpoint, - desktopLayoutDirection: desktopLayoutDirection, - headerBuilder: headerBuilder, - headerMaxExtent: headerMaxExtent, - sideBuilder: sideBuilder, - maxWidth: maxWidth, - contentFlex: 2, - child: Padding( - padding: const EdgeInsets.all(32), - child: _EmailVerificationScreenContent( - auth: auth, - actionCodeSettings: actionCodeSettings, + final auth = this.auth ?? FirebaseAuthProvider.maybeOf(context) ?? fba.FirebaseAuth.instance; + + return FirebaseAuthProvider( + auth: auth, + child: FirebaseUIActions( + actions: actions, + child: UniversalScaffold( + body: ResponsivePage( + breakpoint: breakpoint, + desktopLayoutDirection: desktopLayoutDirection, + headerBuilder: headerBuilder, + headerMaxExtent: headerMaxExtent, + sideBuilder: sideBuilder, + maxWidth: maxWidth, + contentFlex: 2, + child: Padding( + padding: const EdgeInsets.all(32), + child: _EmailVerificationScreenContent( + auth: auth, + actionCodeSettings: actionCodeSettings, + ), ), ), ), diff --git a/packages/firebase_ui_auth/lib/src/screens/forgot_password_screen.dart b/packages/firebase_ui_auth/lib/src/screens/forgot_password_screen.dart index 1b68c766..30323444 100644 --- a/packages/firebase_ui_auth/lib/src/screens/forgot_password_screen.dart +++ b/packages/firebase_ui_auth/lib/src/screens/forgot_password_screen.dart @@ -61,6 +61,8 @@ class ForgotPasswordScreen extends StatelessWidget { @override Widget build(BuildContext context) { + final auth = this.auth ?? FirebaseAuthProvider.maybeOf(context) ?? fba.FirebaseAuth.instance; + final child = ForgotPasswordView( auth: auth, email: email, @@ -68,17 +70,23 @@ class ForgotPasswordScreen extends StatelessWidget { subtitleBuilder: subtitleBuilder, ); - return UniversalScaffold( - resizeToAvoidBottomInset: resizeToAvoidBottomInset, - body: ResponsivePage( - desktopLayoutDirection: desktopLayoutDirection, - headerBuilder: headerBuilder, - headerMaxExtent: headerMaxExtent, - sideBuilder: sideBuilder, - breakpoint: breakpoint, - maxWidth: maxWidth, - contentFlex: 1, - child: Padding(padding: const EdgeInsets.all(32), child: child), + return FirebaseAuthProvider( + auth: auth, + child: UniversalScaffold( + resizeToAvoidBottomInset: resizeToAvoidBottomInset, + body: ResponsivePage( + desktopLayoutDirection: desktopLayoutDirection, + headerBuilder: headerBuilder, + headerMaxExtent: headerMaxExtent, + sideBuilder: sideBuilder, + breakpoint: breakpoint, + maxWidth: maxWidth, + contentFlex: 1, + child: Padding( + padding: const EdgeInsets.all(32), + child: child, + ), + ), ), ); } diff --git a/packages/firebase_ui_auth/lib/src/screens/internal/login_screen.dart b/packages/firebase_ui_auth/lib/src/screens/internal/login_screen.dart index a7fc528e..6fed0868 100644 --- a/packages/firebase_ui_auth/lib/src/screens/internal/login_screen.dart +++ b/packages/firebase_ui_auth/lib/src/screens/internal/login_screen.dart @@ -57,6 +57,9 @@ class LoginScreen extends StatelessWidget { /// {@macro ui.auth.screens.responsive_page.max_width} final double? maxWidth; + /// A builder that allows to customize the order and appearance of the providers. + final ProvidersBuilder? providersBuilder; + const LoginScreen({ super.key, required this.action, @@ -77,6 +80,7 @@ class LoginScreen extends StatelessWidget { this.styles, this.showPasswordVisibilityToggle = false, this.maxWidth, + this.providersBuilder, }); @override @@ -96,6 +100,7 @@ class LoginScreen extends StatelessWidget { subtitleBuilder: subtitleBuilder, footerBuilder: footerBuilder, showPasswordVisibilityToggle: showPasswordVisibilityToggle, + providersBuilder: providersBuilder, ), ), ); diff --git a/packages/firebase_ui_auth/lib/src/screens/internal/multi_provider_screen.dart b/packages/firebase_ui_auth/lib/src/screens/internal/multi_provider_screen.dart index b1d04baf..c4b5c970 100644 --- a/packages/firebase_ui_auth/lib/src/screens/internal/multi_provider_screen.dart +++ b/packages/firebase_ui_auth/lib/src/screens/internal/multi_provider_screen.dart @@ -61,6 +61,9 @@ class ScreenElement extends ComponentElement { @override Widget build() { - return widget.build(this); + return FirebaseAuthProvider( + auth: widget.auth, + child: widget.build(this), + ); } } diff --git a/packages/firebase_ui_auth/lib/src/screens/phone_input_screen.dart b/packages/firebase_ui_auth/lib/src/screens/phone_input_screen.dart index 1375ffce..0de00d54 100644 --- a/packages/firebase_ui_auth/lib/src/screens/phone_input_screen.dart +++ b/packages/firebase_ui_auth/lib/src/screens/phone_input_screen.dart @@ -102,32 +102,36 @@ class PhoneInputScreen extends StatelessWidget { @override Widget build(BuildContext context) { final flowKey = Object(); - - return FirebaseUIActions( - actions: actions ?? [SMSCodeRequestedAction(_next)], - child: UniversalScaffold( - body: ResponsivePage( - desktopLayoutDirection: desktopLayoutDirection, - sideBuilder: sideBuilder, - headerBuilder: headerBuilder, - headerMaxExtent: headerMaxExtent, - breakpoint: breakpoint, - maxWidth: maxWidth, - child: Padding( - padding: const EdgeInsets.all(20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - PhoneInputView( - auth: auth, - action: action, - subtitleBuilder: subtitleBuilder, - footerBuilder: footerBuilder, - flowKey: flowKey, - multiFactorSession: multiFactorSession, - mfaHint: mfaHint, - ), - ], + final auth = this.auth ?? FirebaseAuthProvider.maybeOf(context) ?? fba.FirebaseAuth.instance; + + return FirebaseAuthProvider( + auth: auth, + child: FirebaseUIActions( + actions: actions ?? [SMSCodeRequestedAction(_next)], + child: UniversalScaffold( + body: ResponsivePage( + desktopLayoutDirection: desktopLayoutDirection, + sideBuilder: sideBuilder, + headerBuilder: headerBuilder, + headerMaxExtent: headerMaxExtent, + breakpoint: breakpoint, + maxWidth: maxWidth, + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + PhoneInputView( + auth: auth, + action: action, + subtitleBuilder: subtitleBuilder, + footerBuilder: footerBuilder, + flowKey: flowKey, + multiFactorSession: multiFactorSession, + mfaHint: mfaHint, + ), + ], + ), ), ), ), diff --git a/packages/firebase_ui_auth/lib/src/screens/register_screen.dart b/packages/firebase_ui_auth/lib/src/screens/register_screen.dart index 362913d3..94b94a93 100644 --- a/packages/firebase_ui_auth/lib/src/screens/register_screen.dart +++ b/packages/firebase_ui_auth/lib/src/screens/register_screen.dart @@ -93,6 +93,12 @@ class RegisterScreen extends MultiProviderScreen { /// {@macro ui.auth.screens.responsive_page.max_width} final double? maxWidth; + /// A builder that allows to customize the order and appearance of the providers. + /// + /// If not provided, the default explicit order is used: + /// Email, Phone, Email Link, OAuth. + final ProvidersBuilder? providersBuilder; + const RegisterScreen({ super.key, super.auth, @@ -112,6 +118,7 @@ class RegisterScreen extends MultiProviderScreen { this.styles, this.showPasswordVisibilityToggle = false, this.maxWidth, + this.providersBuilder, }); @override @@ -136,6 +143,7 @@ class RegisterScreen extends MultiProviderScreen { breakpoint: breakpoint, showPasswordVisibilityToggle: showPasswordVisibilityToggle, maxWidth: maxWidth, + providersBuilder: providersBuilder, ), ); } diff --git a/packages/firebase_ui_auth/lib/src/screens/sign_in_screen.dart b/packages/firebase_ui_auth/lib/src/screens/sign_in_screen.dart index 79b92bd8..0266e5e5 100644 --- a/packages/firebase_ui_auth/lib/src/screens/sign_in_screen.dart +++ b/packages/firebase_ui_auth/lib/src/screens/sign_in_screen.dart @@ -101,6 +101,12 @@ class SignInScreen extends MultiProviderScreen { /// {@macro ui.auth.screens.responsive_page.max_width} final double? maxWidth; + /// A builder that allows to customize the order and appearance of the providers. + /// + /// If not provided, the default explicit order is used: + /// Email, Phone, Email Link, OAuth. + final ProvidersBuilder? providersBuilder; + /// {@macro ui.auth.screens.sign_in_screen} const SignInScreen({ super.key, @@ -122,6 +128,7 @@ class SignInScreen extends MultiProviderScreen { this.styles, this.showPasswordVisibilityToggle = false, this.maxWidth, + this.providersBuilder, }); @override @@ -149,6 +156,7 @@ class SignInScreen extends MultiProviderScreen { breakpoint: breakpoint, showPasswordVisibilityToggle: showPasswordVisibilityToggle, maxWidth: maxWidth, + providersBuilder: providersBuilder, ), ); } diff --git a/packages/firebase_ui_auth/lib/src/screens/sms_code_input_screen.dart b/packages/firebase_ui_auth/lib/src/screens/sms_code_input_screen.dart index 55eb050d..8f49e8ab 100644 --- a/packages/firebase_ui_auth/lib/src/screens/sms_code_input_screen.dart +++ b/packages/firebase_ui_auth/lib/src/screens/sms_code_input_screen.dart @@ -92,49 +92,53 @@ class SMSCodeInputScreen extends StatelessWidget { @override Widget build(BuildContext context) { final l = FirebaseUILocalizations.labelsOf(context); + final auth = this.auth ?? FirebaseAuthProvider.maybeOf(context) ?? fba.FirebaseAuth.instance; // ignore: deprecated_member_use - return WillPopScope( - onWillPop: () async { - _reset(); - return true; - }, - child: FirebaseUIActions( - actions: actions ?? const [], - child: UniversalScaffold( - body: Center( - child: ResponsivePage( - breakpoint: breakpoint, - maxWidth: maxWidth, - desktopLayoutDirection: desktopLayoutDirection, - sideBuilder: sideBuilder, - headerBuilder: headerBuilder, - headerMaxExtent: headerMaxExtent, - contentFlex: contentFlex, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - SMSCodeInputView( - auth: auth, - action: action, - flowKey: flowKey, - onCodeVerified: () { - if (actions != null) return; - - Navigator.of(context).popUntil((route) { - return route.isFirst; - }); - }, - ), - UniversalButton( - variant: ButtonVariant.text, - text: l.goBackButtonLabel, - onPressed: () { - _reset(); - Navigator.of(context).pop(); - }, - ), - ], + return FirebaseAuthProvider( + auth: auth, + child: WillPopScope( + onWillPop: () async { + _reset(); + return true; + }, + child: FirebaseUIActions( + actions: actions ?? const [], + child: UniversalScaffold( + body: Center( + child: ResponsivePage( + breakpoint: breakpoint, + maxWidth: maxWidth, + desktopLayoutDirection: desktopLayoutDirection, + sideBuilder: sideBuilder, + headerBuilder: headerBuilder, + headerMaxExtent: headerMaxExtent, + contentFlex: contentFlex, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SMSCodeInputView( + auth: auth, + action: action, + flowKey: flowKey, + onCodeVerified: () { + if (actions != null) return; + + Navigator.of(context).popUntil((route) { + return route.isFirst; + }); + }, + ), + UniversalButton( + variant: ButtonVariant.text, + text: l.goBackButtonLabel, + onPressed: () { + _reset(); + Navigator.of(context).pop(); + }, + ) + ], + ), ), ), ), diff --git a/packages/firebase_ui_auth/lib/src/views/login_view.dart b/packages/firebase_ui_auth/lib/src/views/login_view.dart index f2617c48..a0d379bb 100644 --- a/packages/firebase_ui_auth/lib/src/views/login_view.dart +++ b/packages/firebase_ui_auth/lib/src/views/login_view.dart @@ -16,6 +16,12 @@ import '../widgets/internal/title.dart'; typedef AuthViewContentBuilder = Widget Function(BuildContext context, AuthAction action); +typedef ProvidersBuilder = List Function( + BuildContext context, + List providers, + AuthAction action, +); + /// {@template ui.auth.views.login_view} /// A view that could be used to build a custom [SignInScreen] or /// [RegisterScreen]. @@ -56,6 +62,12 @@ class LoginView extends StatefulWidget { /// {@macro ui.auth.widgets.email_from.showPasswordVisibilityToggle} final bool showPasswordVisibilityToggle; + /// A builder that allows to customize the order and appearance of the providers. + /// + /// If not provided, the default explicit order is used: + /// Email, Phone, Email Link, OAuth. + final ProvidersBuilder? providersBuilder; + /// {@macro ui.auth.views.login_view} const LoginView({ super.key, @@ -70,6 +82,7 @@ class LoginView extends StatefulWidget { this.subtitleBuilder, this.actionButtonLabelOverride, this.showPasswordVisibilityToggle = false, + this.providersBuilder, }); @override @@ -193,47 +206,91 @@ class _LoginViewState extends State { super.didUpdateWidget(oldWidget); } - @override - Widget build(BuildContext context) { + Widget? _buildProviderWidget(TargetPlatform platform, AuthProvider provider) { final l = FirebaseUILocalizations.labelsOf(context); + + if (provider is EmailAuthProvider) { + return EmailForm( + key: ValueKey(_action), + auth: widget.auth, + action: _action, + provider: provider, + email: widget.email, + actionButtonLabelOverride: widget.actionButtonLabelOverride, + showPasswordVisibilityToggle: widget.showPasswordVisibilityToggle, + ); + } else if (provider is PhoneAuthProvider) { + return PhoneVerificationButton( + label: l.signInWithPhoneButtonText, + action: _action, + auth: widget.auth, + ); + } else if (provider is EmailLinkAuthProvider) { + return EmailLinkSignInButton( + auth: widget.auth, + provider: provider, + ); + } else if (provider is OAuthProvider && !_buttonsBuilt) { + return _buildOAuthButtons(platform); + } + return null; + } + + List _defaultProvidersBuilder( + BuildContext context, + List providers, + AuthAction action, + ) { final platform = Theme.of(context).platform; + final children = []; + + void addForType() { + for (var provider in providers) { + if (provider is T && provider.supportsPlatform(platform)) { + final w = _buildProviderWidget(platform, provider); + + if (w != null) { + if (provider is OAuthProvider) { + children.add(w); + } else { + children.add(const SizedBox(height: 8)); + children.add(w); + if (provider is PhoneAuthProvider) { + children.add(const SizedBox(height: 8)); + } + } + } + } + } + } + + addForType(); + addForType(); + addForType(); + addForType(); + + return children; + } + + @override + Widget build(BuildContext context) { _buttonsBuilt = false; + final children = [ + if (_showTitle) ..._buildHeader(context), + ]; + + final builder = widget.providersBuilder ?? _defaultProvidersBuilder; + children.addAll(builder(context, widget.providers, _action)); + + if (widget.footerBuilder != null) { + children.add(widget.footerBuilder!(context, _action)); + } + return IntrinsicHeight( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - if (_showTitle) ..._buildHeader(context), - for (var provider in widget.providers) - if (provider.supportsPlatform(platform)) - if (provider is EmailAuthProvider) ...[ - const SizedBox(height: 8), - EmailForm( - key: ValueKey(_action), - auth: widget.auth, - action: _action, - provider: provider, - email: widget.email, - actionButtonLabelOverride: widget.actionButtonLabelOverride, - showPasswordVisibilityToggle: - widget.showPasswordVisibilityToggle, - ), - ] else if (provider is PhoneAuthProvider) ...[ - const SizedBox(height: 8), - PhoneVerificationButton( - label: l.signInWithPhoneButtonText, - action: _action, - auth: widget.auth, - ), - const SizedBox(height: 8), - ] else if (provider is EmailLinkAuthProvider) ...[ - const SizedBox(height: 8), - EmailLinkSignInButton(auth: widget.auth, provider: provider), - ] else if (provider is OAuthProvider && !_buttonsBuilt) - _buildOAuthButtons(platform), - if (widget.footerBuilder != null) - widget.footerBuilder!(context, _action), - ], + children: children, ), ); } diff --git a/packages/firebase_ui_auth/lib/src/widgets/auth_flow_builder.dart b/packages/firebase_ui_auth/lib/src/widgets/auth_flow_builder.dart index 461a861d..6ef96ae8 100644 --- a/packages/firebase_ui_auth/lib/src/widgets/auth_flow_builder.dart +++ b/packages/firebase_ui_auth/lib/src/widgets/auth_flow_builder.dart @@ -201,9 +201,13 @@ class _AuthFlowBuilderState @override void initState() { super.initState(); - provider.auth = widget.auth ?? fba.FirebaseAuth.instance; + final resolvedAuth = widget.auth ?? + FirebaseAuthProvider.findAuth(context) ?? + fba.FirebaseAuth.instance; - flow = widget.flow ?? createFlow(); + provider.auth = resolvedAuth; + + flow = widget.flow ?? createFlow(resolvedAuth); if (widget.flowKey != null) { AuthFlowBuilder._flows[widget.flowKey!] = flow; @@ -232,7 +236,7 @@ class _AuthFlowBuilderState } } - AuthFlow createFlow() { + AuthFlow createFlow(fba.FirebaseAuth auth) { if (widget.flowKey != null) { final existingFlow = AuthFlowBuilder._flows[widget.flowKey!]; if (existingFlow != null) { @@ -246,21 +250,24 @@ class _AuthFlowBuilderState return EmailAuthFlow( provider: provider, action: widget.action, - auth: widget.auth, + auth: auth, ); } else if (provider is EmailLinkAuthProvider) { - return EmailLinkFlow(provider: provider, auth: widget.auth); + return EmailLinkFlow( + provider: provider, + auth: auth, + ); } else if (provider is OAuthProvider) { return OAuthFlow( provider: provider, action: widget.action, - auth: widget.auth, + auth: auth, ); } else if (provider is PhoneAuthProvider) { return PhoneAuthFlow( provider: provider, action: widget.action, - auth: widget.auth, + auth: auth, ); } else { throw Exception('Unknown provider $provider'); diff --git a/packages/firebase_ui_auth/lib/src/widgets/delete_account_button.dart b/packages/firebase_ui_auth/lib/src/widgets/delete_account_button.dart index 9634404b..f56672f9 100644 --- a/packages/firebase_ui_auth/lib/src/widgets/delete_account_button.dart +++ b/packages/firebase_ui_auth/lib/src/widgets/delete_account_button.dart @@ -85,7 +85,10 @@ class DeleteAccountButton extends StatefulWidget { } class _DeleteAccountButtonState extends State { - fba.FirebaseAuth get auth => widget.auth ?? fba.FirebaseAuth.instance; + fba.FirebaseAuth get auth => + widget.auth ?? + FirebaseAuthProvider.maybeOf(context) ?? + fba.FirebaseAuth.instance; bool _isLoading = false; void Function() pop(BuildContext context, T result) => diff --git a/packages/firebase_ui_auth/lib/src/widgets/editable_user_display_name.dart b/packages/firebase_ui_auth/lib/src/widgets/editable_user_display_name.dart index af9ddf22..fba65d2f 100644 --- a/packages/firebase_ui_auth/lib/src/widgets/editable_user_display_name.dart +++ b/packages/firebase_ui_auth/lib/src/widgets/editable_user_display_name.dart @@ -3,6 +3,7 @@ // BSD-style license that can be found in the LICENSE file. import 'package:firebase_auth/firebase_auth.dart' as fba; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; import 'package:firebase_ui_localizations/firebase_ui_localizations.dart'; import 'package:firebase_ui_shared/firebase_ui_shared.dart'; import 'package:flutter/cupertino.dart'; @@ -45,7 +46,10 @@ class EditableUserDisplayName extends StatefulWidget { } class _EditableUserDisplayNameState extends State { - fba.FirebaseAuth get auth => widget.auth ?? fba.FirebaseAuth.instance; + fba.FirebaseAuth get auth => + widget.auth ?? + FirebaseAuthProvider.maybeOf(context) ?? + fba.FirebaseAuth.instance; String? get displayName => auth.currentUser?.displayName; late final ctrl = TextEditingController(text: displayName ?? ''); diff --git a/packages/firebase_ui_auth/lib/src/widgets/email_form.dart b/packages/firebase_ui_auth/lib/src/widgets/email_form.dart index 83986080..dfdf0ae6 100644 --- a/packages/firebase_ui_auth/lib/src/widgets/email_form.dart +++ b/packages/firebase_ui_auth/lib/src/widgets/email_form.dart @@ -202,6 +202,11 @@ class _SignInFormContent extends StatefulWidget { } class _SignInFormContentState extends State<_SignInFormContent> { + fba.FirebaseAuth get auth => + widget.auth ?? + FirebaseAuthProvider.maybeOf(context) ?? + fba.FirebaseAuth.instance; + final emailCtrl = TextEditingController(); final passwordCtrl = TextEditingController(); final confirmPasswordCtrl = TextEditingController(); @@ -283,7 +288,7 @@ class _SignInFormContentState extends State<_SignInFormContent> { showForgotPasswordScreen( context: context, email: email, - auth: widget.auth, + auth: auth, ); } }, diff --git a/packages/firebase_ui_auth/lib/src/widgets/email_link_sign_in_button.dart b/packages/firebase_ui_auth/lib/src/widgets/email_link_sign_in_button.dart index 02b45eca..db0d69f0 100644 --- a/packages/firebase_ui_auth/lib/src/widgets/email_link_sign_in_button.dart +++ b/packages/firebase_ui_auth/lib/src/widgets/email_link_sign_in_button.dart @@ -32,6 +32,7 @@ class EmailLinkSignInButton extends StatelessWidget { @override Widget build(BuildContext context) { final l = FirebaseUILocalizations.labelsOf(context); + final resolvedAuth = auth ?? FirebaseAuthProvider.maybeOf(context); return UniversalButton( text: l.emailLinkSignInButtonLabel, @@ -48,7 +49,10 @@ class EmailLinkSignInButton extends StatelessWidget { builder: (_) { return FirebaseUIActions.inherit( from: context, - child: EmailLinkSignInScreen(auth: auth, provider: provider), + child: EmailLinkSignInScreen( + auth: resolvedAuth, + provider: provider, + ), ); }, ), diff --git a/packages/firebase_ui_auth/lib/src/widgets/phone_verification_button.dart b/packages/firebase_ui_auth/lib/src/widgets/phone_verification_button.dart index 2db7e1bb..5670b9b7 100644 --- a/packages/firebase_ui_auth/lib/src/widgets/phone_verification_button.dart +++ b/packages/firebase_ui_auth/lib/src/widgets/phone_verification_button.dart @@ -34,11 +34,16 @@ class PhoneVerificationButton extends StatelessWidget { void _onPressed(BuildContext context) { final a = FirebaseUIAction.ofType(context); + final resolvedAuth = auth ?? FirebaseAuthProvider.maybeOf(context); if (a != null) { a.callback(context, action); } else { - startPhoneVerification(context: context, action: action, auth: auth); + startPhoneVerification( + context: context, + action: action, + auth: resolvedAuth, + ); } } diff --git a/packages/firebase_ui_auth/lib/src/widgets/sign_out_button.dart b/packages/firebase_ui_auth/lib/src/widgets/sign_out_button.dart index 19386b10..61987256 100644 --- a/packages/firebase_ui_auth/lib/src/widgets/sign_out_button.dart +++ b/packages/firebase_ui_auth/lib/src/widgets/sign_out_button.dart @@ -29,10 +29,14 @@ class SignOutButton extends StatelessWidget { @override Widget build(BuildContext context) { final l = FirebaseUILocalizations.labelsOf(context); + final resolvedAuth = auth ?? FirebaseAuthProvider.maybeOf(context); return UniversalButton( text: l.signOutButtonText, - onPressed: () => FirebaseUIAuth.signOut(context: context, auth: auth), + onPressed: () => FirebaseUIAuth.signOut( + context: context, + auth: resolvedAuth, + ), cupertinoIcon: CupertinoIcons.arrow_right_circle, materialIcon: Icons.logout, variant: variant, diff --git a/packages/firebase_ui_auth/lib/src/widgets/user_avatar.dart b/packages/firebase_ui_auth/lib/src/widgets/user_avatar.dart index c7ee9a6c..88dcc457 100644 --- a/packages/firebase_ui_auth/lib/src/widgets/user_avatar.dart +++ b/packages/firebase_ui_auth/lib/src/widgets/user_avatar.dart @@ -3,6 +3,7 @@ // BSD-style license that can be found in the LICENSE file. import 'package:firebase_auth/firebase_auth.dart' as fba; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; import 'package:flutter/material.dart'; /// {@template ui.auth.widgets.user_avatar} @@ -45,7 +46,10 @@ class UserAvatar extends StatefulWidget { } class _UserAvatarState extends State { - fba.FirebaseAuth get auth => widget.auth ?? fba.FirebaseAuth.instance; + fba.FirebaseAuth get auth => + widget.auth ?? + FirebaseAuthProvider.maybeOf(context) ?? + fba.FirebaseAuth.instance; ShapeBorder get shape => widget.shape ?? const CircleBorder(); Color get placeholderColor => widget.placeholderColor ?? Colors.grey; double get size => widget.size ?? 120; diff --git a/packages/firebase_ui_auth/test/flows/auth_state_change_action_test.dart b/packages/firebase_ui_auth/test/flows/auth_state_change_action_test.dart new file mode 100644 index 00000000..14e0770c --- /dev/null +++ b/packages/firebase_ui_auth/test/flows/auth_state_change_action_test.dart @@ -0,0 +1,236 @@ +// Copyright 2024, the Chromium project authors. Please see the AUTHORS file +// 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 'package:firebase_ui_auth/firebase_ui_auth.dart'; +import 'package:firebase_ui_shared/firebase_ui_shared.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; + +import '../test_utils.dart'; + +void main() { + group('AuthStateChangeAction', () { + late MockAuth auth; + + setUpAll(() { + setFirebaseUiIsTestMode(true); + }); + + setUp(() { + auth = MockAuth(); + }); + + testWidgets( + 'is triggered when signing in via EmailForm inside providersBuilder', + (tester) async { + final actionCalled = []; + + await tester.pumpWidget( + TestMaterialApp( + child: SignInScreen( + auth: auth, + providers: [EmailAuthProvider()], + actions: [ + AuthStateChangeAction((context, state) { + actionCalled.add(state); + }), + ], + providersBuilder: (context, providers, action) { + return [ + EmailForm( + auth: auth, + action: action, + provider: providers.first as EmailAuthProvider, + ), + ]; + }, + ), + ), + ); + + // Stub successful sign in + when(auth.signInWithCredential(any)).thenAnswer((_) async { + return MockCredential(); + }); + + // Fill form + await tester.enterText( + find.byType(EmailInput), + 'test@example.com', + ); + await tester.enterText( + find.byType(PasswordInput), + 'password', + ); + await tester.pump(); + + // Tap sign in + await tester.tap(find.byType(LoadingButton)); + await tester.pump(); + + // Expect action to be called + expect(actionCalled, hasLength(1)); + }, + ); + + testWidgets( + 'is triggered when signing up via EmailForm inside providersBuilder', + (tester) async { + final actionCalled = []; + + await tester.pumpWidget( + TestMaterialApp( + child: RegisterScreen( + auth: auth, + providers: [EmailAuthProvider()], + actions: [ + AuthStateChangeAction((context, state) { + actionCalled.add(state); + }), + ], + providersBuilder: (context, providers, action) { + return [ + EmailForm( + auth: auth, + action: action, + provider: providers.first as EmailAuthProvider, + ), + ]; + }, + ), + ), + ); + + // Stub successful sign up + final credential = MockCredential(); + when(credential.user).thenReturn(MockUser()); + + when(auth.createUserWithEmailAndPassword( + email: anyNamed('email'), + password: anyNamed('password'), + )).thenAnswer((_) async { + return credential; + }); + + // Fill form + await tester.enterText( + find.byType(EmailInput), + 'newuser@example.com', + ); + await tester.enterText( + find.byType(PasswordInput).first, + 'password123', + ); + await tester.enterText( + find.byType(PasswordInput).at(1), + 'password123', + ); + await tester.pump(); + + // Tap register + await tester.tap(find.byType(LoadingButton)); + await tester.pumpAndSettle(); + + verify(auth.createUserWithEmailAndPassword( + email: anyNamed('email'), + password: anyNamed('password'), + )).called(1); + + // Expect action to be called + expect(actionCalled, hasLength(1)); + }, + ); + + testWidgets( + 'is triggered when signing in via EmailForm inside providersBuilder without passing auth explicitly', + (tester) async { + final actionCalled = []; + + await tester.pumpWidget( + TestMaterialApp( + child: SignInScreen( + auth: auth, + providers: [EmailAuthProvider()], + actions: [ + AuthStateChangeAction((context, state) { + actionCalled.add(state); + }), + ], + providersBuilder: (context, providers, action) { + return [ + EmailForm( + action: action, + provider: providers.first as EmailAuthProvider, + ), + ]; + }, + ), + ), + ); + + // Stub successful sign in on the shared MockAuth + when(auth.signInWithCredential(any)).thenAnswer((_) async { + return MockCredential(); + }); + + // Fill form + await tester.enterText(find.byType(EmailInput), 'test@example.com'); + await tester.enterText(find.byType(PasswordInput), 'password'); + await tester.pump(); + + // Tap sign in + await tester.tap(find.byType(LoadingButton)); + await tester.pump(); + + // Expect action to be called + expect(actionCalled, hasLength(1)); + }, + ); + + testWidgets( + 'generic AuthStateChangeAction is triggered when custom providersBuilder is used', + (tester) async { + final actionCalled = []; + + await tester.pumpWidget( + TestMaterialApp( + child: SignInScreen( + auth: auth, + providers: [EmailAuthProvider()], + actions: [ + AuthStateChangeAction((context, state) { + actionCalled.add(state); + }), + ], + providersBuilder: (context, providers, action) { + return [ + EmailForm( + auth: auth, + action: action, + provider: providers.first as EmailAuthProvider, + ), + ]; + }, + ), + ), + ); + + when(auth.signInWithCredential(any)).thenAnswer((_) async { + return MockCredential(); + }); + + await tester.enterText(find.byType(EmailInput), 'test@example.com'); + await tester.enterText(find.byType(PasswordInput), 'password'); + await tester.pump(); + await tester.tap(find.byType(LoadingButton)); + await tester.pumpAndSettle(); + + // Should be called at least for SigningIn and SignedIn + expect(actionCalled, isNotEmpty); + expect(actionCalled.any((s) => s is SignedIn), isTrue); + }, + ); + }); +} \ No newline at end of file diff --git a/packages/firebase_ui_auth/test/flows/auth_state_transition_test.dart b/packages/firebase_ui_auth/test/flows/auth_state_transition_test.dart new file mode 100644 index 00000000..88cd174f --- /dev/null +++ b/packages/firebase_ui_auth/test/flows/auth_state_transition_test.dart @@ -0,0 +1,70 @@ +// Copyright 2024, the Chromium project authors. Please see the AUTHORS file +// 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 'package:firebase_ui_auth/firebase_ui_auth.dart'; +import 'package:firebase_ui_shared/firebase_ui_shared.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; + +import '../test_utils.dart'; + +void main() { + group('AuthStateChangeAction Debugging', () { + late MockAuth auth; + + setUpAll(() { + setFirebaseUiIsTestMode(true); + }); + + setUp(() { + auth = MockAuth(); + }); + + testWidgets( + 'notification bubbles up from providersBuilder', + (tester) async { + final notifications = []; + + await tester.pumpWidget( + TestMaterialApp( + child: NotificationListener( + onNotification: (n) { + if (n.runtimeType.toString().contains('AuthStateTransition')) { + notifications.add(n); + } + return false; + }, + child: SignInScreen( + auth: auth, + providers: [EmailAuthProvider()], + providersBuilder: (context, providers, action) { + return [ + EmailForm( + auth: auth, + action: action, + provider: providers.first as EmailAuthProvider, + ), + ]; + }, + ), + ), + ), + ); + + when(auth.signInWithCredential(any)).thenAnswer((_) async { + return MockCredential(); + }); + + await tester.enterText(find.byType(EmailInput), 'test@example.com'); + await tester.enterText(find.byType(PasswordInput), 'password'); + await tester.pump(); + await tester.tap(find.byType(LoadingButton)); + await tester.pumpAndSettle(); + + expect(notifications, isNotEmpty, reason: 'Notification should have bubbled up to the top'); + }, + ); + }); +} diff --git a/packages/firebase_ui_auth/test/screens/register_screen_providers_builder_test.dart b/packages/firebase_ui_auth/test/screens/register_screen_providers_builder_test.dart new file mode 100644 index 00000000..1108284f --- /dev/null +++ b/packages/firebase_ui_auth/test/screens/register_screen_providers_builder_test.dart @@ -0,0 +1,32 @@ +// Copyright 2024, the Chromium project authors. Please see the AUTHORS file +// 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 'package:firebase_ui_auth/firebase_ui_auth.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import '../test_utils.dart'; + +void main() { + group('RegisterScreen providersBuilder', () { + setUpAll(() { + setFirebaseUiIsTestMode(true); + }); + + testWidgets('propagates providersBuilder to LoginView', (tester) async { + await tester.pumpWidget( + TestMaterialApp( + child: RegisterScreen( + auth: MockAuth(), + providers: [EmailAuthProvider()], + providersBuilder: (context, providers, action) { + return [const Text('Custom Register Builder')]; + }, + ), + ), + ); + + expect(find.text('Custom Register Builder'), findsOneWidget); + }); + }); +} diff --git a/packages/firebase_ui_auth/test/screens/sign_in_screen_providers_builder_test.dart b/packages/firebase_ui_auth/test/screens/sign_in_screen_providers_builder_test.dart new file mode 100644 index 00000000..5c8f20cb --- /dev/null +++ b/packages/firebase_ui_auth/test/screens/sign_in_screen_providers_builder_test.dart @@ -0,0 +1,32 @@ +// Copyright 2024, the Chromium project authors. Please see the AUTHORS file +// 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 'package:firebase_ui_auth/firebase_ui_auth.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import '../test_utils.dart'; + +void main() { + group('SignInScreen providersBuilder', () { + setUpAll(() { + setFirebaseUiIsTestMode(true); + }); + + testWidgets('propagates providersBuilder to LoginView', (tester) async { + await tester.pumpWidget( + TestMaterialApp( + child: SignInScreen( + auth: MockAuth(), + providers: [EmailAuthProvider()], + providersBuilder: (context, providers, action) { + return [const Text('Custom SignIn Builder')]; + }, + ), + ), + ); + + expect(find.text('Custom SignIn Builder'), findsOneWidget); + }); + }); +} diff --git a/packages/firebase_ui_auth/test/views/login_view_providers_builder_test.dart b/packages/firebase_ui_auth/test/views/login_view_providers_builder_test.dart new file mode 100644 index 00000000..575c3d99 --- /dev/null +++ b/packages/firebase_ui_auth/test/views/login_view_providers_builder_test.dart @@ -0,0 +1,83 @@ +// Copyright 2024, the Chromium project authors. Please see the AUTHORS file +// 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 'package:firebase_ui_auth/firebase_ui_auth.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import '../test_utils.dart'; + +void main() { + group('LoginView providersBuilder', () { + testWidgets('uses custom providersBuilder when provided', (tester) async { + await tester.pumpWidget( + TestMaterialApp( + child: LoginView( + auth: MockAuth(), + action: AuthAction.signIn, + providers: [ + EmailAuthProvider(), + PhoneAuthProvider(), + ], + providersBuilder: (context, providers, action) { + return [ + const Text('Custom Header'), + for (final provider in providers) + if (provider is EmailAuthProvider) + const Text('Custom Email Form'), + ]; + }, + ), + ), + ); + + expect(find.text('Custom Header'), findsOneWidget); + expect(find.text('Custom Email Form'), findsOneWidget); + // Phone provider should be ignored as per our custom builder logic + expect(find.byType(PhoneVerificationButton), findsNothing); + }); + + testWidgets('uses default explicit order when no builder provided', + (tester) async { + await tester.pumpWidget( + TestMaterialApp( + child: LoginView( + auth: MockAuth(), + action: AuthAction.signIn, + providers: [ + PhoneAuthProvider(), // Phone first in list + EmailAuthProvider(), // Email second + ], + ), + ), + ); + + final emailFinder = find.byType(EmailForm); + final phoneFinder = find.byType(PhoneVerificationButton); + + expect(emailFinder, findsOneWidget); + expect(phoneFinder, findsOneWidget); + + final emailTop = tester.getTopLeft(emailFinder).dy; + final phoneTop = tester.getTopLeft(phoneFinder).dy; + + // Verify explicit default order: Email should be above Phone + expect(emailTop, lessThan(phoneTop)); + }); + + testWidgets('shows confirm password field in signUp mode', (tester) async { + await tester.pumpWidget( + TestMaterialApp( + child: LoginView( + auth: MockAuth(), + action: AuthAction.signUp, + providers: [EmailAuthProvider()], + ), + ), + ); + + // In signUp mode, EmailForm should show confirm password field. + expect(find.text('Confirm password'), findsOneWidget); + }); + }); +} \ No newline at end of file diff --git a/packages/firebase_ui_oauth/lib/src/oauth_provider_button_base.dart b/packages/firebase_ui_oauth/lib/src/oauth_provider_button_base.dart index d6fa7460..302f82c7 100644 --- a/packages/firebase_ui_oauth/lib/src/oauth_provider_button_base.dart +++ b/packages/firebase_ui_oauth/lib/src/oauth_provider_button_base.dart @@ -150,8 +150,13 @@ class _OAuthProviderButtonBaseState extends State void initState() { super.initState(); - provider.auth = widget.auth ?? fba.FirebaseAuth.instance; - provider.authListener = this; + provider.auth = widget.auth ?? + FirebaseAuthProvider.findAuth(context) ?? + fba.FirebaseAuth.instance; + + if (!widget.overrideDefaultTapAction) { + provider.authListener = this; + } } void _signIn() { diff --git a/packages/firebase_ui_oauth/test/flutterfire_ui_oauth_test.dart b/packages/firebase_ui_oauth/test/flutterfire_ui_oauth_test.dart index 55ec8d78..5d460043 100644 --- a/packages/firebase_ui_oauth/test/flutterfire_ui_oauth_test.dart +++ b/packages/firebase_ui_oauth/test/flutterfire_ui_oauth_test.dart @@ -224,5 +224,73 @@ void main() { expect(find.byType(LayoutFlowAwarePadding), findsOneWidget); }); + + testWidgets('triggers AuthStateChangeAction via AuthFlowBuilder', (tester) async { + final actionCalled = []; + final provider = TestOAuthProvider(); + final auth = MockAuth(); + provider.auth = auth; + + await tester.pumpWidget( + DefaultAssetBundle( + bundle: FakeAssetBundle(), + child: MaterialApp( + home: Scaffold( + body: FirebaseUIActions( + actions: [ + AuthStateChangeAction((context, state) { + actionCalled.add(state); + }), + ], + child: Center( + child: AuthFlowBuilder( + provider: provider, + auth: auth, + builder: (context, state, ctrl, child) { + return OAuthProviderButtonBase( + provider: provider, + auth: auth, + label: 'Sign in with Test provider', + loadingIndicator: const CircularProgressIndicator(), + onTap: () => ctrl.signIn(Theme.of(context).platform), + overrideDefaultTapAction: true, // Simulating the fix + ); + }, + ), + ), + ), + ), + ), + ), + ); + + await tester.tap(find.text('Sign in with Test provider'), warnIfMissed: false); + await tester.pump(); + + expect(actionCalled, hasLength(1)); + }); }); } + +class TestOAuthProvider extends FakeOAuthProvider { + @override + void mobileSignIn(AuthAction action) { + authListener.onBeforeSignIn(); + authListener.onSignedIn(MockUserCredential()); + } +} + +class MockAuth extends Fake implements fba.FirebaseAuth { + @override + fba.User? get currentUser => null; +} + +class MockUserCredential extends Fake implements fba.UserCredential { + @override + fba.User? get user => MockUser(); + @override + fba.AdditionalUserInfo? get additionalUserInfo => null; +} + +class MockUser extends Fake implements fba.User {} + diff --git a/packages/firebase_ui_oauth_apple/lib/firebase_ui_oauth_apple.dart b/packages/firebase_ui_oauth_apple/lib/firebase_ui_oauth_apple.dart index bb2efaf5..52d1832a 100644 --- a/packages/firebase_ui_oauth_apple/lib/firebase_ui_oauth_apple.dart +++ b/packages/firebase_ui_oauth_apple/lib/firebase_ui_oauth_apple.dart @@ -6,7 +6,8 @@ export 'src/provider.dart' show AppleProvider; export 'src/theme.dart' show AppleProviderButtonStyle; import 'package:firebase_auth/firebase_auth.dart' as fba; -import 'package:flutter/widgets.dart'; +import 'package:flutter/material.dart'; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; import 'package:firebase_ui_oauth/firebase_ui_oauth.dart'; import 'src/provider.dart'; @@ -99,20 +100,27 @@ class _AppleSignInButton extends StatelessWidget { @override Widget build(BuildContext context) { - return OAuthProviderButtonBase( + return AuthFlowBuilder( provider: provider, - label: label, - onTap: onTap, - loadingIndicator: loadingIndicator, - isLoading: isLoading, action: action, - auth: auth ?? fba.FirebaseAuth.instance, - onDifferentProvidersFound: onDifferentProvidersFound, - onSignedIn: onSignedIn, - overrideDefaultTapAction: overrideDefaultTapAction, - size: size, - onError: onError, - onCancelled: onCanceled, + auth: auth, + builder: (context, state, ctrl, child) { + return OAuthProviderButtonBase( + provider: provider, + label: label, + onTap: () => ctrl.signIn(Theme.of(context).platform), + loadingIndicator: loadingIndicator, + isLoading: state is SigningIn || state is CredentialReceived, + action: action, + auth: auth ?? fba.FirebaseAuth.instance, + onDifferentProvidersFound: onDifferentProvidersFound, + onSignedIn: onSignedIn, + overrideDefaultTapAction: true, + size: size, + onError: onError, + onCancelled: onCanceled, + ); + }, ); } } diff --git a/packages/firebase_ui_oauth_apple/pubspec.yaml b/packages/firebase_ui_oauth_apple/pubspec.yaml index 11e75300..3a34b34f 100644 --- a/packages/firebase_ui_oauth_apple/pubspec.yaml +++ b/packages/firebase_ui_oauth_apple/pubspec.yaml @@ -10,6 +10,7 @@ environment: dependencies: firebase_auth: ^6.1.3 + firebase_ui_auth: ^3.0.1 firebase_ui_oauth: ^2.0.1 flutter: sdk: flutter diff --git a/packages/firebase_ui_oauth_facebook/lib/firebase_ui_oauth_facebook.dart b/packages/firebase_ui_oauth_facebook/lib/firebase_ui_oauth_facebook.dart index 71f77b63..a59102ae 100644 --- a/packages/firebase_ui_oauth_facebook/lib/firebase_ui_oauth_facebook.dart +++ b/packages/firebase_ui_oauth_facebook/lib/firebase_ui_oauth_facebook.dart @@ -7,7 +7,7 @@ export 'src/theme.dart' show FacebookProviderButtonStyle; import 'package:firebase_auth/firebase_auth.dart' as fba; import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; import 'package:firebase_ui_oauth/firebase_ui_oauth.dart'; import 'src/provider.dart'; @@ -106,20 +106,27 @@ class _FacebookSignInButton extends StatelessWidget { @override Widget build(BuildContext context) { - return OAuthProviderButtonBase( + return AuthFlowBuilder( provider: provider, - label: label, - onTap: onTap, - loadingIndicator: loadingIndicator, - isLoading: isLoading, action: action, - auth: auth ?? fba.FirebaseAuth.instance, - onDifferentProvidersFound: onDifferentProvidersFound, - onSignedIn: onSignedIn, - overrideDefaultTapAction: overrideDefaultTapAction, - size: size, - onError: onError, - onCancelled: onCanceled, + auth: auth, + builder: (context, state, ctrl, child) { + return OAuthProviderButtonBase( + provider: provider, + label: label, + onTap: () => ctrl.signIn(Theme.of(context).platform), + loadingIndicator: loadingIndicator, + isLoading: state is SigningIn || state is CredentialReceived, + action: action, + auth: auth ?? fba.FirebaseAuth.instance, + onDifferentProvidersFound: onDifferentProvidersFound, + onSignedIn: onSignedIn, + overrideDefaultTapAction: true, + size: size, + onError: onError, + onCancelled: onCanceled, + ); + }, ); } } diff --git a/packages/firebase_ui_oauth_facebook/pubspec.yaml b/packages/firebase_ui_oauth_facebook/pubspec.yaml index 3b579a31..c3326973 100644 --- a/packages/firebase_ui_oauth_facebook/pubspec.yaml +++ b/packages/firebase_ui_oauth_facebook/pubspec.yaml @@ -10,6 +10,7 @@ environment: dependencies: firebase_auth: ^6.1.3 + firebase_ui_auth: ^3.0.1 firebase_ui_oauth: ^2.0.1 flutter: sdk: flutter diff --git a/packages/firebase_ui_oauth_google/lib/firebase_ui_oauth_google.dart b/packages/firebase_ui_oauth_google/lib/firebase_ui_oauth_google.dart index 5d1b3a16..24466248 100644 --- a/packages/firebase_ui_oauth_google/lib/firebase_ui_oauth_google.dart +++ b/packages/firebase_ui_oauth_google/lib/firebase_ui_oauth_google.dart @@ -6,7 +6,8 @@ export 'src/provider.dart' show GoogleProvider; export 'src/theme.dart' show GoogleProviderButtonStyle; import 'package:firebase_auth/firebase_auth.dart' as fba; -import 'package:flutter/widgets.dart'; +import 'package:flutter/material.dart'; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; import 'package:firebase_ui_oauth/firebase_ui_oauth.dart'; import 'src/provider.dart'; @@ -113,20 +114,27 @@ class _GoogleSignInButton extends StatelessWidget { @override Widget build(BuildContext context) { - return OAuthProviderButtonBase( + return AuthFlowBuilder( provider: provider, - label: label, - onTap: onTap, - loadingIndicator: loadingIndicator, - isLoading: isLoading, action: action, - auth: auth ?? fba.FirebaseAuth.instance, - onDifferentProvidersFound: onDifferentProvidersFound, - onSignedIn: onSignedIn, - overrideDefaultTapAction: overrideDefaultTapAction, - size: size, - onError: onError, - onCancelled: onCanceled, + auth: auth, + builder: (context, state, ctrl, child) { + return OAuthProviderButtonBase( + provider: provider, + label: label, + onTap: () => ctrl.signIn(Theme.of(context).platform), + loadingIndicator: loadingIndicator, + isLoading: state is SigningIn || state is CredentialReceived, + action: action, + auth: auth ?? fba.FirebaseAuth.instance, + onDifferentProvidersFound: onDifferentProvidersFound, + onSignedIn: onSignedIn, + overrideDefaultTapAction: true, + size: size, + onError: onError, + onCancelled: onCanceled, + ); + }, ); } } diff --git a/packages/firebase_ui_oauth_google/pubspec.yaml b/packages/firebase_ui_oauth_google/pubspec.yaml index 990b6d7b..bc2f3354 100644 --- a/packages/firebase_ui_oauth_google/pubspec.yaml +++ b/packages/firebase_ui_oauth_google/pubspec.yaml @@ -10,6 +10,7 @@ environment: dependencies: firebase_auth: ^6.1.3 + firebase_ui_auth: ^3.0.1 firebase_ui_oauth: ^2.0.1 flutter: sdk: flutter diff --git a/packages/firebase_ui_oauth_twitter/lib/firebase_ui_oauth_twitter.dart b/packages/firebase_ui_oauth_twitter/lib/firebase_ui_oauth_twitter.dart index 3b9bbb5a..1ee185e5 100644 --- a/packages/firebase_ui_oauth_twitter/lib/firebase_ui_oauth_twitter.dart +++ b/packages/firebase_ui_oauth_twitter/lib/firebase_ui_oauth_twitter.dart @@ -6,7 +6,8 @@ export 'src/provider.dart' show TwitterProvider; export 'src/theme.dart' show TwitterProviderButtonStyle; import 'package:firebase_auth/firebase_auth.dart' as fba; -import 'package:flutter/widgets.dart'; +import 'package:flutter/material.dart'; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; import 'package:firebase_ui_oauth/firebase_ui_oauth.dart'; import 'src/provider.dart'; @@ -113,20 +114,27 @@ class _TwitterSignInButton extends StatelessWidget { @override Widget build(BuildContext context) { - return OAuthProviderButtonBase( + return AuthFlowBuilder( provider: provider, - label: label, - onTap: onTap, - loadingIndicator: loadingIndicator, - isLoading: isLoading, action: action, - auth: auth ?? fba.FirebaseAuth.instance, - onDifferentProvidersFound: onDifferentProvidersFound, - onSignedIn: onSignedIn, - overrideDefaultTapAction: overrideDefaultTapAction, - size: size, - onError: onError, - onCancelled: onCanceled, + auth: auth, + builder: (context, state, ctrl, child) { + return OAuthProviderButtonBase( + provider: provider, + label: label, + onTap: () => ctrl.signIn(Theme.of(context).platform), + loadingIndicator: loadingIndicator, + isLoading: state is SigningIn || state is CredentialReceived, + action: action, + auth: auth ?? fba.FirebaseAuth.instance, + onDifferentProvidersFound: onDifferentProvidersFound, + onSignedIn: onSignedIn, + overrideDefaultTapAction: true, + size: size, + onError: onError, + onCancelled: onCanceled, + ); + }, ); } } diff --git a/packages/firebase_ui_oauth_twitter/pubspec.yaml b/packages/firebase_ui_oauth_twitter/pubspec.yaml index b57fbf5d..4f9e1de9 100644 --- a/packages/firebase_ui_oauth_twitter/pubspec.yaml +++ b/packages/firebase_ui_oauth_twitter/pubspec.yaml @@ -12,6 +12,7 @@ dependencies: flutter: sdk: flutter firebase_auth: ^6.1.3 + firebase_ui_auth: ^3.0.1 firebase_ui_oauth: ^2.0.1 twitter_login: ^4.4.2 From dea12ce74345677c178211bc552f3bbab4ec3e36 Mon Sep 17 00:00:00 2001 From: Fabio Carneiro Date: Wed, 6 May 2026 14:41:55 +0200 Subject: [PATCH 2/3] fix: cache GoogleProvider in state to prevent double initWithParams on web MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On web, GoogleSignIn calls initWithParams eagerly in its constructor. The provider getter previously created a new GoogleProvider (and thus a new GoogleSignIn) on every access, and build() called it twice — once for AuthFlowBuilder and once for OAuthProviderButtonBase. Both called initWithParams on the singleton GoogleSignInPlugin, triggering a 'Future already completed' race condition. Fix: convert _GoogleSignInButton to StatefulWidget and create the GoogleProvider once in initState. --- .../lib/firebase_ui_oauth_google.dart | 50 +++++++++++-------- 1 file changed, 29 insertions(+), 21 deletions(-) diff --git a/packages/firebase_ui_oauth_google/lib/firebase_ui_oauth_google.dart b/packages/firebase_ui_oauth_google/lib/firebase_ui_oauth_google.dart index 24466248..59636bec 100644 --- a/packages/firebase_ui_oauth_google/lib/firebase_ui_oauth_google.dart +++ b/packages/firebase_ui_oauth_google/lib/firebase_ui_oauth_google.dart @@ -60,7 +60,7 @@ void setMockGoogleProvider(GoogleProvider provider) { _mockProvider = provider; } -class _GoogleSignInButton extends StatelessWidget { +class _GoogleSignInButton extends StatefulWidget { final String label; final Widget loadingIndicator; final void Function()? onTap; @@ -102,37 +102,45 @@ class _GoogleSignInButton extends StatelessWidget { overrideDefaultTapAction = overrideDefaultTapAction ?? false, size = size ?? 19; - GoogleProvider get provider { - if (_mockProvider != null) return _mockProvider!; + @override + State<_GoogleSignInButton> createState() => _GoogleSignInButtonState(); +} - return GoogleProvider( - clientId: clientId, - redirectUri: redirectUri, - scopes: scopes ?? [], - ); +class _GoogleSignInButtonState extends State<_GoogleSignInButton> { + late final GoogleProvider _provider; + + @override + void initState() { + super.initState(); + _provider = _mockProvider ?? + GoogleProvider( + clientId: widget.clientId, + redirectUri: widget.redirectUri, + scopes: widget.scopes ?? [], + ); } @override Widget build(BuildContext context) { return AuthFlowBuilder( - provider: provider, - action: action, - auth: auth, + provider: _provider, + action: widget.action, + auth: widget.auth, builder: (context, state, ctrl, child) { return OAuthProviderButtonBase( - provider: provider, - label: label, + provider: _provider, + label: widget.label, onTap: () => ctrl.signIn(Theme.of(context).platform), - loadingIndicator: loadingIndicator, + loadingIndicator: widget.loadingIndicator, isLoading: state is SigningIn || state is CredentialReceived, - action: action, - auth: auth ?? fba.FirebaseAuth.instance, - onDifferentProvidersFound: onDifferentProvidersFound, - onSignedIn: onSignedIn, + action: widget.action, + auth: widget.auth ?? fba.FirebaseAuth.instance, + onDifferentProvidersFound: widget.onDifferentProvidersFound, + onSignedIn: widget.onSignedIn, overrideDefaultTapAction: true, - size: size, - onError: onError, - onCancelled: onCanceled, + size: widget.size, + onError: widget.onError, + onCancelled: widget.onCanceled, ); }, ); From a64cd0e7f8551efc4ce017f733279603f7dbb3e5 Mon Sep 17 00:00:00 2001 From: Fabio Carneiro Date: Wed, 6 May 2026 15:02:19 +0200 Subject: [PATCH 3/3] fix: address gemini-code-assist review comments on PR #640 --- .../lib/src/auth_controller.dart | 8 ++-- .../lib/src/views/login_view.dart | 10 ++++ .../lib/firebase_ui_oauth_apple.dart | 30 +++++------- .../lib/firebase_ui_oauth_facebook.dart | 30 +++++------- .../lib/firebase_ui_oauth_google.dart | 46 +++++++++++-------- .../lib/firebase_ui_oauth_twitter.dart | 30 +++++------- 6 files changed, 73 insertions(+), 81 deletions(-) diff --git a/packages/firebase_ui_auth/lib/src/auth_controller.dart b/packages/firebase_ui_auth/lib/src/auth_controller.dart index d45f48af..74948167 100644 --- a/packages/firebase_ui_auth/lib/src/auth_controller.dart +++ b/packages/firebase_ui_auth/lib/src/auth_controller.dart @@ -119,14 +119,14 @@ class FirebaseAuthProvider extends InheritedWidget { } /// Looks up an instance of [fba.FirebaseAuth] in the widget tree. - /// Throws an [Exception] if no [FirebaseAuthProvider] was found. + /// Throws a [FlutterError] if no [FirebaseAuthProvider] was found. static fba.FirebaseAuth of(BuildContext context) { final auth = maybeOf(context); if (auth == null) { - throw Exception( - 'No FirebaseAuthProvider found. ' - 'Make sure to wrap your code with FirebaseAuthProvider', + throw FlutterError( + 'No FirebaseAuthProvider found in the widget tree. ' + 'Make sure to wrap your widget tree with a FirebaseAuthProvider.', ); } diff --git a/packages/firebase_ui_auth/lib/src/views/login_view.dart b/packages/firebase_ui_auth/lib/src/views/login_view.dart index a0d379bb..c79703ff 100644 --- a/packages/firebase_ui_auth/lib/src/views/login_view.dart +++ b/packages/firebase_ui_auth/lib/src/views/login_view.dart @@ -16,6 +16,16 @@ import '../widgets/internal/title.dart'; typedef AuthViewContentBuilder = Widget Function(BuildContext context, AuthAction action); +/// A builder that allows full customization of the layout and ordering of +/// authentication provider widgets. +/// +/// The [providers] parameter contains [AuthProvider] configuration objects, +/// not widgets. You must build the appropriate widget for each provider +/// manually (e.g., [EmailForm] for [EmailAuthProvider], [OAuthProviderButton] +/// for OAuth providers). +/// +/// The [action] parameter reflects the current [AuthAction] (sign in or +/// sign up) so the builder can adapt its output accordingly. typedef ProvidersBuilder = List Function( BuildContext context, List providers, diff --git a/packages/firebase_ui_oauth_apple/lib/firebase_ui_oauth_apple.dart b/packages/firebase_ui_oauth_apple/lib/firebase_ui_oauth_apple.dart index 52d1832a..d27bfc7b 100644 --- a/packages/firebase_ui_oauth_apple/lib/firebase_ui_oauth_apple.dart +++ b/packages/firebase_ui_oauth_apple/lib/firebase_ui_oauth_apple.dart @@ -7,7 +7,6 @@ export 'src/theme.dart' show AppleProviderButtonStyle; import 'package:firebase_auth/firebase_auth.dart' as fba; import 'package:flutter/material.dart'; -import 'package:firebase_ui_auth/firebase_ui_auth.dart'; import 'package:firebase_ui_oauth/firebase_ui_oauth.dart'; import 'src/provider.dart'; @@ -100,27 +99,20 @@ class _AppleSignInButton extends StatelessWidget { @override Widget build(BuildContext context) { - return AuthFlowBuilder( + return OAuthProviderButtonBase( provider: provider, + label: label, + onTap: onTap, + loadingIndicator: loadingIndicator, + isLoading: isLoading, action: action, auth: auth, - builder: (context, state, ctrl, child) { - return OAuthProviderButtonBase( - provider: provider, - label: label, - onTap: () => ctrl.signIn(Theme.of(context).platform), - loadingIndicator: loadingIndicator, - isLoading: state is SigningIn || state is CredentialReceived, - action: action, - auth: auth ?? fba.FirebaseAuth.instance, - onDifferentProvidersFound: onDifferentProvidersFound, - onSignedIn: onSignedIn, - overrideDefaultTapAction: true, - size: size, - onError: onError, - onCancelled: onCanceled, - ); - }, + onDifferentProvidersFound: onDifferentProvidersFound, + onSignedIn: onSignedIn, + overrideDefaultTapAction: overrideDefaultTapAction, + size: size, + onError: onError, + onCancelled: onCanceled, ); } } diff --git a/packages/firebase_ui_oauth_facebook/lib/firebase_ui_oauth_facebook.dart b/packages/firebase_ui_oauth_facebook/lib/firebase_ui_oauth_facebook.dart index a59102ae..bcfb0c44 100644 --- a/packages/firebase_ui_oauth_facebook/lib/firebase_ui_oauth_facebook.dart +++ b/packages/firebase_ui_oauth_facebook/lib/firebase_ui_oauth_facebook.dart @@ -7,7 +7,6 @@ export 'src/theme.dart' show FacebookProviderButtonStyle; import 'package:firebase_auth/firebase_auth.dart' as fba; import 'package:flutter/material.dart'; -import 'package:firebase_ui_auth/firebase_ui_auth.dart'; import 'package:firebase_ui_oauth/firebase_ui_oauth.dart'; import 'src/provider.dart'; @@ -106,27 +105,20 @@ class _FacebookSignInButton extends StatelessWidget { @override Widget build(BuildContext context) { - return AuthFlowBuilder( + return OAuthProviderButtonBase( provider: provider, + label: label, + onTap: onTap, + loadingIndicator: loadingIndicator, + isLoading: isLoading, action: action, auth: auth, - builder: (context, state, ctrl, child) { - return OAuthProviderButtonBase( - provider: provider, - label: label, - onTap: () => ctrl.signIn(Theme.of(context).platform), - loadingIndicator: loadingIndicator, - isLoading: state is SigningIn || state is CredentialReceived, - action: action, - auth: auth ?? fba.FirebaseAuth.instance, - onDifferentProvidersFound: onDifferentProvidersFound, - onSignedIn: onSignedIn, - overrideDefaultTapAction: true, - size: size, - onError: onError, - onCancelled: onCanceled, - ); - }, + onDifferentProvidersFound: onDifferentProvidersFound, + onSignedIn: onSignedIn, + overrideDefaultTapAction: overrideDefaultTapAction, + size: size, + onError: onError, + onCancelled: onCanceled, ); } } diff --git a/packages/firebase_ui_oauth_google/lib/firebase_ui_oauth_google.dart b/packages/firebase_ui_oauth_google/lib/firebase_ui_oauth_google.dart index 59636bec..92594907 100644 --- a/packages/firebase_ui_oauth_google/lib/firebase_ui_oauth_google.dart +++ b/packages/firebase_ui_oauth_google/lib/firebase_ui_oauth_google.dart @@ -7,7 +7,6 @@ export 'src/theme.dart' show GoogleProviderButtonStyle; import 'package:firebase_auth/firebase_auth.dart' as fba; import 'package:flutter/material.dart'; -import 'package:firebase_ui_auth/firebase_ui_auth.dart'; import 'package:firebase_ui_oauth/firebase_ui_oauth.dart'; import 'src/provider.dart'; @@ -107,11 +106,25 @@ class _GoogleSignInButton extends StatefulWidget { } class _GoogleSignInButtonState extends State<_GoogleSignInButton> { - late final GoogleProvider _provider; + late GoogleProvider _provider; @override void initState() { super.initState(); + _initProvider(); + } + + @override + void didUpdateWidget(_GoogleSignInButton oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.clientId != widget.clientId || + oldWidget.redirectUri != widget.redirectUri || + oldWidget.scopes != widget.scopes) { + _initProvider(); + } + } + + void _initProvider() { _provider = _mockProvider ?? GoogleProvider( clientId: widget.clientId, @@ -122,27 +135,20 @@ class _GoogleSignInButtonState extends State<_GoogleSignInButton> { @override Widget build(BuildContext context) { - return AuthFlowBuilder( + return OAuthProviderButtonBase( provider: _provider, + label: widget.label, + onTap: widget.onTap, + loadingIndicator: widget.loadingIndicator, + isLoading: widget.isLoading, action: widget.action, auth: widget.auth, - builder: (context, state, ctrl, child) { - return OAuthProviderButtonBase( - provider: _provider, - label: widget.label, - onTap: () => ctrl.signIn(Theme.of(context).platform), - loadingIndicator: widget.loadingIndicator, - isLoading: state is SigningIn || state is CredentialReceived, - action: widget.action, - auth: widget.auth ?? fba.FirebaseAuth.instance, - onDifferentProvidersFound: widget.onDifferentProvidersFound, - onSignedIn: widget.onSignedIn, - overrideDefaultTapAction: true, - size: widget.size, - onError: widget.onError, - onCancelled: widget.onCanceled, - ); - }, + onDifferentProvidersFound: widget.onDifferentProvidersFound, + onSignedIn: widget.onSignedIn, + overrideDefaultTapAction: widget.overrideDefaultTapAction, + size: widget.size, + onError: widget.onError, + onCancelled: widget.onCanceled, ); } } diff --git a/packages/firebase_ui_oauth_twitter/lib/firebase_ui_oauth_twitter.dart b/packages/firebase_ui_oauth_twitter/lib/firebase_ui_oauth_twitter.dart index 1ee185e5..52b29259 100644 --- a/packages/firebase_ui_oauth_twitter/lib/firebase_ui_oauth_twitter.dart +++ b/packages/firebase_ui_oauth_twitter/lib/firebase_ui_oauth_twitter.dart @@ -7,7 +7,6 @@ export 'src/theme.dart' show TwitterProviderButtonStyle; import 'package:firebase_auth/firebase_auth.dart' as fba; import 'package:flutter/material.dart'; -import 'package:firebase_ui_auth/firebase_ui_auth.dart'; import 'package:firebase_ui_oauth/firebase_ui_oauth.dart'; import 'src/provider.dart'; @@ -114,27 +113,20 @@ class _TwitterSignInButton extends StatelessWidget { @override Widget build(BuildContext context) { - return AuthFlowBuilder( + return OAuthProviderButtonBase( provider: provider, + label: label, + onTap: onTap, + loadingIndicator: loadingIndicator, + isLoading: isLoading, action: action, auth: auth, - builder: (context, state, ctrl, child) { - return OAuthProviderButtonBase( - provider: provider, - label: label, - onTap: () => ctrl.signIn(Theme.of(context).platform), - loadingIndicator: loadingIndicator, - isLoading: state is SigningIn || state is CredentialReceived, - action: action, - auth: auth ?? fba.FirebaseAuth.instance, - onDifferentProvidersFound: onDifferentProvidersFound, - onSignedIn: onSignedIn, - overrideDefaultTapAction: true, - size: size, - onError: onError, - onCancelled: onCanceled, - ); - }, + onDifferentProvidersFound: onDifferentProvidersFound, + onSignedIn: onSignedIn, + overrideDefaultTapAction: overrideDefaultTapAction, + size: size, + onError: onError, + onCancelled: onCanceled, ); } }