feat(01-09): integrate enhanced auth components into auth pages

Updated login and signup pages:
- Replace inline error displays with AuthForm.formWideError
- Add onErrorDismissed callbacks for better UX
- Maintain consistent error handling across auth flows

Updated password reset form:
- Add form-wide error display matching AuthForm style
- Add field-specific error styling with icons
- Implement auto-clear error when user starts typing
- Add accessibility announcements for error clearing
- Maintain consistent visual hierarchy
This commit is contained in:
Dani B
2026-01-28 12:49:19 -05:00
parent 501951d3bb
commit ec1b7648db
3 changed files with 109 additions and 78 deletions

View File

@@ -98,42 +98,13 @@ class _LoginPageState extends ConsumerState<LoginPage> {
submitButtonText: 'Sign In',
isLoading: _isLoading,
autofocusEmail: true,
formWideError: _errorMessage,
onErrorDismissed: () {
setState(() {
_errorMessage = null;
});
},
),
const SizedBox(height: 16),
// Error message display
if (_errorMessage != null) ...[
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.errorContainer,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: Theme.of(context).colorScheme.error,
width: 1,
),
),
child: Row(
children: [
Icon(
Icons.error_outline,
size: 20,
color: Theme.of(context).colorScheme.error,
),
const SizedBox(width: 8),
Expanded(
child: Text(
_errorMessage!,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onErrorContainer,
),
),
),
],
),
),
const SizedBox(height: 16),
],
// Forgot password link
Align(

View File

@@ -109,42 +109,13 @@ class _SignupPageState extends ConsumerState<SignupPage> {
submitButtonText: 'Create Account',
isLoading: _isLoading,
autofocusEmail: true,
formWideError: _errorMessage,
onErrorDismissed: () {
setState(() {
_errorMessage = null;
});
},
),
const SizedBox(height: 16),
// Error message display
if (_errorMessage != null) ...[
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.errorContainer,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: Theme.of(context).colorScheme.error,
width: 1,
),
),
child: Row(
children: [
Icon(
Icons.error_outline,
size: 20,
color: Theme.of(context).colorScheme.error,
),
const SizedBox(width: 8),
Expanded(
child: Text(
_errorMessage!,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onErrorContainer,
),
),
),
],
),
),
const SizedBox(height: 16),
],
// Terms and conditions checkbox
_buildTermsCheckbox(),

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/semantics.dart';
import 'auth_button.dart';
/// Reusable password reset form with email validation and submission
@@ -37,6 +38,7 @@ class PasswordResetForm extends StatefulWidget {
class _PasswordResetFormState extends State<PasswordResetForm> {
String? _emailError;
String? _lastAnnouncedError;
@override
void initState() {
@@ -70,7 +72,16 @@ class _PasswordResetFormState extends State<PasswordResetForm> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Form-wide error display
if (widget.errorMessage != null) ...[
_buildFormWideError(),
const SizedBox(height: 16),
],
_buildEmailField(),
if (_emailError != null) ...[
const SizedBox(height: 8),
_buildFieldError(_emailError!),
],
if (widget.helperText != null) ...[
const SizedBox(height: 8),
_buildHelperText(),
@@ -146,12 +157,8 @@ class _PasswordResetFormState extends State<PasswordResetForm> {
return null;
},
onChanged: (value) {
// Clear error when user starts typing
if (_emailError != null) {
setState(() {
_emailError = null;
});
}
// Clear errors when user starts typing
_clearErrorsOnUserInput();
// Call external callback if provided
if (widget.onEmailChanged != null) {
widget.onEmailChanged!(value);
@@ -192,6 +199,88 @@ class _PasswordResetFormState extends State<PasswordResetForm> {
);
}
/// Builds form-wide error display
Widget _buildFormWideError() {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.errorContainer,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: Theme.of(context).colorScheme.error,
width: 1,
),
),
child: Row(
children: [
Icon(
Icons.error_outline,
size: 20,
color: Theme.of(context).colorScheme.error,
),
const SizedBox(width: 8),
Expanded(
child: Text(
widget.errorMessage!,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onErrorContainer,
),
),
),
],
),
);
}
/// Builds field-specific error display
Widget _buildFieldError(String error) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.errorContainer.withOpacity(0.5),
borderRadius: BorderRadius.circular(6),
border: Border.left(
color: Theme.of(context).colorScheme.error,
width: 3,
),
),
child: Row(
children: [
Icon(
Icons.info_outline,
size: 16,
color: Theme.of(context).colorScheme.error,
),
const SizedBox(width: 8),
Expanded(
child: Text(
error,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.error,
fontWeight: FontWeight.w500,
),
),
),
],
),
);
}
/// Clears errors when user starts typing
void _clearErrorsOnUserInput() {
bool hadError = _emailError != null || widget.errorMessage != null;
if (hadError && _lastAnnouncedError != widget.errorMessage) {
_announceForAccessibility('Error cleared');
_lastAnnouncedError = widget.errorMessage;
}
}
/// Announces messages for screen readers
void _announceForAccessibility(String message) {
SemanticsService.announce(message, TextDirection.ltr);
}
Widget _buildSubmitButton() {
return AuthButton(
text: widget.submitButtonText,