From 69a229d8730671b5fff7db246c0950ee55473d9f Mon Sep 17 00:00:00 2001 From: Mauro Vanetti Date: Thu, 19 Jun 2025 11:27:43 +0200 Subject: [PATCH 1/8] feat: Add prefilled email, password to SupaEmailAuth --- example/lib/main.dart | 8 +-- example/lib/sign_in_prefilled.dart | 68 +++++++++++++++++++++++++ lib/src/components/supa_email_auth.dart | 10 ++++ pubspec.yaml | 6 +-- 4 files changed, 86 insertions(+), 6 deletions(-) create mode 100644 example/lib/sign_in_prefilled.dart diff --git a/example/lib/main.dart b/example/lib/main.dart index ad28ebd..922c5ac 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -3,10 +3,11 @@ import 'package:flutter/material.dart'; import 'package:supabase_auth_ui/supabase_auth_ui.dart'; import './home.dart'; -import './sign_in.dart'; import './magic_link.dart'; +import './phone_sign_in.dart'; +import './sign_in.dart'; +import './sign_in_prefilled.dart'; import './update_password.dart'; -import 'phone_sign_in.dart'; import './verify_phone.dart'; void main() async { @@ -34,7 +35,7 @@ class MyApp extends StatelessWidget { border: OutlineInputBorder(), ), ), - initialRoute: '/', + initialRoute: '/prefilled', routes: { '/': (context) => const SignUp(), '/magic_link': (context) => const MagicLink(), @@ -42,6 +43,7 @@ class MyApp extends StatelessWidget { '/phone_sign_in': (context) => const PhoneSignIn(), '/phone_sign_up': (context) => const PhoneSignUp(), '/verify_phone': (context) => const VerifyPhone(), + '/prefilled': (context) => const SignInPrefilled(), '/home': (context) => const Home(), }, onUnknownRoute: (RouteSettings settings) { diff --git a/example/lib/sign_in_prefilled.dart b/example/lib/sign_in_prefilled.dart new file mode 100644 index 0000000..68f6238 --- /dev/null +++ b/example/lib/sign_in_prefilled.dart @@ -0,0 +1,68 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:supabase_auth_ui/supabase_auth_ui.dart'; + +import 'constants.dart'; + +class SignInPrefilled extends StatelessWidget { + const SignInPrefilled({Key? key}) : super(key: key); + @override + Widget build(BuildContext context) { + void navigateHome(AuthResponse response) { + Navigator.of(context).pushReplacementNamed('/home'); + } + + return Scaffold( + appBar: appBar('Sign In (Prefilled)'), + body: ListView( + padding: const EdgeInsets.all(24.0), + children: [ + SupaEmailAuth( + prefilledEmail: "mail@example.com", + prefilledPassword: "password", + redirectTo: kIsWeb ? null : 'io.supabase.flutter://', + onSignInComplete: navigateHome, + onSignUpComplete: navigateHome, + metadataFields: [ + MetaDataField( + prefixIcon: const Icon(Icons.person), + label: 'Username', + key: 'username', + validator: (val) { + if (val == null || val.isEmpty) { + return 'Please enter something'; + } + return null; + }, + ), + BooleanMetaDataField( + label: 'Keep me up to date with the latest news and updates.', + key: 'marketing_consent', + checkboxPosition: ListTileControlAffinity.leading, + ), + BooleanMetaDataField( + key: 'terms_agreement', + isRequired: true, + checkboxPosition: ListTileControlAffinity.leading, + richLabelSpans: [ + const TextSpan(text: 'I have read and agree to the '), + TextSpan( + text: 'Terms and Conditions', + style: const TextStyle( + color: Colors.blue, + ), + recognizer: TapGestureRecognizer() + ..onTap = () { + // Handle tap on Terms and Conditions + }, + ), + ], + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/src/components/supa_email_auth.dart b/lib/src/components/supa_email_auth.dart index 0fd2396..50aa70b 100644 --- a/lib/src/components/supa_email_auth.dart +++ b/lib/src/components/supa_email_auth.dart @@ -220,6 +220,12 @@ class SupaEmailAuth extends StatefulWidget { /// Whether the confirm password field should be displayed final bool showConfirmPasswordField; + /// Pre-filled email for the form + final String? prefilledEmail; + + /// Pre-filled password for the form + final String? prefilledPassword; + /// {@macro supa_email_auth} const SupaEmailAuth({ super.key, @@ -240,6 +246,8 @@ class SupaEmailAuth extends StatefulWidget { this.prefixIconEmail = const Icon(Icons.email), this.prefixIconPassword = const Icon(Icons.lock), this.showConfirmPasswordField = false, + this.prefilledEmail, + this.prefilledPassword, }); @override @@ -265,6 +273,8 @@ class _SupaEmailAuthState extends State { @override void initState() { super.initState(); + _emailController.text = widget.prefilledEmail ?? ''; + _passwordController.text = widget.prefilledPassword ?? ''; _isSigningIn = widget.isInitiallySigningIn; _metadataControllers = Map.fromEntries((widget.metadataFields ?? []).map( (metadataField) => MapEntry( diff --git a/pubspec.yaml b/pubspec.yaml index 880ca10..f21c691 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: supabase_auth_ui description: UI library to implement auth forms using Supabase and Flutter -version: 0.5.5 +version: 0.5.6 homepage: https://supabase.com repository: 'https://github.com/supabase-community/flutter-auth-ui' @@ -12,10 +12,10 @@ dependencies: flutter: sdk: flutter supabase_flutter: ^2.5.6 - email_validator: ^2.0.1 + email_validator: ^3.0.0 font_awesome_flutter: ^10.6.0 google_sign_in: ^6.2.1 - sign_in_with_apple: ^6.1.0 + sign_in_with_apple: ^7.0.1 crypto: ^3.0.3 dev_dependencies: From f28878ed4c8da42bef7a1d70e79b7e5368c18b99 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 14 Oct 2025 13:32:40 +0000 Subject: [PATCH 2/8] Initial plan From 0c36bc592d58197be768331e387108c1b4238257 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 14 Oct 2025 13:39:33 +0000 Subject: [PATCH 3/8] Add enableAutomaticFormSubmission flag to all auth components Co-authored-by: maurovanetti <402070+maurovanetti@users.noreply.github.com> --- lib/src/components/supa_email_auth.dart | 18 +++++- lib/src/components/supa_magic_auth.dart | 45 +++++++++++++++ lib/src/components/supa_phone_auth.dart | 62 +++++++++++++++++++++ lib/src/components/supa_reset_password.dart | 41 ++++++++++++++ 4 files changed, 163 insertions(+), 3 deletions(-) diff --git a/lib/src/components/supa_email_auth.dart b/lib/src/components/supa_email_auth.dart index 50aa70b..446ab4d 100644 --- a/lib/src/components/supa_email_auth.dart +++ b/lib/src/components/supa_email_auth.dart @@ -226,6 +226,15 @@ class SupaEmailAuth extends StatefulWidget { /// Pre-filled password for the form final String? prefilledPassword; + /// Whether pressing Enter on the on-screen keyboard should automatically + /// submit the form. + /// + /// When set to `false`, the user must explicitly click the submit button + /// to proceed with the authentication process. + /// + /// Defaults to `true` for backward compatibility. + final bool enableAutomaticFormSubmission; + /// {@macro supa_email_auth} const SupaEmailAuth({ super.key, @@ -248,6 +257,7 @@ class SupaEmailAuth extends StatefulWidget { this.showConfirmPasswordField = false, this.prefilledEmail, this.prefilledPassword, + this.enableAutomaticFormSubmission = true, }); @override @@ -331,7 +341,8 @@ class _SupaEmailAuthState extends State { ), controller: _emailController, onFieldSubmitted: (_) { - if (_isRecoveringPassword) { + if (_isRecoveringPassword && + widget.enableAutomaticFormSubmission) { _passwordRecovery(); } }, @@ -360,7 +371,8 @@ class _SupaEmailAuthState extends State { obscureText: true, controller: _passwordController, onFieldSubmitted: (_) { - if (widget.metadataFields == null || _isSigningIn) { + if ((widget.metadataFields == null || _isSigningIn) && + widget.enableAutomaticFormSubmission) { _signInSignUp(); } }, @@ -463,7 +475,7 @@ class _SupaEmailAuthState extends State { if (metadataField != widget.metadataFields!.last) { FocusScope.of(context).nextFocus(); - } else { + } else if (widget.enableAutomaticFormSubmission) { _signInSignUp(); } }, diff --git a/lib/src/components/supa_magic_auth.dart b/lib/src/components/supa_magic_auth.dart index 71f366d..5ad6812 100644 --- a/lib/src/components/supa_magic_auth.dart +++ b/lib/src/components/supa_magic_auth.dart @@ -22,12 +22,22 @@ class SupaMagicAuth extends StatefulWidget { /// Localization for the form final SupaMagicAuthLocalization localization; + /// Whether pressing Enter on the on-screen keyboard should automatically + /// submit the form. + /// + /// When set to `false`, the user must explicitly click the submit button + /// to proceed with the authentication process. + /// + /// Defaults to `true` for backward compatibility. + final bool enableAutomaticFormSubmission; + const SupaMagicAuth({ super.key, this.redirectUrl, required this.onSuccess, this.onError, this.localization = const SupaMagicAuthLocalization(), + this.enableAutomaticFormSubmission = true, }); @override @@ -84,6 +94,41 @@ class _SupaMagicAuthState extends State { label: Text(localization.enterEmail), ), controller: _email, + onFieldSubmitted: (_) async { + if (widget.enableAutomaticFormSubmission) { + if (!_formKey.currentState!.validate()) { + return; + } + setState(() { + _isLoading = true; + }); + try { + await supabase.auth.signInWithOtp( + email: _email.text, + emailRedirectTo: widget.redirectUrl, + ); + if (context.mounted) { + context.showSnackBar(localization.checkYourEmail); + } + } on AuthException catch (error) { + if (widget.onError == null && context.mounted) { + context.showErrorSnackBar(error.message); + } else { + widget.onError?.call(error); + } + } catch (error) { + if (widget.onError == null && context.mounted) { + context.showErrorSnackBar( + '${localization.unexpectedError}: $error'); + } else { + widget.onError?.call(error); + } + } + setState(() { + _isLoading = false; + }); + } + }, ), spacer(16), ElevatedButton( diff --git a/lib/src/components/supa_phone_auth.dart b/lib/src/components/supa_phone_auth.dart index a6a0935..8d7e4bc 100644 --- a/lib/src/components/supa_phone_auth.dart +++ b/lib/src/components/supa_phone_auth.dart @@ -16,12 +16,22 @@ class SupaPhoneAuth extends StatefulWidget { /// Localization for the form final SupaPhoneAuthLocalization localization; + /// Whether pressing Enter on the on-screen keyboard should automatically + /// submit the form. + /// + /// When set to `false`, the user must explicitly click the submit button + /// to proceed with the authentication process. + /// + /// Defaults to `true` for backward compatibility. + final bool enableAutomaticFormSubmission; + const SupaPhoneAuth({ super.key, required this.authAction, required this.onSuccess, this.onError, this.localization = const SupaPhoneAuthLocalization(), + this.enableAutomaticFormSubmission = true, }); @override @@ -88,6 +98,58 @@ class _SupaPhoneAuthState extends State { ), obscureText: true, controller: _password, + onFieldSubmitted: (_) async { + if (widget.enableAutomaticFormSubmission) { + // Trigger form validation and submission + if (!_formKey.currentState!.validate()) { + return; + } + try { + if (isSigningIn) { + final response = await supabase.auth.signInWithPassword( + phone: _phone.text, + password: _password.text, + ); + widget.onSuccess(response); + } else { + late final AuthResponse response; + final user = supabase.auth.currentUser; + if (user?.isAnonymous == true) { + await supabase.auth.updateUser( + UserAttributes( + phone: _phone.text, + password: _password.text, + ), + ); + } else { + response = await supabase.auth.signUp( + phone: _phone.text, + password: _password.text, + ); + } + if (!mounted) return; + widget.onSuccess(response); + } + } on AuthException catch (error) { + if (widget.onError == null && context.mounted) { + context.showErrorSnackBar(error.message); + } else { + widget.onError?.call(error); + } + } catch (error) { + if (widget.onError == null && context.mounted) { + context.showErrorSnackBar( + '${localization.unexpectedError}: $error'); + } else { + widget.onError?.call(error); + } + } + setState(() { + _phone.text = ''; + _password.text = ''; + }); + } + }, ), spacer(16), ElevatedButton( diff --git a/lib/src/components/supa_reset_password.dart b/lib/src/components/supa_reset_password.dart index 7aa0dd1..79171d1 100644 --- a/lib/src/components/supa_reset_password.dart +++ b/lib/src/components/supa_reset_password.dart @@ -17,12 +17,22 @@ class SupaResetPassword extends StatefulWidget { /// Localization for the form final SupaResetPasswordLocalization localization; + /// Whether pressing Enter on the on-screen keyboard should automatically + /// submit the form. + /// + /// When set to `false`, the user must explicitly click the submit button + /// to proceed with the authentication process. + /// + /// Defaults to `true` for backward compatibility. + final bool enableAutomaticFormSubmission; + const SupaResetPassword({ super.key, this.accessToken, required this.onSuccess, this.onError, this.localization = const SupaResetPasswordLocalization(), + this.enableAutomaticFormSubmission = true, }); @override @@ -60,6 +70,37 @@ class _SupaResetPasswordState extends State { label: Text(localization.enterPassword), ), controller: _password, + onFieldSubmitted: (_) async { + if (widget.enableAutomaticFormSubmission) { + if (!_formKey.currentState!.validate()) { + return; + } + try { + final response = await supabase.auth.updateUser( + UserAttributes( + password: _password.text, + ), + ); + widget.onSuccess.call(response); + // FIX use_build_context_synchronously + if (!context.mounted) return; + context.showSnackBar(localization.passwordResetSent); + } on AuthException catch (error) { + if (widget.onError == null && context.mounted) { + context.showErrorSnackBar(error.message); + } else { + widget.onError?.call(error); + } + } catch (error) { + if (widget.onError == null && context.mounted) { + context.showErrorSnackBar( + '${localization.passwordLengthError}: $error'); + } else { + widget.onError?.call(error); + } + } + } + }, ), spacer(16), ElevatedButton( From 035b438106356495b382bb1bd3f2e61c29aef4eb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 14 Oct 2025 13:43:24 +0000 Subject: [PATCH 4/8] Apply dart format to code Co-authored-by: maurovanetti <402070+maurovanetti@users.noreply.github.com> --- lib/src/components/supa_email_auth.dart | 235 ++++++------ lib/src/components/supa_magic_auth.dart | 20 +- lib/src/components/supa_phone_auth.dart | 6 +- lib/src/components/supa_reset_password.dart | 14 +- lib/src/components/supa_socials_auth.dart | 386 ++++++++++---------- lib/src/components/supa_verify_phone.dart | 3 +- lib/src/utils/constants.dart | 28 +- 7 files changed, 350 insertions(+), 342 deletions(-) diff --git a/lib/src/components/supa_email_auth.dart b/lib/src/components/supa_email_auth.dart index 446ab4d..6097a73 100644 --- a/lib/src/components/supa_email_auth.dart +++ b/lib/src/components/supa_email_auth.dart @@ -119,9 +119,11 @@ class BooleanMetaDataField extends MetaDataField { this.isRequired = false, this.checkboxPosition = ListTileControlAffinity.platform, required super.key, - }) : assert(label != null || richLabelSpans != null, - 'Either label or richLabelSpans must be provided'), - super(label: label ?? ''); + }) : assert( + label != null || richLabelSpans != null, + 'Either label or richLabelSpans must be provided', + ), + super(label: label ?? ''); Widget getLabelWidget(BuildContext context) { // This matches the default style of [TextField], to match the other fields @@ -132,10 +134,7 @@ class BooleanMetaDataField extends MetaDataField { : Theme.of(context).textTheme.titleMedium; return richLabelSpans != null ? RichText( - text: TextSpan( - style: defaultStyle, - children: richLabelSpans, - ), + text: TextSpan(style: defaultStyle, children: richLabelSpans), ) : Text(label, style: defaultStyle); } @@ -286,14 +285,16 @@ class _SupaEmailAuthState extends State { _emailController.text = widget.prefilledEmail ?? ''; _passwordController.text = widget.prefilledPassword ?? ''; _isSigningIn = widget.isInitiallySigningIn; - _metadataControllers = Map.fromEntries((widget.metadataFields ?? []).map( - (metadataField) => MapEntry( - metadataField.key, - metadataField is BooleanMetaDataField - ? metadataField.value - : TextEditingController(), + _metadataControllers = Map.fromEntries( + (widget.metadataFields ?? []).map( + (metadataField) => MapEntry( + metadataField.key, + metadataField is BooleanMetaDataField + ? metadataField.value + : TextEditingController(), + ), ), - )); + ); } @override @@ -357,7 +358,8 @@ class _SupaEmailAuthState extends State { textInputAction: widget.metadataFields != null && !_isSigningIn ? TextInputAction.next : TextInputAction.done, - validator: widget.passwordValidator ?? + validator: + widget.passwordValidator ?? (value) { if (value == null || value.isEmpty || value.length < 6) { return localization.passwordLengthError; @@ -397,91 +399,98 @@ class _SupaEmailAuthState extends State { spacer(16), if (widget.metadataFields != null && !_isSigningIn) ...widget.metadataFields! - .map((metadataField) => [ - // Render a Checkbox that displays an error message - // beneath it if the field is required and the user - // hasn't checked it when submitting the form. - if (metadataField is BooleanMetaDataField) - FormField( - validator: metadataField.isRequired - ? (bool? value) { - if (value != true) { - return localization.requiredFieldError; - } - return null; + .map( + (metadataField) => [ + // Render a Checkbox that displays an error message + // beneath it if the field is required and the user + // hasn't checked it when submitting the form. + if (metadataField is BooleanMetaDataField) + FormField( + validator: metadataField.isRequired + ? (bool? value) { + if (value != true) { + return localization.requiredFieldError; } - : null, - builder: (FormFieldState field) { - final theme = Theme.of(context); - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - CheckboxListTile( - title: - metadataField.getLabelWidget(context), - value: _metadataControllers[ - metadataField.key] as bool, - onChanged: (bool? value) { - setState(() { - _metadataControllers[metadataField - .key] = value ?? false; - }); - field.didChange(value); - }, - checkboxSemanticLabel: - metadataField.checkboxSemanticLabel, - controlAffinity: - metadataField.checkboxPosition, - contentPadding: - const EdgeInsets.symmetric( - horizontal: 4.0), + return null; + } + : null, + builder: (FormFieldState field) { + final theme = Theme.of(context); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CheckboxListTile( + title: metadataField.getLabelWidget( + context, ), - if (field.hasError) - Padding( - padding: const EdgeInsets.only( - left: 16, top: 4), - child: Text( - field.errorText!, - style: theme.textTheme.labelSmall - ?.copyWith( - color: theme.colorScheme.error, - ), - ), + value: + _metadataControllers[metadataField.key] + as bool, + onChanged: (bool? value) { + setState(() { + _metadataControllers[metadataField + .key] = + value ?? false; + }); + field.didChange(value); + }, + checkboxSemanticLabel: + metadataField.checkboxSemanticLabel, + controlAffinity: + metadataField.checkboxPosition, + contentPadding: const EdgeInsets.symmetric( + horizontal: 4.0, + ), + ), + if (field.hasError) + Padding( + padding: const EdgeInsets.only( + left: 16, + top: 4, + ), + child: Text( + field.errorText!, + style: theme.textTheme.labelSmall + ?.copyWith( + color: theme.colorScheme.error, + ), ), - ], - ); - }, - ) - else - // Otherwise render a normal TextFormField matching - // the style of the other fields in the form. - TextFormField( - controller: - _metadataControllers[metadataField.key] - as TextEditingController, - textInputAction: - widget.metadataFields!.last == metadataField - ? TextInputAction.done - : TextInputAction.next, - decoration: InputDecoration( - label: Text(metadataField.label), - prefixIcon: metadataField.prefixIcon, - ), - validator: metadataField.validator, - autovalidateMode: - AutovalidateMode.onUserInteraction, - onFieldSubmitted: (_) { - if (metadataField != - widget.metadataFields!.last) { - FocusScope.of(context).nextFocus(); - } else if (widget.enableAutomaticFormSubmission) { - _signInSignUp(); - } - }, + ), + ], + ); + }, + ) + else + // Otherwise render a normal TextFormField matching + // the style of the other fields in the form. + TextFormField( + controller: + _metadataControllers[metadataField.key] + as TextEditingController, + textInputAction: + widget.metadataFields!.last == metadataField + ? TextInputAction.done + : TextInputAction.next, + decoration: InputDecoration( + label: Text(metadataField.label), + prefixIcon: metadataField.prefixIcon, ), - spacer(16), - ]) + validator: metadataField.validator, + autovalidateMode: + AutovalidateMode.onUserInteraction, + onFieldSubmitted: (_) { + if (metadataField != + widget.metadataFields!.last) { + FocusScope.of(context).nextFocus(); + } else if (widget.enableAutomaticFormSubmission) { + _signInSignUp(); + } + }, + ), + spacer(16), + ], + ) .expand((element) => element), ElevatedButton( onPressed: _signInSignUp, @@ -494,9 +503,11 @@ class _SupaEmailAuthState extends State { strokeWidth: 1.5, ), ) - : Text(_isSigningIn - ? localization.signIn - : localization.signUp), + : Text( + _isSigningIn + ? localization.signIn + : localization.signUp, + ), ), spacer(16), if (_isSigningIn) ...[ @@ -520,9 +531,11 @@ class _SupaEmailAuthState extends State { widget.onToggleSignIn?.call(_isSigningIn); widget.onToggleRecoverPassword?.call(_isRecoveringPassword); }, - child: Text(_isSigningIn - ? localization.dontHaveAccount - : localization.haveAccount), + child: Text( + _isSigningIn + ? localization.dontHaveAccount + : localization.haveAccount, + ), ), ], if (_isSigningIn && _isRecoveringPassword) ...[ @@ -596,7 +609,8 @@ class _SupaEmailAuthState extends State { } catch (error) { if (widget.onError == null && mounted) { context.showErrorSnackBar( - '${widget.localization.unexpectedError}: $error'); + '${widget.localization.unexpectedError}: $error', + ); } else { widget.onError?.call(error); } @@ -657,10 +671,15 @@ class _SupaEmailAuthState extends State { /// Resolve the user_metadata coming from the metadataFields Map _resolveMetadataFieldsData() { - return Map.fromEntries(_metadataControllers.entries.map((entry) => MapEntry( - entry.key, - entry.value is TextEditingController - ? (entry.value as TextEditingController).text - : entry.value))); + return Map.fromEntries( + _metadataControllers.entries.map( + (entry) => MapEntry( + entry.key, + entry.value is TextEditingController + ? (entry.value as TextEditingController).text + : entry.value, + ), + ), + ); } } diff --git a/lib/src/components/supa_magic_auth.dart b/lib/src/components/supa_magic_auth.dart index 5ad6812..b2614cd 100644 --- a/lib/src/components/supa_magic_auth.dart +++ b/lib/src/components/supa_magic_auth.dart @@ -54,13 +54,13 @@ class _SupaMagicAuthState extends State { @override void initState() { super.initState(); - _gotrueSubscription = - Supabase.instance.client.auth.onAuthStateChange.listen((data) { - final session = data.session; - if (session != null && mounted) { - widget.onSuccess(session); - } - }); + _gotrueSubscription = Supabase.instance.client.auth.onAuthStateChange + .listen((data) { + final session = data.session; + if (session != null && mounted) { + widget.onSuccess(session); + } + }); } @override @@ -119,7 +119,8 @@ class _SupaMagicAuthState extends State { } catch (error) { if (widget.onError == null && context.mounted) { context.showErrorSnackBar( - '${localization.unexpectedError}: $error'); + '${localization.unexpectedError}: $error', + ); } else { widget.onError?.call(error); } @@ -169,7 +170,8 @@ class _SupaMagicAuthState extends State { } catch (error) { if (widget.onError == null && context.mounted) { context.showErrorSnackBar( - '${localization.unexpectedError}: $error'); + '${localization.unexpectedError}: $error', + ); } else { widget.onError?.call(error); } diff --git a/lib/src/components/supa_phone_auth.dart b/lib/src/components/supa_phone_auth.dart index 8d7e4bc..48f6ff8 100644 --- a/lib/src/components/supa_phone_auth.dart +++ b/lib/src/components/supa_phone_auth.dart @@ -139,7 +139,8 @@ class _SupaPhoneAuthState extends State { } catch (error) { if (widget.onError == null && context.mounted) { context.showErrorSnackBar( - '${localization.unexpectedError}: $error'); + '${localization.unexpectedError}: $error', + ); } else { widget.onError?.call(error); } @@ -196,7 +197,8 @@ class _SupaPhoneAuthState extends State { } catch (error) { if (widget.onError == null && context.mounted) { context.showErrorSnackBar( - '${localization.unexpectedError}: $error'); + '${localization.unexpectedError}: $error', + ); } else { widget.onError?.call(error); } diff --git a/lib/src/components/supa_reset_password.dart b/lib/src/components/supa_reset_password.dart index 79171d1..1ba0bc3 100644 --- a/lib/src/components/supa_reset_password.dart +++ b/lib/src/components/supa_reset_password.dart @@ -77,9 +77,7 @@ class _SupaResetPasswordState extends State { } try { final response = await supabase.auth.updateUser( - UserAttributes( - password: _password.text, - ), + UserAttributes(password: _password.text), ); widget.onSuccess.call(response); // FIX use_build_context_synchronously @@ -94,7 +92,8 @@ class _SupaResetPasswordState extends State { } catch (error) { if (widget.onError == null && context.mounted) { context.showErrorSnackBar( - '${localization.passwordLengthError}: $error'); + '${localization.passwordLengthError}: $error', + ); } else { widget.onError?.call(error); } @@ -114,9 +113,7 @@ class _SupaResetPasswordState extends State { } try { final response = await supabase.auth.updateUser( - UserAttributes( - password: _password.text, - ), + UserAttributes(password: _password.text), ); widget.onSuccess.call(response); // FIX use_build_context_synchronously @@ -131,7 +128,8 @@ class _SupaResetPasswordState extends State { } catch (error) { if (widget.onError == null && context.mounted) { context.showErrorSnackBar( - '${localization.passwordLengthError}: $error'); + '${localization.passwordLengthError}: $error', + ); } else { widget.onError?.call(error); } diff --git a/lib/src/components/supa_socials_auth.dart b/lib/src/components/supa_socials_auth.dart index 4e21f71..cda3680 100644 --- a/lib/src/components/supa_socials_auth.dart +++ b/lib/src/components/supa_socials_auth.dart @@ -14,45 +14,45 @@ import 'package:supabase_flutter/supabase_flutter.dart'; extension on OAuthProvider { IconData get iconData => switch (this) { - OAuthProvider.apple => FontAwesomeIcons.apple, - OAuthProvider.azure => FontAwesomeIcons.microsoft, - OAuthProvider.bitbucket => FontAwesomeIcons.bitbucket, - OAuthProvider.discord => FontAwesomeIcons.discord, - OAuthProvider.facebook => FontAwesomeIcons.facebook, - OAuthProvider.figma => FontAwesomeIcons.figma, - OAuthProvider.github => FontAwesomeIcons.github, - OAuthProvider.gitlab => FontAwesomeIcons.gitlab, - OAuthProvider.google => FontAwesomeIcons.google, - OAuthProvider.linkedin => FontAwesomeIcons.linkedin, - OAuthProvider.slack => FontAwesomeIcons.slack, - OAuthProvider.spotify => FontAwesomeIcons.spotify, - OAuthProvider.twitch => FontAwesomeIcons.twitch, - OAuthProvider.twitter => FontAwesomeIcons.xTwitter, - _ => Icons.close, - }; + OAuthProvider.apple => FontAwesomeIcons.apple, + OAuthProvider.azure => FontAwesomeIcons.microsoft, + OAuthProvider.bitbucket => FontAwesomeIcons.bitbucket, + OAuthProvider.discord => FontAwesomeIcons.discord, + OAuthProvider.facebook => FontAwesomeIcons.facebook, + OAuthProvider.figma => FontAwesomeIcons.figma, + OAuthProvider.github => FontAwesomeIcons.github, + OAuthProvider.gitlab => FontAwesomeIcons.gitlab, + OAuthProvider.google => FontAwesomeIcons.google, + OAuthProvider.linkedin => FontAwesomeIcons.linkedin, + OAuthProvider.slack => FontAwesomeIcons.slack, + OAuthProvider.spotify => FontAwesomeIcons.spotify, + OAuthProvider.twitch => FontAwesomeIcons.twitch, + OAuthProvider.twitter => FontAwesomeIcons.xTwitter, + _ => Icons.close, + }; Color get btnBgColor => switch (this) { - OAuthProvider.apple => Colors.black, - OAuthProvider.azure => Colors.blueAccent, - OAuthProvider.bitbucket => Colors.blue, - OAuthProvider.discord => Colors.purple, - OAuthProvider.facebook => const Color(0xFF3b5998), - OAuthProvider.figma => const Color.fromRGBO(241, 77, 27, 1), - OAuthProvider.github => Colors.black, - OAuthProvider.gitlab => Colors.deepOrange, - OAuthProvider.google => Colors.white, - OAuthProvider.kakao => const Color(0xFFFFE812), - OAuthProvider.keycloak => const Color.fromRGBO(0, 138, 170, 1), - OAuthProvider.linkedin => const Color.fromRGBO(0, 136, 209, 1), - OAuthProvider.notion => const Color.fromRGBO(69, 75, 78, 1), - OAuthProvider.slack => const Color.fromRGBO(74, 21, 75, 1), - OAuthProvider.spotify => Colors.green, - OAuthProvider.twitch => Colors.purpleAccent, - OAuthProvider.twitter => Colors.black, - OAuthProvider.workos => const Color.fromRGBO(99, 99, 241, 1), - // ignore: unreachable_switch_case - _ => Colors.black, - }; + OAuthProvider.apple => Colors.black, + OAuthProvider.azure => Colors.blueAccent, + OAuthProvider.bitbucket => Colors.blue, + OAuthProvider.discord => Colors.purple, + OAuthProvider.facebook => const Color(0xFF3b5998), + OAuthProvider.figma => const Color.fromRGBO(241, 77, 27, 1), + OAuthProvider.github => Colors.black, + OAuthProvider.gitlab => Colors.deepOrange, + OAuthProvider.google => Colors.white, + OAuthProvider.kakao => const Color(0xFFFFE812), + OAuthProvider.keycloak => const Color.fromRGBO(0, 138, 170, 1), + OAuthProvider.linkedin => const Color.fromRGBO(0, 136, 209, 1), + OAuthProvider.notion => const Color.fromRGBO(69, 75, 78, 1), + OAuthProvider.slack => const Color.fromRGBO(74, 21, 75, 1), + OAuthProvider.spotify => Colors.green, + OAuthProvider.twitch => Colors.purpleAccent, + OAuthProvider.twitter => Colors.black, + OAuthProvider.workos => const Color.fromRGBO(99, 99, 241, 1), + // ignore: unreachable_switch_case + _ => Colors.black, + }; String get labelText => 'Continue with ${name[0].toUpperCase()}${name.substring(1)}'; @@ -77,10 +77,7 @@ class NativeGoogleAuthConfig { /// Required to perform native Google Sign In on iOS final String? iosClientId; - const NativeGoogleAuthConfig({ - this.webClientId, - this.iosClientId, - }); + const NativeGoogleAuthConfig({this.webClientId, this.iosClientId}); } /// UI Component to create social login form @@ -170,11 +167,13 @@ class _SupaSocialsAuthState extends State { if (accessToken == null) { throw const AuthException( - 'No Access Token found from Google sign in result.'); + 'No Access Token found from Google sign in result.', + ); } if (idToken == null) { throw const AuthException( - 'No ID Token found from Google sign in result.'); + 'No ID Token found from Google sign in result.', + ); } return supabase.auth.signInWithIdToken( @@ -200,7 +199,8 @@ class _SupaSocialsAuthState extends State { final idToken = credential.identityToken; if (idToken == null) { throw const AuthException( - 'Could not find ID Token from generated Apple sign in credential.'); + 'Could not find ID Token from generated Apple sign in credential.', + ); } return supabase.auth.signInWithIdToken( @@ -214,16 +214,16 @@ class _SupaSocialsAuthState extends State { void initState() { super.initState(); localization = widget.localization; - _gotrueSubscription = - Supabase.instance.client.auth.onAuthStateChange.listen((data) { - final session = data.session; - if (session != null && mounted) { - widget.onSuccess.call(session); - if (widget.showSuccessSnackBar) { - context.showSnackBar(localization.successSignInMessage); - } - } - }); + _gotrueSubscription = Supabase.instance.client.auth.onAuthStateChange + .listen((data) { + final session = data.session; + if (session != null && mounted) { + widget.onSuccess.call(session); + if (widget.showSuccessSnackBar) { + context.showSnackBar(localization.successSignInMessage); + } + } + }); } @override @@ -243,176 +243,168 @@ class _SupaSocialsAuthState extends State { return ErrorWidget(Exception('Social provider list cannot be empty')); } - final authButtons = List.generate( - providers.length, - (index) { - final socialProvider = providers[index]; + final authButtons = List.generate(providers.length, (index) { + final socialProvider = providers[index]; - Color? foregroundColor = coloredBg ? Colors.white : null; - Color? backgroundColor = coloredBg ? socialProvider.btnBgColor : null; - Color? overlayColor = coloredBg ? Colors.white10 : null; + Color? foregroundColor = coloredBg ? Colors.white : null; + Color? backgroundColor = coloredBg ? socialProvider.btnBgColor : null; + Color? overlayColor = coloredBg ? Colors.white10 : null; - Color? iconColor = coloredBg ? Colors.white : null; + Color? iconColor = coloredBg ? Colors.white : null; - Widget iconWidget = SizedBox( - height: 48, + Widget iconWidget = SizedBox( + height: 48, + width: 48, + child: Icon(socialProvider.iconData, color: iconColor), + ); + if (socialProvider == OAuthProvider.google && coloredBg) { + iconWidget = Image.asset( + 'assets/logos/google_light.png', + package: 'supabase_auth_ui', width: 48, - child: Icon( - socialProvider.iconData, - color: iconColor, - ), + height: 48, ); - if (socialProvider == OAuthProvider.google && coloredBg) { + foregroundColor = Colors.black; + backgroundColor = Colors.white; + overlayColor = Colors.white; + } + + switch (socialProvider) { + case OAuthProvider.notion: iconWidget = Image.asset( - 'assets/logos/google_light.png', + 'assets/logos/notion.png', package: 'supabase_auth_ui', width: 48, height: 48, ); - foregroundColor = Colors.black; - backgroundColor = Colors.white; - overlayColor = Colors.white; - } - - switch (socialProvider) { - case OAuthProvider.notion: - iconWidget = Image.asset( - 'assets/logos/notion.png', - package: 'supabase_auth_ui', - width: 48, - height: 48, - ); - break; - case OAuthProvider.kakao: - iconWidget = Image.asset( - 'assets/logos/kakao.png', - package: 'supabase_auth_ui', - width: 48, - height: 48, - ); - break; - case OAuthProvider.keycloak: - iconWidget = Image.asset( - 'assets/logos/keycloak.png', - package: 'supabase_auth_ui', - width: 48, - height: 48, - ); - break; - case OAuthProvider.workos: - iconWidget = Image.asset( - 'assets/logos/workOS.png', - package: 'supabase_auth_ui', - color: coloredBg ? Colors.white : null, - width: 48, - height: 48, - ); - break; - default: - break; - } - - onAuthButtonPressed() async { - try { - // Check if native Google login should be performed - if (socialProvider == OAuthProvider.google) { - final webClientId = googleAuthConfig?.webClientId; - final iosClientId = googleAuthConfig?.iosClientId; - final shouldPerformNativeGoogleSignIn = - (webClientId != null && !kIsWeb && Platform.isAndroid) || - (iosClientId != null && !kIsWeb && Platform.isIOS); - if (shouldPerformNativeGoogleSignIn) { - await _nativeGoogleSignIn( - webClientId: webClientId, - iosClientId: iosClientId, - ); - return; - } - } + break; + case OAuthProvider.kakao: + iconWidget = Image.asset( + 'assets/logos/kakao.png', + package: 'supabase_auth_ui', + width: 48, + height: 48, + ); + break; + case OAuthProvider.keycloak: + iconWidget = Image.asset( + 'assets/logos/keycloak.png', + package: 'supabase_auth_ui', + width: 48, + height: 48, + ); + break; + case OAuthProvider.workos: + iconWidget = Image.asset( + 'assets/logos/workOS.png', + package: 'supabase_auth_ui', + color: coloredBg ? Colors.white : null, + width: 48, + height: 48, + ); + break; + default: + break; + } - // Check if native Apple login should be performed - if (socialProvider == OAuthProvider.apple) { - final shouldPerformNativeAppleSignIn = - (isNativeAppleAuthEnabled && !kIsWeb && Platform.isIOS) || - (isNativeAppleAuthEnabled && !kIsWeb && Platform.isMacOS); - if (shouldPerformNativeAppleSignIn) { - await _nativeAppleSignIn(); - return; - } + onAuthButtonPressed() async { + try { + // Check if native Google login should be performed + if (socialProvider == OAuthProvider.google) { + final webClientId = googleAuthConfig?.webClientId; + final iosClientId = googleAuthConfig?.iosClientId; + final shouldPerformNativeGoogleSignIn = + (webClientId != null && !kIsWeb && Platform.isAndroid) || + (iosClientId != null && !kIsWeb && Platform.isIOS); + if (shouldPerformNativeGoogleSignIn) { + await _nativeGoogleSignIn( + webClientId: webClientId, + iosClientId: iosClientId, + ); + return; } + } - final user = supabase.auth.currentUser; - if (user?.isAnonymous == true) { - await supabase.auth.linkIdentity( - socialProvider, - redirectTo: widget.redirectUrl, - scopes: widget.scopes?[socialProvider], - queryParams: widget.queryParams?[socialProvider], - ); + // Check if native Apple login should be performed + if (socialProvider == OAuthProvider.apple) { + final shouldPerformNativeAppleSignIn = + (isNativeAppleAuthEnabled && !kIsWeb && Platform.isIOS) || + (isNativeAppleAuthEnabled && !kIsWeb && Platform.isMacOS); + if (shouldPerformNativeAppleSignIn) { + await _nativeAppleSignIn(); return; } + } - await supabase.auth.signInWithOAuth( + final user = supabase.auth.currentUser; + if (user?.isAnonymous == true) { + await supabase.auth.linkIdentity( socialProvider, redirectTo: widget.redirectUrl, scopes: widget.scopes?[socialProvider], queryParams: widget.queryParams?[socialProvider], - authScreenLaunchMode: widget.authScreenLaunchMode, ); - } on AuthException catch (error) { - if (widget.onError == null && context.mounted) { - context.showErrorSnackBar(error.message); - } else { - widget.onError?.call(error); - } - } catch (error) { - if (widget.onError == null && context.mounted) { - context - .showErrorSnackBar('${localization.unexpectedError}: $error'); - } else { - widget.onError?.call(error); - } + return; } - } - final authButtonStyle = ButtonStyle( - foregroundColor: WidgetStateProperty.all(foregroundColor), - backgroundColor: WidgetStateProperty.all(backgroundColor), - overlayColor: WidgetStateProperty.all(overlayColor), - iconColor: WidgetStateProperty.all(iconColor), - ); + await supabase.auth.signInWithOAuth( + socialProvider, + redirectTo: widget.redirectUrl, + scopes: widget.scopes?[socialProvider], + queryParams: widget.queryParams?[socialProvider], + authScreenLaunchMode: widget.authScreenLaunchMode, + ); + } on AuthException catch (error) { + if (widget.onError == null && context.mounted) { + context.showErrorSnackBar(error.message); + } else { + widget.onError?.call(error); + } + } catch (error) { + if (widget.onError == null && context.mounted) { + context.showErrorSnackBar( + '${localization.unexpectedError}: $error', + ); + } else { + widget.onError?.call(error); + } + } + } - return Padding( - padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 4), - child: widget.socialButtonVariant == SocialButtonVariant.icon - ? Material( - shape: const CircleBorder(), - elevation: 2, - color: backgroundColor, - child: InkResponse( - radius: 24, - onTap: onAuthButtonPressed, - child: iconWidget, - ), - ) - : ElevatedButton.icon( - icon: iconWidget, - style: authButtonStyle, - onPressed: onAuthButtonPressed, - label: Text( - localization.oAuthButtonLabels[socialProvider] ?? - socialProvider.labelText, - ), + final authButtonStyle = ButtonStyle( + foregroundColor: WidgetStateProperty.all(foregroundColor), + backgroundColor: WidgetStateProperty.all(backgroundColor), + overlayColor: WidgetStateProperty.all(overlayColor), + iconColor: WidgetStateProperty.all(iconColor), + ); + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 4), + child: widget.socialButtonVariant == SocialButtonVariant.icon + ? Material( + shape: const CircleBorder(), + elevation: 2, + color: backgroundColor, + child: InkResponse( + radius: 24, + onTap: onAuthButtonPressed, + child: iconWidget, ), - ); - }, - ); + ) + : ElevatedButton.icon( + icon: iconWidget, + style: authButtonStyle, + onPressed: onAuthButtonPressed, + label: Text( + localization.oAuthButtonLabels[socialProvider] ?? + socialProvider.labelText, + ), + ), + ); + }); return widget.socialButtonVariant == SocialButtonVariant.icon - ? Wrap( - alignment: WrapAlignment.spaceEvenly, - children: authButtons, - ) + ? Wrap(alignment: WrapAlignment.spaceEvenly, children: authButtons) : Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: authButtons, diff --git a/lib/src/components/supa_verify_phone.dart b/lib/src/components/supa_verify_phone.dart index 83b68df..f085f55 100644 --- a/lib/src/components/supa_verify_phone.dart +++ b/lib/src/components/supa_verify_phone.dart @@ -90,7 +90,8 @@ class _SupaVerifyPhoneState extends State { } catch (error) { if (widget.onError == null && context.mounted) { context.showErrorSnackBar( - '${localization.unexpectedErrorOccurred}: $error'); + '${localization.unexpectedErrorOccurred}: $error', + ); } else { widget.onError?.call(error); } diff --git a/lib/src/utils/constants.dart b/lib/src/utils/constants.dart index 6b66211..751e832 100644 --- a/lib/src/utils/constants.dart +++ b/lib/src/utils/constants.dart @@ -4,9 +4,7 @@ import 'package:supabase_flutter/supabase_flutter.dart'; final supabase = Supabase.instance.client; SizedBox spacer(double height) { - return SizedBox( - height: height, - ); + return SizedBox(height: height); } /// Set of extension methods to easily display a snackbar @@ -18,24 +16,20 @@ extension ShowSnackBar on BuildContext { Color? backgroundColor, String? actionLabel, }) { - ScaffoldMessenger.of(this).showSnackBar(SnackBar( - content: Text( - message, - style: textColor == null ? null : TextStyle(color: textColor), + ScaffoldMessenger.of(this).showSnackBar( + SnackBar( + content: Text( + message, + style: textColor == null ? null : TextStyle(color: textColor), + ), + backgroundColor: backgroundColor, + action: SnackBarAction(label: actionLabel ?? 'ok', onPressed: () {}), ), - backgroundColor: backgroundColor, - action: SnackBarAction( - label: actionLabel ?? 'ok', - onPressed: () {}, - ), - )); + ); } /// Displays a red snackbar indicating error - void showErrorSnackBar( - String message, { - String? actionLabel, - }) { + void showErrorSnackBar(String message, {String? actionLabel}) { showSnackBar( message, textColor: Theme.of(this).colorScheme.onErrorContainer, From 97963d3d6966215c9373f18004c5d40c5b7a2670 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 14 Oct 2025 13:44:22 +0000 Subject: [PATCH 5/8] Add documentation for enableAutomaticFormSubmission feature Co-authored-by: maurovanetti <402070+maurovanetti@users.noreply.github.com> --- CHANGELOG.md | 4 ++++ README.md | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a34229..0b5b596 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## Unreleased + +- feat: Add `enableAutomaticFormSubmission` flag to prevent automatic form submission when pressing Enter on on-screen keyboard in all auth components (SupaEmailAuth, SupaPhoneAuth, SupaMagicAuth, SupaResetPassword) + ## 0.5.5 - feat: Add Confirm Password Field to SupaEmailAuth Component for Sign-Up Process [#129](https://github.com/supabase-community/flutter-auth-ui/pull/129) diff --git a/README.md b/README.md index 6772c15..20a5eec 100644 --- a/README.md +++ b/README.md @@ -133,3 +133,53 @@ SupaSocialsAuth( This library uses bare Flutter components so that you can control the appearance of the components using your own theme. See theme example in example/lib/sign_in.dart + +## Controlling Form Submission Behavior + +All auth components (`SupaEmailAuth`, `SupaPhoneAuth`, `SupaMagicAuth`, and `SupaResetPassword`) support the `enableAutomaticFormSubmission` parameter to control whether pressing Enter/Done on the on-screen keyboard automatically submits the form. + +By default, this is set to `true` for backward compatibility, which means pressing Enter will submit the form. If you want users to be forced to explicitly tap the submit button, set this to `false`: + +```dart +SupaEmailAuth( + redirectTo: kIsWeb ? null : 'io.mydomain.myapp://callback', + enableAutomaticFormSubmission: false, // Disable auto-submit on Enter + onSignInComplete: (response) { + // do something, for example: navigate('home'); + }, + onSignUpComplete: (response) { + // do something, for example: navigate("wait_for_email"); + }, +), +``` + +This applies to all auth components: + +```dart +// Phone Auth +SupaPhoneAuth( + authAction: SupaAuthAction.signIn, + enableAutomaticFormSubmission: false, + onSuccess: (response) { + // handle success + }, +), + +// Magic Link Auth +SupaMagicAuth( + redirectUrl: kIsWeb ? null : 'io.supabase.flutter://reset-callback/', + enableAutomaticFormSubmission: false, + onSuccess: (Session response) { + // handle success + }, +), + +// Reset Password +SupaResetPassword( + accessToken: supabase.auth.currentSession?.accessToken, + enableAutomaticFormSubmission: false, + onSuccess: (UserResponse response) { + // handle success + }, +), +``` From 9b8f3bdcfba283492d2f2c78e961338d8291fc9b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 14 Oct 2025 13:45:21 +0000 Subject: [PATCH 6/8] Apply final dart format Co-authored-by: maurovanetti <402070+maurovanetti@users.noreply.github.com> --- lib/src/components/supa_email_auth.dart | 31 ++++--- lib/src/components/supa_magic_auth.dart | 14 ++-- lib/src/components/supa_socials_auth.dart | 98 +++++++++++------------ 3 files changed, 70 insertions(+), 73 deletions(-) diff --git a/lib/src/components/supa_email_auth.dart b/lib/src/components/supa_email_auth.dart index 6097a73..cdfda86 100644 --- a/lib/src/components/supa_email_auth.dart +++ b/lib/src/components/supa_email_auth.dart @@ -119,11 +119,11 @@ class BooleanMetaDataField extends MetaDataField { this.isRequired = false, this.checkboxPosition = ListTileControlAffinity.platform, required super.key, - }) : assert( - label != null || richLabelSpans != null, - 'Either label or richLabelSpans must be provided', - ), - super(label: label ?? ''); + }) : assert( + label != null || richLabelSpans != null, + 'Either label or richLabelSpans must be provided', + ), + super(label: label ?? ''); Widget getLabelWidget(BuildContext context) { // This matches the default style of [TextField], to match the other fields @@ -358,8 +358,7 @@ class _SupaEmailAuthState extends State { textInputAction: widget.metadataFields != null && !_isSigningIn ? TextInputAction.next : TextInputAction.done, - validator: - widget.passwordValidator ?? + validator: widget.passwordValidator ?? (value) { if (value == null || value.isEmpty || value.length < 6) { return localization.passwordLengthError; @@ -429,9 +428,8 @@ class _SupaEmailAuthState extends State { as bool, onChanged: (bool? value) { setState(() { - _metadataControllers[metadataField - .key] = - value ?? false; + _metadataControllers[ + metadataField.key] = value ?? false; }); field.didChange(value); }, @@ -453,8 +451,8 @@ class _SupaEmailAuthState extends State { field.errorText!, style: theme.textTheme.labelSmall ?.copyWith( - color: theme.colorScheme.error, - ), + color: theme.colorScheme.error, + ), ), ), ], @@ -465,13 +463,12 @@ class _SupaEmailAuthState extends State { // Otherwise render a normal TextFormField matching // the style of the other fields in the form. TextFormField( - controller: - _metadataControllers[metadataField.key] - as TextEditingController, + controller: _metadataControllers[metadataField.key] + as TextEditingController, textInputAction: widget.metadataFields!.last == metadataField - ? TextInputAction.done - : TextInputAction.next, + ? TextInputAction.done + : TextInputAction.next, decoration: InputDecoration( label: Text(metadataField.label), prefixIcon: metadataField.prefixIcon, diff --git a/lib/src/components/supa_magic_auth.dart b/lib/src/components/supa_magic_auth.dart index b2614cd..2d107fc 100644 --- a/lib/src/components/supa_magic_auth.dart +++ b/lib/src/components/supa_magic_auth.dart @@ -54,13 +54,13 @@ class _SupaMagicAuthState extends State { @override void initState() { super.initState(); - _gotrueSubscription = Supabase.instance.client.auth.onAuthStateChange - .listen((data) { - final session = data.session; - if (session != null && mounted) { - widget.onSuccess(session); - } - }); + _gotrueSubscription = + Supabase.instance.client.auth.onAuthStateChange.listen((data) { + final session = data.session; + if (session != null && mounted) { + widget.onSuccess(session); + } + }); } @override diff --git a/lib/src/components/supa_socials_auth.dart b/lib/src/components/supa_socials_auth.dart index cda3680..8388523 100644 --- a/lib/src/components/supa_socials_auth.dart +++ b/lib/src/components/supa_socials_auth.dart @@ -14,45 +14,45 @@ import 'package:supabase_flutter/supabase_flutter.dart'; extension on OAuthProvider { IconData get iconData => switch (this) { - OAuthProvider.apple => FontAwesomeIcons.apple, - OAuthProvider.azure => FontAwesomeIcons.microsoft, - OAuthProvider.bitbucket => FontAwesomeIcons.bitbucket, - OAuthProvider.discord => FontAwesomeIcons.discord, - OAuthProvider.facebook => FontAwesomeIcons.facebook, - OAuthProvider.figma => FontAwesomeIcons.figma, - OAuthProvider.github => FontAwesomeIcons.github, - OAuthProvider.gitlab => FontAwesomeIcons.gitlab, - OAuthProvider.google => FontAwesomeIcons.google, - OAuthProvider.linkedin => FontAwesomeIcons.linkedin, - OAuthProvider.slack => FontAwesomeIcons.slack, - OAuthProvider.spotify => FontAwesomeIcons.spotify, - OAuthProvider.twitch => FontAwesomeIcons.twitch, - OAuthProvider.twitter => FontAwesomeIcons.xTwitter, - _ => Icons.close, - }; + OAuthProvider.apple => FontAwesomeIcons.apple, + OAuthProvider.azure => FontAwesomeIcons.microsoft, + OAuthProvider.bitbucket => FontAwesomeIcons.bitbucket, + OAuthProvider.discord => FontAwesomeIcons.discord, + OAuthProvider.facebook => FontAwesomeIcons.facebook, + OAuthProvider.figma => FontAwesomeIcons.figma, + OAuthProvider.github => FontAwesomeIcons.github, + OAuthProvider.gitlab => FontAwesomeIcons.gitlab, + OAuthProvider.google => FontAwesomeIcons.google, + OAuthProvider.linkedin => FontAwesomeIcons.linkedin, + OAuthProvider.slack => FontAwesomeIcons.slack, + OAuthProvider.spotify => FontAwesomeIcons.spotify, + OAuthProvider.twitch => FontAwesomeIcons.twitch, + OAuthProvider.twitter => FontAwesomeIcons.xTwitter, + _ => Icons.close, + }; Color get btnBgColor => switch (this) { - OAuthProvider.apple => Colors.black, - OAuthProvider.azure => Colors.blueAccent, - OAuthProvider.bitbucket => Colors.blue, - OAuthProvider.discord => Colors.purple, - OAuthProvider.facebook => const Color(0xFF3b5998), - OAuthProvider.figma => const Color.fromRGBO(241, 77, 27, 1), - OAuthProvider.github => Colors.black, - OAuthProvider.gitlab => Colors.deepOrange, - OAuthProvider.google => Colors.white, - OAuthProvider.kakao => const Color(0xFFFFE812), - OAuthProvider.keycloak => const Color.fromRGBO(0, 138, 170, 1), - OAuthProvider.linkedin => const Color.fromRGBO(0, 136, 209, 1), - OAuthProvider.notion => const Color.fromRGBO(69, 75, 78, 1), - OAuthProvider.slack => const Color.fromRGBO(74, 21, 75, 1), - OAuthProvider.spotify => Colors.green, - OAuthProvider.twitch => Colors.purpleAccent, - OAuthProvider.twitter => Colors.black, - OAuthProvider.workos => const Color.fromRGBO(99, 99, 241, 1), - // ignore: unreachable_switch_case - _ => Colors.black, - }; + OAuthProvider.apple => Colors.black, + OAuthProvider.azure => Colors.blueAccent, + OAuthProvider.bitbucket => Colors.blue, + OAuthProvider.discord => Colors.purple, + OAuthProvider.facebook => const Color(0xFF3b5998), + OAuthProvider.figma => const Color.fromRGBO(241, 77, 27, 1), + OAuthProvider.github => Colors.black, + OAuthProvider.gitlab => Colors.deepOrange, + OAuthProvider.google => Colors.white, + OAuthProvider.kakao => const Color(0xFFFFE812), + OAuthProvider.keycloak => const Color.fromRGBO(0, 138, 170, 1), + OAuthProvider.linkedin => const Color.fromRGBO(0, 136, 209, 1), + OAuthProvider.notion => const Color.fromRGBO(69, 75, 78, 1), + OAuthProvider.slack => const Color.fromRGBO(74, 21, 75, 1), + OAuthProvider.spotify => Colors.green, + OAuthProvider.twitch => Colors.purpleAccent, + OAuthProvider.twitter => Colors.black, + OAuthProvider.workos => const Color.fromRGBO(99, 99, 241, 1), + // ignore: unreachable_switch_case + _ => Colors.black, + }; String get labelText => 'Continue with ${name[0].toUpperCase()}${name.substring(1)}'; @@ -214,16 +214,16 @@ class _SupaSocialsAuthState extends State { void initState() { super.initState(); localization = widget.localization; - _gotrueSubscription = Supabase.instance.client.auth.onAuthStateChange - .listen((data) { - final session = data.session; - if (session != null && mounted) { - widget.onSuccess.call(session); - if (widget.showSuccessSnackBar) { - context.showSnackBar(localization.successSignInMessage); - } - } - }); + _gotrueSubscription = + Supabase.instance.client.auth.onAuthStateChange.listen((data) { + final session = data.session; + if (session != null && mounted) { + widget.onSuccess.call(session); + if (widget.showSuccessSnackBar) { + context.showSnackBar(localization.successSignInMessage); + } + } + }); } @override @@ -315,7 +315,7 @@ class _SupaSocialsAuthState extends State { final iosClientId = googleAuthConfig?.iosClientId; final shouldPerformNativeGoogleSignIn = (webClientId != null && !kIsWeb && Platform.isAndroid) || - (iosClientId != null && !kIsWeb && Platform.isIOS); + (iosClientId != null && !kIsWeb && Platform.isIOS); if (shouldPerformNativeGoogleSignIn) { await _nativeGoogleSignIn( webClientId: webClientId, @@ -329,7 +329,7 @@ class _SupaSocialsAuthState extends State { if (socialProvider == OAuthProvider.apple) { final shouldPerformNativeAppleSignIn = (isNativeAppleAuthEnabled && !kIsWeb && Platform.isIOS) || - (isNativeAppleAuthEnabled && !kIsWeb && Platform.isMacOS); + (isNativeAppleAuthEnabled && !kIsWeb && Platform.isMacOS); if (shouldPerformNativeAppleSignIn) { await _nativeAppleSignIn(); return; From 200264ba46f6b9e14f82acaf45ac8a805b806689 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 14 Oct 2025 14:12:21 +0000 Subject: [PATCH 7/8] Refactor: extract form submission logic into separate methods - Extract _signInWithMagicLink() method in SupaMagicAuth - Extract _submitForm() method in SupaPhoneAuth - Extract _updatePassword() method in SupaResetPassword - Fix use_build_context_synchronously warnings by properly checking context.mounted - Fix sort_child_properties_last warnings by placing child parameter last - Reduces code duplication and improves maintainability Co-authored-by: maurovanetti <402070+maurovanetti@users.noreply.github.com> --- lib/src/components/supa_magic_auth.dart | 107 ++++++-------- lib/src/components/supa_phone_auth.dart | 152 +++++++------------- lib/src/components/supa_reset_password.dart | 87 +++++------ 3 files changed, 127 insertions(+), 219 deletions(-) diff --git a/lib/src/components/supa_magic_auth.dart b/lib/src/components/supa_magic_auth.dart index 2d107fc..b9e38b0 100644 --- a/lib/src/components/supa_magic_auth.dart +++ b/lib/src/components/supa_magic_auth.dart @@ -70,6 +70,45 @@ class _SupaMagicAuthState extends State { super.dispose(); } + Future _signInWithMagicLink() async { + if (!_formKey.currentState!.validate()) { + return; + } + setState(() { + _isLoading = true; + }); + try { + await supabase.auth.signInWithOtp( + email: _email.text, + emailRedirectTo: widget.redirectUrl, + ); + if (!mounted) return; + if (context.mounted) { + context.showSnackBar(widget.localization.checkYourEmail); + } + } on AuthException catch (error) { + if (widget.onError != null) { + widget.onError?.call(error); + } else if (context.mounted) { + context.showErrorSnackBar(error.message); + } + } catch (error) { + if (widget.onError != null) { + widget.onError?.call(error); + } else if (context.mounted) { + context.showErrorSnackBar( + '${widget.localization.unexpectedError}: $error', + ); + } + } finally { + if (mounted) { + setState(() { + _isLoading = false; + }); + } + } + } + @override Widget build(BuildContext context) { final localization = widget.localization; @@ -96,43 +135,13 @@ class _SupaMagicAuthState extends State { controller: _email, onFieldSubmitted: (_) async { if (widget.enableAutomaticFormSubmission) { - if (!_formKey.currentState!.validate()) { - return; - } - setState(() { - _isLoading = true; - }); - try { - await supabase.auth.signInWithOtp( - email: _email.text, - emailRedirectTo: widget.redirectUrl, - ); - if (context.mounted) { - context.showSnackBar(localization.checkYourEmail); - } - } on AuthException catch (error) { - if (widget.onError == null && context.mounted) { - context.showErrorSnackBar(error.message); - } else { - widget.onError?.call(error); - } - } catch (error) { - if (widget.onError == null && context.mounted) { - context.showErrorSnackBar( - '${localization.unexpectedError}: $error', - ); - } else { - widget.onError?.call(error); - } - } - setState(() { - _isLoading = false; - }); + await _signInWithMagicLink(); } }, ), spacer(16), ElevatedButton( + onPressed: _signInWithMagicLink, child: (_isLoading) ? SizedBox( height: 16, @@ -146,40 +155,6 @@ class _SupaMagicAuthState extends State { localization.continueWithMagicLink, style: const TextStyle(fontWeight: FontWeight.bold), ), - onPressed: () async { - if (!_formKey.currentState!.validate()) { - return; - } - setState(() { - _isLoading = true; - }); - try { - await supabase.auth.signInWithOtp( - email: _email.text, - emailRedirectTo: widget.redirectUrl, - ); - if (context.mounted) { - context.showSnackBar(localization.checkYourEmail); - } - } on AuthException catch (error) { - if (widget.onError == null && context.mounted) { - context.showErrorSnackBar(error.message); - } else { - widget.onError?.call(error); - } - } catch (error) { - if (widget.onError == null && context.mounted) { - context.showErrorSnackBar( - '${localization.unexpectedError}: $error', - ); - } else { - widget.onError?.call(error); - } - } - setState(() { - _isLoading = false; - }); - }, ), spacer(10), ], diff --git a/lib/src/components/supa_phone_auth.dart b/lib/src/components/supa_phone_auth.dart index 48f6ff8..437448e 100644 --- a/lib/src/components/supa_phone_auth.dart +++ b/lib/src/components/supa_phone_auth.dart @@ -55,6 +55,57 @@ class _SupaPhoneAuthState extends State { super.dispose(); } + Future _submitForm() async { + if (!_formKey.currentState!.validate()) { + return; + } + final localization = widget.localization; + final isSigningIn = widget.authAction == SupaAuthAction.signIn; + try { + if (isSigningIn) { + final response = await supabase.auth.signInWithPassword( + phone: _phone.text, + password: _password.text, + ); + if (!mounted) return; + widget.onSuccess(response); + } else { + late final AuthResponse response; + final user = supabase.auth.currentUser; + if (user?.isAnonymous == true) { + await supabase.auth.updateUser( + UserAttributes(phone: _phone.text, password: _password.text), + ); + } else { + response = await supabase.auth.signUp( + phone: _phone.text, + password: _password.text, + ); + } + if (!mounted) return; + widget.onSuccess(response); + } + } on AuthException catch (error) { + if (widget.onError != null) { + widget.onError?.call(error); + } else if (context.mounted) { + context.showErrorSnackBar(error.message); + } + } catch (error) { + if (widget.onError != null) { + widget.onError?.call(error); + } else if (context.mounted) { + context.showErrorSnackBar('${localization.unexpectedError}: $error'); + } + } + if (mounted) { + setState(() { + _phone.text = ''; + _password.text = ''; + }); + } + } + @override Widget build(BuildContext context) { final localization = widget.localization; @@ -100,114 +151,17 @@ class _SupaPhoneAuthState extends State { controller: _password, onFieldSubmitted: (_) async { if (widget.enableAutomaticFormSubmission) { - // Trigger form validation and submission - if (!_formKey.currentState!.validate()) { - return; - } - try { - if (isSigningIn) { - final response = await supabase.auth.signInWithPassword( - phone: _phone.text, - password: _password.text, - ); - widget.onSuccess(response); - } else { - late final AuthResponse response; - final user = supabase.auth.currentUser; - if (user?.isAnonymous == true) { - await supabase.auth.updateUser( - UserAttributes( - phone: _phone.text, - password: _password.text, - ), - ); - } else { - response = await supabase.auth.signUp( - phone: _phone.text, - password: _password.text, - ); - } - if (!mounted) return; - widget.onSuccess(response); - } - } on AuthException catch (error) { - if (widget.onError == null && context.mounted) { - context.showErrorSnackBar(error.message); - } else { - widget.onError?.call(error); - } - } catch (error) { - if (widget.onError == null && context.mounted) { - context.showErrorSnackBar( - '${localization.unexpectedError}: $error', - ); - } else { - widget.onError?.call(error); - } - } - setState(() { - _phone.text = ''; - _password.text = ''; - }); + await _submitForm(); } }, ), spacer(16), ElevatedButton( + onPressed: _submitForm, child: Text( isSigningIn ? localization.signIn : localization.signUp, style: const TextStyle(fontWeight: FontWeight.bold), ), - onPressed: () async { - if (!_formKey.currentState!.validate()) { - return; - } - try { - if (isSigningIn) { - final response = await supabase.auth.signInWithPassword( - phone: _phone.text, - password: _password.text, - ); - widget.onSuccess(response); - } else { - late final AuthResponse response; - final user = supabase.auth.currentUser; - if (user?.isAnonymous == true) { - await supabase.auth.updateUser( - UserAttributes( - phone: _phone.text, - password: _password.text, - ), - ); - } else { - response = await supabase.auth.signUp( - phone: _phone.text, - password: _password.text, - ); - } - if (!mounted) return; - widget.onSuccess(response); - } - } on AuthException catch (error) { - if (widget.onError == null && context.mounted) { - context.showErrorSnackBar(error.message); - } else { - widget.onError?.call(error); - } - } catch (error) { - if (widget.onError == null && context.mounted) { - context.showErrorSnackBar( - '${localization.unexpectedError}: $error', - ); - } else { - widget.onError?.call(error); - } - } - setState(() { - _phone.text = ''; - _password.text = ''; - }); - }, ), spacer(10), ], diff --git a/lib/src/components/supa_reset_password.dart b/lib/src/components/supa_reset_password.dart index 1ba0bc3..6629712 100644 --- a/lib/src/components/supa_reset_password.dart +++ b/lib/src/components/supa_reset_password.dart @@ -49,6 +49,37 @@ class _SupaResetPasswordState extends State { super.dispose(); } + Future _updatePassword() async { + if (!_formKey.currentState!.validate()) { + return; + } + final localization = widget.localization; + try { + final response = await supabase.auth.updateUser( + UserAttributes(password: _password.text), + ); + widget.onSuccess.call(response); + if (!mounted) return; + if (context.mounted) { + context.showSnackBar(localization.passwordResetSent); + } + } on AuthException catch (error) { + if (widget.onError != null) { + widget.onError?.call(error); + } else if (context.mounted) { + context.showErrorSnackBar(error.message); + } + } catch (error) { + if (widget.onError != null) { + widget.onError?.call(error); + } else if (context.mounted) { + context.showErrorSnackBar( + '${localization.passwordLengthError}: $error', + ); + } + } + } + @override Widget build(BuildContext context) { final localization = widget.localization; @@ -72,69 +103,17 @@ class _SupaResetPasswordState extends State { controller: _password, onFieldSubmitted: (_) async { if (widget.enableAutomaticFormSubmission) { - if (!_formKey.currentState!.validate()) { - return; - } - try { - final response = await supabase.auth.updateUser( - UserAttributes(password: _password.text), - ); - widget.onSuccess.call(response); - // FIX use_build_context_synchronously - if (!context.mounted) return; - context.showSnackBar(localization.passwordResetSent); - } on AuthException catch (error) { - if (widget.onError == null && context.mounted) { - context.showErrorSnackBar(error.message); - } else { - widget.onError?.call(error); - } - } catch (error) { - if (widget.onError == null && context.mounted) { - context.showErrorSnackBar( - '${localization.passwordLengthError}: $error', - ); - } else { - widget.onError?.call(error); - } - } + await _updatePassword(); } }, ), spacer(16), ElevatedButton( + onPressed: _updatePassword, child: Text( localization.updatePassword, style: const TextStyle(fontWeight: FontWeight.bold), ), - onPressed: () async { - if (!_formKey.currentState!.validate()) { - return; - } - try { - final response = await supabase.auth.updateUser( - UserAttributes(password: _password.text), - ); - widget.onSuccess.call(response); - // FIX use_build_context_synchronously - if (!context.mounted) return; - context.showSnackBar(localization.passwordResetSent); - } on AuthException catch (error) { - if (widget.onError == null && context.mounted) { - context.showErrorSnackBar(error.message); - } else { - widget.onError?.call(error); - } - } catch (error) { - if (widget.onError == null && context.mounted) { - context.showErrorSnackBar( - '${localization.passwordLengthError}: $error', - ); - } else { - widget.onError?.call(error); - } - } - }, ), spacer(10), ], From 89c703bc3446a7e092d988a25b23b8bc5760c76b Mon Sep 17 00:00:00 2001 From: Mauro Vanetti Date: Tue, 14 Oct 2025 17:29:20 +0200 Subject: [PATCH 8/8] feat: Add autosubmit flag --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index f21c691..4d42306 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: supabase_auth_ui description: UI library to implement auth forms using Supabase and Flutter -version: 0.5.6 +version: 0.5.7 homepage: https://supabase.com repository: 'https://github.com/supabase-community/flutter-auth-ui'