Skip to content

Commit a450e29

Browse files
authored
feat: Improve form interaction and code organization in supa_email_auth component (#106)
* feat: Improve form interaction and code organization in supa_email_auth component - Automatically submit form on Enter key press for email and password fields. - Introduced focus management to ensure email field is focused on validation failure. - Enabled autovalidate mode for email and password fields to provide immediate feedback. - Reorganized button press handlers into separate methods `_signInSignUp` and `_passwordRecovery`. - Improved code readability and maintainability by refactoring and reorganizing logic. Changes: - Added FocusNode for email field. - Implemented onFieldSubmitted for automatic form submission. - Enabled AutovalidateMode.onUserInteraction for immediate validation feedback. - Moved sign-in/sign-up and password recovery logic into dedicated methods. * FIX Usage of BuildContext's across async gaps
1 parent a35cfd3 commit a450e29

File tree

1 file changed

+124
-92
lines changed

1 file changed

+124
-92
lines changed

lib/src/components/supa_email_auth.dart

Lines changed: 124 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,9 @@ class _SupaEmailAuthState extends State<SupaEmailAuth> {
129129
/// Whether the user is signing in or signing up
130130
bool _isSigningIn = true;
131131

132+
/// Focus node for email field
133+
final FocusNode _emailFocusNode = FocusNode();
134+
132135
@override
133136
void initState() {
134137
super.initState();
@@ -158,6 +161,9 @@ class _SupaEmailAuthState extends State<SupaEmailAuth> {
158161
TextFormField(
159162
keyboardType: TextInputType.emailAddress,
160163
autofillHints: const [AutofillHints.email],
164+
autovalidateMode: AutovalidateMode.onUserInteraction,
165+
autofocus: true,
166+
focusNode: _emailFocusNode,
161167
textInputAction: _isRecoveringPassword
162168
? TextInputAction.done
163169
: TextInputAction.next,
@@ -174,13 +180,19 @@ class _SupaEmailAuthState extends State<SupaEmailAuth> {
174180
label: Text(localization.enterEmail),
175181
),
176182
controller: _emailController,
183+
onFieldSubmitted: (_) {
184+
if (_isRecoveringPassword) {
185+
_passwordRecovery();
186+
}
187+
},
177188
),
178189
if (!_isRecoveringPassword) ...[
179190
spacer(16),
180191
TextFormField(
181192
autofillHints: _isSigningIn
182193
? [AutofillHints.password]
183194
: [AutofillHints.newPassword],
195+
autovalidateMode: AutovalidateMode.onUserInteraction,
184196
textInputAction: widget.metadataFields != null && !_isSigningIn
185197
? TextInputAction.next
186198
: TextInputAction.done,
@@ -196,6 +208,11 @@ class _SupaEmailAuthState extends State<SupaEmailAuth> {
196208
),
197209
obscureText: true,
198210
controller: _passwordController,
211+
onFieldSubmitted: (_) {
212+
if (widget.metadataFields == null || _isSigningIn) {
213+
_signInSignUp();
214+
}
215+
},
199216
),
200217
spacer(16),
201218
if (widget.metadataFields != null && !_isSigningIn)
@@ -212,11 +229,20 @@ class _SupaEmailAuthState extends State<SupaEmailAuth> {
212229
prefixIcon: metadataField.prefixIcon,
213230
),
214231
validator: metadataField.validator,
232+
onFieldSubmitted: (_) {
233+
if (metadataField !=
234+
widget.metadataFields!.last) {
235+
FocusScope.of(context).nextFocus();
236+
} else {
237+
_signInSignUp();
238+
}
239+
},
215240
),
216241
spacer(16),
217242
])
218243
.expand((element) => element),
219244
ElevatedButton(
245+
onPressed: _signInSignUp,
220246
child: (_isLoading)
221247
? SizedBox(
222248
height: 16,
@@ -229,64 +255,6 @@ class _SupaEmailAuthState extends State<SupaEmailAuth> {
229255
: Text(_isSigningIn
230256
? localization.signIn
231257
: localization.signUp),
232-
onPressed: () async {
233-
if (!_formKey.currentState!.validate()) {
234-
return;
235-
}
236-
setState(() {
237-
_isLoading = true;
238-
});
239-
try {
240-
if (_isSigningIn) {
241-
final response = await supabase.auth.signInWithPassword(
242-
email: _emailController.text.trim(),
243-
password: _passwordController.text.trim(),
244-
);
245-
widget.onSignInComplete.call(response);
246-
} else {
247-
final user = supabase.auth.currentUser;
248-
late final AuthResponse response;
249-
if (user?.isAnonymous == true) {
250-
await supabase.auth.updateUser(
251-
UserAttributes(
252-
email: _emailController.text.trim(),
253-
password: _passwordController.text.trim(),
254-
data: _resolveData(),
255-
),
256-
emailRedirectTo: widget.redirectTo,
257-
);
258-
final newSession = supabase.auth.currentSession;
259-
response = AuthResponse(session: newSession);
260-
} else {
261-
response = await supabase.auth.signUp(
262-
email: _emailController.text.trim(),
263-
password: _passwordController.text.trim(),
264-
emailRedirectTo: widget.redirectTo,
265-
data: _resolveData(),
266-
);
267-
}
268-
widget.onSignUpComplete.call(response);
269-
}
270-
} on AuthException catch (error) {
271-
if (widget.onError == null && context.mounted) {
272-
context.showErrorSnackBar(error.message);
273-
} else {
274-
widget.onError?.call(error);
275-
}
276-
} catch (error) {
277-
if (widget.onError == null && context.mounted) {
278-
context.showErrorSnackBar(
279-
'${localization.unexpectedError}: $error');
280-
} else {
281-
widget.onError?.call(error);
282-
}
283-
}
284-
if (mounted) {
285-
setState(() {
286-
_isLoading = false;
287-
});
288-
}
289-
},
290258
),
291259
spacer(16),
292260
if (_isSigningIn) ...[
@@ -318,40 +286,7 @@ class _SupaEmailAuthState extends State<SupaEmailAuth> {
318286
if (_isSigningIn && _isRecoveringPassword) ...[
319287
spacer(16),
320288
ElevatedButton(
321-
onPressed: () async {
322-
try {
323-
if (!_formKey.currentState!.validate()) {
324-
return;
325-
}
326-
setState(() {
327-
_isLoading = true;
328-
});
329-
330-
final email = _emailController.text.trim();
331-
await supabase.auth.resetPasswordForEmail(
332-
email,
333-
redirectTo:
334-
widget.resetPasswordRedirectTo ?? widget.redirectTo,
335-
);
336-
widget.onPasswordResetEmailSent?.call();
337-
// FIX use_build_context_synchronously
338-
if (!context.mounted) return;
339-
context.showSnackBar(localization.passwordResetSent);
340-
setState(() {
341-
_isRecoveringPassword = false;
342-
});
343-
} on AuthException catch (error) {
344-
widget.onError?.call(error);
345-
} catch (error) {
346-
widget.onError?.call(error);
347-
} finally {
348-
if (mounted) {
349-
setState(() {
350-
_isLoading = false;
351-
});
352-
}
353-
}
354-
},
289+
onPressed: _passwordRecovery,
355290
child: Text(localization.sendPasswordReset),
356291
),
357292
spacer(16),
@@ -371,6 +306,103 @@ class _SupaEmailAuthState extends State<SupaEmailAuth> {
371306
);
372307
}
373308

309+
void _signInSignUp() async {
310+
if (!_formKey.currentState!.validate()) {
311+
return;
312+
}
313+
setState(() {
314+
_isLoading = true;
315+
});
316+
try {
317+
if (_isSigningIn) {
318+
final response = await supabase.auth.signInWithPassword(
319+
email: _emailController.text.trim(),
320+
password: _passwordController.text.trim(),
321+
);
322+
widget.onSignInComplete.call(response);
323+
} else {
324+
final user = supabase.auth.currentUser;
325+
late final AuthResponse response;
326+
if (user?.isAnonymous == true) {
327+
await supabase.auth.updateUser(
328+
UserAttributes(
329+
email: _emailController.text.trim(),
330+
password: _passwordController.text.trim(),
331+
data: _resolveData(),
332+
),
333+
emailRedirectTo: widget.redirectTo,
334+
);
335+
final newSession = supabase.auth.currentSession;
336+
response = AuthResponse(session: newSession);
337+
} else {
338+
response = await supabase.auth.signUp(
339+
email: _emailController.text.trim(),
340+
password: _passwordController.text.trim(),
341+
emailRedirectTo: widget.redirectTo,
342+
data: _resolveData(),
343+
);
344+
}
345+
widget.onSignUpComplete.call(response);
346+
}
347+
} on AuthException catch (error) {
348+
if (widget.onError == null && mounted) {
349+
context.showErrorSnackBar(error.message);
350+
} else {
351+
widget.onError?.call(error);
352+
}
353+
_emailFocusNode.requestFocus();
354+
} catch (error) {
355+
if (widget.onError == null && mounted) {
356+
context.showErrorSnackBar(
357+
'${widget.localization.unexpectedError}: $error');
358+
} else {
359+
widget.onError?.call(error);
360+
}
361+
_emailFocusNode.requestFocus();
362+
}
363+
if (mounted) {
364+
setState(() {
365+
_isLoading = false;
366+
});
367+
}
368+
}
369+
370+
void _passwordRecovery() async {
371+
try {
372+
if (!_formKey.currentState!.validate()) {
373+
// Focus on email field if validation fails
374+
_emailFocusNode.requestFocus();
375+
return;
376+
}
377+
setState(() {
378+
_isLoading = true;
379+
});
380+
381+
final email = _emailController.text.trim();
382+
await supabase.auth.resetPasswordForEmail(
383+
email,
384+
redirectTo: widget.resetPasswordRedirectTo ?? widget.redirectTo,
385+
);
386+
widget.onPasswordResetEmailSent?.call();
387+
// FIX use_build_context_synchronously
388+
if (!mounted) return;
389+
context.showSnackBar(widget.localization.passwordResetSent);
390+
setState(() {
391+
_isRecoveringPassword = false;
392+
});
393+
} on AuthException catch (error) {
394+
widget.onError?.call(error);
395+
} catch (error) {
396+
widget.onError?.call(error);
397+
} finally {
398+
if (mounted) {
399+
setState(() {
400+
_isLoading = false;
401+
});
402+
}
403+
}
404+
}
405+
374406
/// Resolve the user_metadata that we will send during sign-up
375407
///
376408
/// In case both MetadataFields and extraMetadata have the same

0 commit comments

Comments
 (0)