Login Screen
Clean email + password login with form validation and loading state
View component →A complete forgot-password screen with a validated email field, a send-reset-link flow with loading state, and a built-in success state that confirms the email was sent. It swaps cleanly between the form and the confirmation view inside one widget, so it plugs straight into Firebase Auth's sendPasswordResetEmail or any custom reset endpoint.
Enter your email and we'll send you a reset link.
Back to login
WHAT'S INCLUDED
USE CASES
PRO TIP
Show the same success state whether or not the email actually exists in your system. Revealing 'no account found' lets attackers enumerate which emails are registered; a neutral 'if an account exists, we sent a link' message is the standard secure pattern — and it is exactly what this screen's single success view makes easy.
Copy this entire snippet into your Flutter project. No external packages required.
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
backgroundColor: Colors.white,
body: Center(
child: ForgotPasswordScreen(),
),
),
);
}
}
class ForgotPasswordScreen extends StatefulWidget {
const ForgotPasswordScreen({super.key});
@override
State<ForgotPasswordScreen> createState() => _ForgotPasswordScreenState();
}
class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
bool _isLoading = false;
bool _sent = false;
@override
void dispose() {
_emailController.dispose();
super.dispose();
}
Future<void> _onSend() async {
if (!_formKey.currentState!.validate()) return;
setState(() => _isLoading = true);
await Future.delayed(const Duration(seconds: 2));
if (!mounted) return;
setState(() {
_isLoading = false;
_sent = true;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(elevation: 0, backgroundColor: Colors.transparent),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(24),
child: _sent ? _buildSuccess(context) : _buildForm(context),
),
),
);
}
Widget _buildForm(BuildContext context) {
return Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'Forgot password?',
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 8),
Text(
'Enter your email and we will send you a reset link.',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.grey[600],
),
),
const SizedBox(height: 28),
TextFormField(
controller: _emailController,
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.done,
onFieldSubmitted: (_) => _onSend(),
decoration: const InputDecoration(
labelText: 'Email',
prefixIcon: Icon(Icons.mail_outline),
border: OutlineInputBorder(),
),
validator: (v) {
if (v == null || v.isEmpty) return 'Email is required';
final r = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
if (!r.hasMatch(v)) return 'Enter a valid email';
return null;
},
),
const SizedBox(height: 20),
FilledButton(
onPressed: _isLoading ? null : _onSend,
style: FilledButton.styleFrom(
minimumSize: const Size.fromHeight(52),
),
child: _isLoading
? const SizedBox(
height: 22,
width: 22,
child: CircularProgressIndicator(
strokeWidth: 2.5,
color: Colors.white,
),
)
: const Text('Send reset link',
style: TextStyle(fontSize: 16)),
),
const SizedBox(height: 12),
Center(
child: TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Back to login'),
),
),
],
),
);
}
Widget _buildSuccess(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Container(
width: 88,
height: 88,
alignment: Alignment.center,
decoration: const BoxDecoration(
color: Color(0x331D9E75),
shape: BoxShape.circle,
),
child: const Icon(
Icons.check_rounded,
size: 48,
color: Color(0xFF1D9E75),
),
),
const SizedBox(height: 24),
Text(
'Check your email',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 8),
Text(
'We sent a reset link to \n${_emailController.text}',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.grey[700],
),
),
const SizedBox(height: 28),
FilledButton(
onPressed: () => Navigator.of(context).pop(),
style: FilledButton.styleFrom(
minimumSize: const Size.fromHeight(52),
),
child: const Text('Back to login'),
),
],
);
}
}
Clean email + password login with form validation and loading state
View component →Full name, email, password registration with confirm password validation
View component →6-digit PIN input with auto-focus, auto-advance, and shake on error
View component →