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 + }, +), +``` 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..cdfda86 100644 --- a/lib/src/components/supa_email_auth.dart +++ b/lib/src/components/supa_email_auth.dart @@ -119,8 +119,10 @@ 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'), + }) : assert( + label != null || richLabelSpans != null, + 'Either label or richLabelSpans must be provided', + ), super(label: label ?? ''); Widget getLabelWidget(BuildContext context) { @@ -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); } @@ -220,6 +219,21 @@ 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; + + /// 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, @@ -240,6 +254,9 @@ class SupaEmailAuth extends StatefulWidget { this.prefixIconEmail = const Icon(Icons.email), this.prefixIconPassword = const Icon(Icons.lock), this.showConfirmPasswordField = false, + this.prefilledEmail, + this.prefilledPassword, + this.enableAutomaticFormSubmission = true, }); @override @@ -265,15 +282,19 @@ 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( - 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 @@ -321,7 +342,8 @@ class _SupaEmailAuthState extends State { ), controller: _emailController, onFieldSubmitted: (_) { - if (_isRecoveringPassword) { + if (_isRecoveringPassword && + widget.enableAutomaticFormSubmission) { _passwordRecovery(); } }, @@ -350,7 +372,8 @@ class _SupaEmailAuthState extends State { obscureText: true, controller: _passwordController, onFieldSubmitted: (_) { - if (widget.metadataFields == null || _isSigningIn) { + if ((widget.metadataFields == null || _isSigningIn) && + widget.enableAutomaticFormSubmission) { _signInSignUp(); } }, @@ -375,91 +398,96 @@ 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 { - _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, @@ -472,9 +500,11 @@ class _SupaEmailAuthState extends State { strokeWidth: 1.5, ), ) - : Text(_isSigningIn - ? localization.signIn - : localization.signUp), + : Text( + _isSigningIn + ? localization.signIn + : localization.signUp, + ), ), spacer(16), if (_isSigningIn) ...[ @@ -498,9 +528,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) ...[ @@ -574,7 +606,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); } @@ -635,10 +668,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 71f366d..b9e38b0 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 @@ -60,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; @@ -84,9 +133,15 @@ class _SupaMagicAuthState extends State { label: Text(localization.enterEmail), ), controller: _email, + onFieldSubmitted: (_) async { + if (widget.enableAutomaticFormSubmission) { + await _signInWithMagicLink(); + } + }, ), spacer(16), ElevatedButton( + onPressed: _signInWithMagicLink, child: (_isLoading) ? SizedBox( height: 16, @@ -100,39 +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 a6a0935..437448e 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 @@ -45,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; @@ -88,62 +149,19 @@ class _SupaPhoneAuthState extends State { ), obscureText: true, controller: _password, + onFieldSubmitted: (_) async { + if (widget.enableAutomaticFormSubmission) { + 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 7aa0dd1..6629712 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 @@ -39,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; @@ -60,42 +101,19 @@ class _SupaResetPasswordState extends State { label: Text(localization.enterPassword), ), controller: _password, + onFieldSubmitted: (_) async { + if (widget.enableAutomaticFormSubmission) { + 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), ], diff --git a/lib/src/components/supa_socials_auth.dart b/lib/src/components/supa_socials_auth.dart index 4e21f71..8388523 100644 --- a/lib/src/components/supa_socials_auth.dart +++ b/lib/src/components/supa_socials_auth.dart @@ -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( @@ -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, diff --git a/pubspec.yaml b/pubspec.yaml index 880ca10..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.5 +version: 0.5.7 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: