Skip to content

Commit 550e270

Browse files
authored
Merge pull request #2 from maurovanetti/copilot/add-config-flag-prevent-auto-submit
Add enableAutomaticFormSubmission flag to prevent automatic form submission on Enter key
2 parents 69a229d + 200264b commit 550e270

File tree

9 files changed

+513
-386
lines changed

9 files changed

+513
-386
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## Unreleased
2+
3+
- feat: Add `enableAutomaticFormSubmission` flag to prevent automatic form submission when pressing Enter on on-screen keyboard in all auth components (SupaEmailAuth, SupaPhoneAuth, SupaMagicAuth, SupaResetPassword)
4+
15
## 0.5.5
26

37
- feat: Add Confirm Password Field to SupaEmailAuth Component for Sign-Up Process [#129](https://github.com/supabase-community/flutter-auth-ui/pull/129)

README.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,3 +133,53 @@ SupaSocialsAuth(
133133

134134
This library uses bare Flutter components so that you can control the appearance of the components using your own theme.
135135
See theme example in example/lib/sign_in.dart
136+
137+
## Controlling Form Submission Behavior
138+
139+
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.
140+
141+
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`:
142+
143+
```dart
144+
SupaEmailAuth(
145+
redirectTo: kIsWeb ? null : 'io.mydomain.myapp://callback',
146+
enableAutomaticFormSubmission: false, // Disable auto-submit on Enter
147+
onSignInComplete: (response) {
148+
// do something, for example: navigate('home');
149+
},
150+
onSignUpComplete: (response) {
151+
// do something, for example: navigate("wait_for_email");
152+
},
153+
),
154+
```
155+
156+
This applies to all auth components:
157+
158+
```dart
159+
// Phone Auth
160+
SupaPhoneAuth(
161+
authAction: SupaAuthAction.signIn,
162+
enableAutomaticFormSubmission: false,
163+
onSuccess: (response) {
164+
// handle success
165+
},
166+
),
167+
168+
// Magic Link Auth
169+
SupaMagicAuth(
170+
redirectUrl: kIsWeb ? null : 'io.supabase.flutter://reset-callback/',
171+
enableAutomaticFormSubmission: false,
172+
onSuccess: (Session response) {
173+
// handle success
174+
},
175+
),
176+
177+
// Reset Password
178+
SupaResetPassword(
179+
accessToken: supabase.auth.currentSession?.accessToken,
180+
enableAutomaticFormSubmission: false,
181+
onSuccess: (UserResponse response) {
182+
// handle success
183+
},
184+
),
185+
```

lib/src/components/supa_email_auth.dart

Lines changed: 135 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -119,8 +119,10 @@ class BooleanMetaDataField extends MetaDataField {
119119
this.isRequired = false,
120120
this.checkboxPosition = ListTileControlAffinity.platform,
121121
required super.key,
122-
}) : assert(label != null || richLabelSpans != null,
123-
'Either label or richLabelSpans must be provided'),
122+
}) : assert(
123+
label != null || richLabelSpans != null,
124+
'Either label or richLabelSpans must be provided',
125+
),
124126
super(label: label ?? '');
125127

126128
Widget getLabelWidget(BuildContext context) {
@@ -132,10 +134,7 @@ class BooleanMetaDataField extends MetaDataField {
132134
: Theme.of(context).textTheme.titleMedium;
133135
return richLabelSpans != null
134136
? RichText(
135-
text: TextSpan(
136-
style: defaultStyle,
137-
children: richLabelSpans,
138-
),
137+
text: TextSpan(style: defaultStyle, children: richLabelSpans),
139138
)
140139
: Text(label, style: defaultStyle);
141140
}
@@ -226,6 +225,15 @@ class SupaEmailAuth extends StatefulWidget {
226225
/// Pre-filled password for the form
227226
final String? prefilledPassword;
228227

228+
/// Whether pressing Enter on the on-screen keyboard should automatically
229+
/// submit the form.
230+
///
231+
/// When set to `false`, the user must explicitly click the submit button
232+
/// to proceed with the authentication process.
233+
///
234+
/// Defaults to `true` for backward compatibility.
235+
final bool enableAutomaticFormSubmission;
236+
229237
/// {@macro supa_email_auth}
230238
const SupaEmailAuth({
231239
super.key,
@@ -248,6 +256,7 @@ class SupaEmailAuth extends StatefulWidget {
248256
this.showConfirmPasswordField = false,
249257
this.prefilledEmail,
250258
this.prefilledPassword,
259+
this.enableAutomaticFormSubmission = true,
251260
});
252261

253262
@override
@@ -276,14 +285,16 @@ class _SupaEmailAuthState extends State<SupaEmailAuth> {
276285
_emailController.text = widget.prefilledEmail ?? '';
277286
_passwordController.text = widget.prefilledPassword ?? '';
278287
_isSigningIn = widget.isInitiallySigningIn;
279-
_metadataControllers = Map.fromEntries((widget.metadataFields ?? []).map(
280-
(metadataField) => MapEntry(
281-
metadataField.key,
282-
metadataField is BooleanMetaDataField
283-
? metadataField.value
284-
: TextEditingController(),
288+
_metadataControllers = Map.fromEntries(
289+
(widget.metadataFields ?? []).map(
290+
(metadataField) => MapEntry(
291+
metadataField.key,
292+
metadataField is BooleanMetaDataField
293+
? metadataField.value
294+
: TextEditingController(),
295+
),
285296
),
286-
));
297+
);
287298
}
288299

289300
@override
@@ -331,7 +342,8 @@ class _SupaEmailAuthState extends State<SupaEmailAuth> {
331342
),
332343
controller: _emailController,
333344
onFieldSubmitted: (_) {
334-
if (_isRecoveringPassword) {
345+
if (_isRecoveringPassword &&
346+
widget.enableAutomaticFormSubmission) {
335347
_passwordRecovery();
336348
}
337349
},
@@ -360,7 +372,8 @@ class _SupaEmailAuthState extends State<SupaEmailAuth> {
360372
obscureText: true,
361373
controller: _passwordController,
362374
onFieldSubmitted: (_) {
363-
if (widget.metadataFields == null || _isSigningIn) {
375+
if ((widget.metadataFields == null || _isSigningIn) &&
376+
widget.enableAutomaticFormSubmission) {
364377
_signInSignUp();
365378
}
366379
},
@@ -385,91 +398,96 @@ class _SupaEmailAuthState extends State<SupaEmailAuth> {
385398
spacer(16),
386399
if (widget.metadataFields != null && !_isSigningIn)
387400
...widget.metadataFields!
388-
.map((metadataField) => [
389-
// Render a Checkbox that displays an error message
390-
// beneath it if the field is required and the user
391-
// hasn't checked it when submitting the form.
392-
if (metadataField is BooleanMetaDataField)
393-
FormField<bool>(
394-
validator: metadataField.isRequired
395-
? (bool? value) {
396-
if (value != true) {
397-
return localization.requiredFieldError;
398-
}
399-
return null;
401+
.map(
402+
(metadataField) => [
403+
// Render a Checkbox that displays an error message
404+
// beneath it if the field is required and the user
405+
// hasn't checked it when submitting the form.
406+
if (metadataField is BooleanMetaDataField)
407+
FormField<bool>(
408+
validator: metadataField.isRequired
409+
? (bool? value) {
410+
if (value != true) {
411+
return localization.requiredFieldError;
400412
}
401-
: null,
402-
builder: (FormFieldState<bool> field) {
403-
final theme = Theme.of(context);
404-
405-
return Column(
406-
crossAxisAlignment: CrossAxisAlignment.start,
407-
children: [
408-
CheckboxListTile(
409-
title:
410-
metadataField.getLabelWidget(context),
411-
value: _metadataControllers[
412-
metadataField.key] as bool,
413-
onChanged: (bool? value) {
414-
setState(() {
415-
_metadataControllers[metadataField
416-
.key] = value ?? false;
417-
});
418-
field.didChange(value);
419-
},
420-
checkboxSemanticLabel:
421-
metadataField.checkboxSemanticLabel,
422-
controlAffinity:
423-
metadataField.checkboxPosition,
424-
contentPadding:
425-
const EdgeInsets.symmetric(
426-
horizontal: 4.0),
413+
return null;
414+
}
415+
: null,
416+
builder: (FormFieldState<bool> field) {
417+
final theme = Theme.of(context);
418+
419+
return Column(
420+
crossAxisAlignment: CrossAxisAlignment.start,
421+
children: [
422+
CheckboxListTile(
423+
title: metadataField.getLabelWidget(
424+
context,
425+
),
426+
value:
427+
_metadataControllers[metadataField.key]
428+
as bool,
429+
onChanged: (bool? value) {
430+
setState(() {
431+
_metadataControllers[
432+
metadataField.key] = value ?? false;
433+
});
434+
field.didChange(value);
435+
},
436+
checkboxSemanticLabel:
437+
metadataField.checkboxSemanticLabel,
438+
controlAffinity:
439+
metadataField.checkboxPosition,
440+
contentPadding: const EdgeInsets.symmetric(
441+
horizontal: 4.0,
427442
),
428-
if (field.hasError)
429-
Padding(
430-
padding: const EdgeInsets.only(
431-
left: 16, top: 4),
432-
child: Text(
433-
field.errorText!,
434-
style: theme.textTheme.labelSmall
435-
?.copyWith(
436-
color: theme.colorScheme.error,
437-
),
443+
),
444+
if (field.hasError)
445+
Padding(
446+
padding: const EdgeInsets.only(
447+
left: 16,
448+
top: 4,
449+
),
450+
child: Text(
451+
field.errorText!,
452+
style: theme.textTheme.labelSmall
453+
?.copyWith(
454+
color: theme.colorScheme.error,
438455
),
439456
),
440-
],
441-
);
442-
},
443-
)
444-
else
445-
// Otherwise render a normal TextFormField matching
446-
// the style of the other fields in the form.
447-
TextFormField(
448-
controller:
449-
_metadataControllers[metadataField.key]
450-
as TextEditingController,
451-
textInputAction:
452-
widget.metadataFields!.last == metadataField
453-
? TextInputAction.done
454-
: TextInputAction.next,
455-
decoration: InputDecoration(
456-
label: Text(metadataField.label),
457-
prefixIcon: metadataField.prefixIcon,
458-
),
459-
validator: metadataField.validator,
460-
autovalidateMode:
461-
AutovalidateMode.onUserInteraction,
462-
onFieldSubmitted: (_) {
463-
if (metadataField !=
464-
widget.metadataFields!.last) {
465-
FocusScope.of(context).nextFocus();
466-
} else {
467-
_signInSignUp();
468-
}
469-
},
457+
),
458+
],
459+
);
460+
},
461+
)
462+
else
463+
// Otherwise render a normal TextFormField matching
464+
// the style of the other fields in the form.
465+
TextFormField(
466+
controller: _metadataControllers[metadataField.key]
467+
as TextEditingController,
468+
textInputAction:
469+
widget.metadataFields!.last == metadataField
470+
? TextInputAction.done
471+
: TextInputAction.next,
472+
decoration: InputDecoration(
473+
label: Text(metadataField.label),
474+
prefixIcon: metadataField.prefixIcon,
470475
),
471-
spacer(16),
472-
])
476+
validator: metadataField.validator,
477+
autovalidateMode:
478+
AutovalidateMode.onUserInteraction,
479+
onFieldSubmitted: (_) {
480+
if (metadataField !=
481+
widget.metadataFields!.last) {
482+
FocusScope.of(context).nextFocus();
483+
} else if (widget.enableAutomaticFormSubmission) {
484+
_signInSignUp();
485+
}
486+
},
487+
),
488+
spacer(16),
489+
],
490+
)
473491
.expand((element) => element),
474492
ElevatedButton(
475493
onPressed: _signInSignUp,
@@ -482,9 +500,11 @@ class _SupaEmailAuthState extends State<SupaEmailAuth> {
482500
strokeWidth: 1.5,
483501
),
484502
)
485-
: Text(_isSigningIn
486-
? localization.signIn
487-
: localization.signUp),
503+
: Text(
504+
_isSigningIn
505+
? localization.signIn
506+
: localization.signUp,
507+
),
488508
),
489509
spacer(16),
490510
if (_isSigningIn) ...[
@@ -508,9 +528,11 @@ class _SupaEmailAuthState extends State<SupaEmailAuth> {
508528
widget.onToggleSignIn?.call(_isSigningIn);
509529
widget.onToggleRecoverPassword?.call(_isRecoveringPassword);
510530
},
511-
child: Text(_isSigningIn
512-
? localization.dontHaveAccount
513-
: localization.haveAccount),
531+
child: Text(
532+
_isSigningIn
533+
? localization.dontHaveAccount
534+
: localization.haveAccount,
535+
),
514536
),
515537
],
516538
if (_isSigningIn && _isRecoveringPassword) ...[
@@ -584,7 +606,8 @@ class _SupaEmailAuthState extends State<SupaEmailAuth> {
584606
} catch (error) {
585607
if (widget.onError == null && mounted) {
586608
context.showErrorSnackBar(
587-
'${widget.localization.unexpectedError}: $error');
609+
'${widget.localization.unexpectedError}: $error',
610+
);
588611
} else {
589612
widget.onError?.call(error);
590613
}
@@ -645,10 +668,15 @@ class _SupaEmailAuthState extends State<SupaEmailAuth> {
645668

646669
/// Resolve the user_metadata coming from the metadataFields
647670
Map<String, dynamic> _resolveMetadataFieldsData() {
648-
return Map.fromEntries(_metadataControllers.entries.map((entry) => MapEntry(
649-
entry.key,
650-
entry.value is TextEditingController
651-
? (entry.value as TextEditingController).text
652-
: entry.value)));
671+
return Map.fromEntries(
672+
_metadataControllers.entries.map(
673+
(entry) => MapEntry(
674+
entry.key,
675+
entry.value is TextEditingController
676+
? (entry.value as TextEditingController).text
677+
: entry.value,
678+
),
679+
),
680+
);
653681
}
654682
}

0 commit comments

Comments
 (0)