diff --git a/lib/features/authentication/presentation/widgets/password_reset_form.dart b/lib/features/authentication/presentation/widgets/password_reset_form.dart new file mode 100644 index 0000000..d851ce2 --- /dev/null +++ b/lib/features/authentication/presentation/widgets/password_reset_form.dart @@ -0,0 +1,197 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'auth_button.dart'; + +/// Reusable password reset form with email validation and submission +class PasswordResetForm extends StatefulWidget { + final GlobalKey formKey; + final TextEditingController emailController; + final bool isLoading; + final String? errorMessage; + final ValueChanged? onSubmit; + final String emailLabel; + final String submitButtonText; + final bool autofocusEmail; + final String? hintText; + final String? helperText; + + const PasswordResetForm({ + super.key, + required this.formKey, + required this.emailController, + this.isLoading = false, + this.errorMessage, + this.onSubmit, + this.emailLabel = 'Email', + this.submitButtonText = 'Send Reset Email', + this.autofocusEmail = true, + this.hintText, + this.helperText, + }); + + @override + State createState() => _PasswordResetFormState(); +} + +class _PasswordResetFormState extends State { + String? _emailError; + + @override + void initState() { + super.initState(); + // Set initial error from widget + _emailError = widget.errorMessage; + } + + @override + void didUpdateWidget(PasswordResetForm oldWidget) { + super.didUpdateWidget(oldWidget); + // Update error when widget error changes + if (widget.errorMessage != oldWidget.errorMessage) { + _emailError = widget.errorMessage; + } + } + + Future _handleSubmit() async { + if (!widget.formKey.currentState!.validate()) return; + + final email = widget.emailController.text.trim(); + if (widget.onSubmit != null) { + await widget.onSubmit!(email); + } + } + + @override + Widget build(BuildContext context) { + return Form( + key: widget.formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _buildEmailField(), + if (widget.helperText != null) ...[ + const SizedBox(height: 8), + _buildHelperText(), + ], + const SizedBox(height: 24), + _buildSubmitButton(), + ], + ), + ); + } + + Widget _buildEmailField() { + return TextFormField( + controller: widget.emailController, + autofocus: widget.autofocusEmail, + keyboardType: TextInputType.emailAddress, + textInputAction: TextInputAction.done, + onFieldSubmitted: (_) => _handleSubmit(), + decoration: InputDecoration( + labelText: widget.emailLabel, + hintText: widget.hintText ?? 'Enter your email address', + prefixIcon: const Icon(Icons.email_outlined), + suffixIcon: widget.emailController.text.isNotEmpty + ? IconButton( + icon: const Icon(Icons.clear), + onPressed: () { + widget.emailController.clear(); + setState(() { + _emailError = null; + }); + }, + tooltip: 'Clear email', + ) + : null, + errorText: _emailError, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide( + color: _emailError != null + ? Theme.of(context).colorScheme.error + : Theme.of(context).colorScheme.outline, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide( + color: _emailError != null + ? Theme.of(context).colorScheme.error + : Theme.of(context).colorScheme.primary, + width: 2, + ), + ), + ), + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'Email is required'; + } + + final email = value.trim(); + + // Basic email validation regex + final emailRegex = RegExp( + r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', + ); + + if (!emailRegex.hasMatch(email)) { + return 'Please enter a valid email address'; + } + + return null; + }, + onChanged: (value) { + // Clear error when user starts typing + if (_emailError != null) { + setState(() { + _emailError = null; + }); + } + }, + inputFormatters: [ + FilteringTextInputFormatter.deny(RegExp(r'\s')), // Prevent spaces + ], + ); + } + + Widget _buildHelperText() { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.5), + borderRadius: BorderRadius.circular(6), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon( + Icons.info_outline, + size: 16, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + widget.helperText!, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ), + ], + ), + ); + } + + Widget _buildSubmitButton() { + return AuthButton( + text: widget.submitButtonText, + isLoading: widget.isLoading, + onPressed: _handleSubmit, + icon: const Icon(Icons.send_outlined), + ); + } +} \ No newline at end of file