This blog is divided into two part the first one is logic implementation and second one is ui implementation.Before that let's have a bird's-eye view on this app.
first of all you have to connect your flutter app to the firebase if you are struggling to connect your flutter app to firebase then see this post : CONNECT FLUTTER PROJECT TO FIREBASE
Detailed State Transitions
[App Start] → [Authentication Screen]
↓
[No User] → [Login Screen]
↓
[User Registered] → [Email Verification Screen]
↓
[Email Verified] → [Home Screen]
[App Start] → [Authentication Screen]
↓
[No User] → [Login Screen]
↓
[User Registered] → [Email Verification Screen]
↓
[Email Verified] → [Home Screen]
Technical Flow Diagram
stateDiagram-v2
[*] --> AuthScreen
AuthScreen --> LoginScreen: No User
AuthScreen --> EmailVerificationScreen: User Unverified
AuthScreen --> HomeScreen: User Verified
LoginScreen --> SignUpScreen: New User
SignUpScreen --> EmailVerificationScreen: Registration Complete
EmailVerificationScreen --> HomeScreen: Email Verified
HomeScreen --> LoginScreen: Logout
stateDiagram-v2
[*] --> AuthScreen
AuthScreen --> LoginScreen: No User
AuthScreen --> EmailVerificationScreen: User Unverified
AuthScreen --> HomeScreen: User Verified
LoginScreen --> SignUpScreen: New User
SignUpScreen --> EmailVerificationScreen: Registration Complete
EmailVerificationScreen --> HomeScreen: Email Verified
HomeScreen --> LoginScreen: Logout
Project Structure of applicaiton
Project Folder Layout
emailverifcationtestapp/ │ ├── assets/ │ ├── emailverify.json │ └── loadinginfinity.json │ └── lib/ ├── main.dart ├── authscreen.dart │ └── screens/ ├── emailverify.dart ├── loginscreen.dart ├── signupscreen.dart └── homescreen.dart
To use the assets, make sure your pubspec.yaml
includes:
flutter: assets: - assets/emailverify.json - assets/loadinginfinity.json
#Pubspec.yaml file add these dependencies
firebase_auth: ^5.5.1
firebase_core: ^3.12.1
lottie: ^3.3.1
percent_indicator: ^4.2.4
shared_preferences: ^2.5.2
#Pubspec.yaml file add these dependencies
firebase_auth: ^5.5.1
firebase_core: ^3.12.1
lottie: ^3.3.1
percent_indicator: ^4.2.4
shared_preferences: ^2.5.2
#Androidmanifest.xml add "internet permission"
<uses-permission android:name="android.permission.INTERNET"/>
#Androidmanifest.xml add "internet permission"
<uses-permission android:name="android.permission.INTERNET"/>
Application Flow: Step-by-Step User Journey
1. App Initialization
- Application starts with
main()
function
- Firebase is initialized using
Firebase.initializeApp()
- App launches with
MaterialApp
and Authscreen
as the initial route
main()
functionFirebase.initializeApp()
MaterialApp
and Authscreen
as the initial route2. Authentication State Management
Authscreen
listens to Firebase authentication state changes
- Handles three primary scenarios:
- No user logged in → Redirect to Login Screen
- User logged in but email not verified → Redirect to Email Verification Screen
- User logged in with verified email → Redirect to Home Screen
Authscreen
listens to Firebase authentication state changes- No user logged in → Redirect to Login Screen
- User logged in but email not verified → Redirect to Email Verification Screen
- User logged in with verified email → Redirect to Home Screen
3. User Registration Flow
Sign Up Screen
- User enters details:
- Name
- Email address
- Password
- Confirm password
- Validation checks performed:
- Password and confirm password match
- Email and password not empty
- On successful validation:
- Firebase creates a new user account
- Automatically redirects to Email Verification Screen
- Name
- Email address
- Password
- Confirm password
- Password and confirm password match
- Email and password not empty
- Firebase creates a new user account
- Automatically redirects to Email Verification Screen
Email Verification Screen
- Automatically sends verification email to user
- Implements a 60-second cooldown for resending emails
- Periodic background checks (every 5 seconds) to verify email status
- User can:
- Wait for verification
- Resend verification email
- Go back to login screen
- Wait for verification
- Resend verification email
- Go back to login screen
4. Login Process
- User enters email and password
- Firebase Authentication validates credentials
- Checks email verification status:
- If email verified → Redirect to Home Screen
- If email not verified → Show verification required message
- If email verified → Redirect to Home Screen
- If email not verified → Show verification required message
5. Verification and Navigation Logic
- Email Verification Screen checks user status:
- When email is verified:
- Stop verification timers
- Save verification state in SharedPreferences
- Navigate to Home Screen
- If verification fails:
- Remain on verification screen
- Allow user to resend verification email
- When email is verified:
- Stop verification timers
- Save verification state in SharedPreferences
- Navigate to Home Screen
- If verification fails:
- Remain on verification screen
- Allow user to resend verification email
6. Home Screen Access
- Only accessible after:
- Successful user registration
- Email verification completed
- Login with verified credentials
- Successful user registration
- Email verification completed
- Login with verified credentials
PART I : Flutter Email Verification: Logic Implementation
1. Application Initialization Logic
import 'package:firebase_core/firebase_core.dart';
import 'firebase_options.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
runApp(const MyApp());
}
Firebase Initialization:
- The app ensures that Flutter bindings are initialized using
WidgetsFlutterBinding.ensureInitialized();
. Firebase.initializeApp()
initializes Firebase services, usingDefaultFirebaseOptions.currentPlatform
to load platform-specific configurations.runApp(const MyApp());
starts the Flutter application.
2. Authentication State Management Logic
@override
void initState() {
super.initState();
FirebaseAuth.instance.authStateChanges().listen((User? user) {
if (user == null) {
Navigator.push(context,
MaterialPageRoute(builder: (context) => const Loginscreen()));
} else {
if (!user.emailVerified) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => EmailVerificationScreen(
emailAddress: user.email,
)));
} else {
Navigator.push(context,
MaterialPageRoute(builder: (context) => const Homescreen()));
}
}
});
}
Listening to Authentication Changes:
FirebaseAuth.instance.authStateChanges()
listens for changes in the authentication state (e.g., user logs in, logs out, or the app restarts).- If
user == null
, the app navigates to theLoginScreen
, meaning no user is logged in. - If a user exists but their
emailVerified
property isfalse
, the app navigates to theEmailVerificationScreen
. - If the email is verified, the app navigates to the
HomeScreen
. - This ensures that users cannot access the home screen without verifying their email.
3. User Registration Logic
SignUp Process
bool _isLoading = false;
Future<void> signUpUser(BuildContext context) async {
// Validate inputs
if (passwordController.text != confirmPasswordController.text) {
_showErrorSnackBar(context, 'Passwords do not match');
return;
}
// Validate email and password
if (emailController.text.trim().isEmpty ||
passwordController.text.trim().isEmpty) {
_showErrorSnackBar(context, 'Email and password cannot be empty');
return;
}
setState(() {
_isLoading = true;
});
try {
// Detailed network and authentication logging
debugPrint(
"Attempting to sign up with email: ${emailController.text.trim()}");
// Create user
UserCredential userCredential =
await FirebaseAuth.instance.createUserWithEmailAndPassword(
email: emailController.text.trim(),
password: passwordController.text.trim(),
);
// Log successful user creation
debugPrint("User created successfully: ${userCredential.user?.uid}");
// Navigate to email verification
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => EmailVerificationScreen(
emailAddress: emailController.text.trim(),
),
),
);
} on FirebaseAuthException catch (e) {
// Comprehensive error logging
debugPrint("Firebase Auth Error: ${e.code}");
debugPrint("Firebase Auth Error Message: ${e.message}");
String errorMessage = 'An error occurred';
switch (e.code) {
case 'weak-password':
errorMessage = 'The password provided is too weak.';
break;
case 'email-already-in-use':
errorMessage = 'An account already exists for this email.';
break;
case 'invalid-email':
errorMessage = 'The email address is invalid.';
break;
case 'operation-not-allowed':
errorMessage = 'Email/password accounts are not enabled.';
break;
default:
errorMessage = 'Authentication error: ${e.message}';
}
_showErrorSnackBar(context, errorMessage);
} catch (e) {
// Catch-all error logging
debugPrint("Unexpected error during signup: $e");
_showErrorSnackBar(context, 'An unexpected error occurred: $e');
} finally {
setState(() {
_isLoading = false;
});
}
}
void _showErrorSnackBar(BuildContext context, String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: Colors.red,
),
);
}
Validation Steps:
- Checks if passwords match, showing an error if they don’t.
- Ensures that both email and password fields are not empty.
Creating a Firebase User:
- Calls
createUserWithEmailAndPassword()
to register a user with Firebase Authentication. - If successful, it retrieves
userCredential.user?.uid
and navigates to theEmailVerificationScreen
.
Error Handling:
- Uses
try-catch
to catchFirebaseAuthException
errors such as weak passwords, email already in use, invalid emails, etc. - Shows appropriate error messages using
_showErrorSnackBar()
. - Logs any unexpected errors and resets the loading state in the
finally
block.
Login Process
Future<void> loginUser(BuildContext context) async {
try {
// Sign in with Firebase
UserCredential userCredential =
await FirebaseAuth.instance.signInWithEmailAndPassword(
email: emailController.text.trim(),
password: passwordController.text.trim(),
);
// Check email verification
if (userCredential.user?.emailVerified ?? false) {
// Save verification state
final prefs = await SharedPreferences.getInstance();
await prefs.setBool('isEmailVerified', true);
await prefs.setString('userEmail', userCredential.user?.email ?? '');
// Navigate to Home Screen
Navigator.pushReplacement(context,
MaterialPageRoute(builder: (context) => const Homescreen()));
} else {
// Show verification required message
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Please verify your email first')),
);
}
} on FirebaseAuthException catch (e) {
// Handle specific Firebase authentication errors
String errorMessage = 'Login failed';
if (e.code == 'user-not-found') {
errorMessage = 'No user found for this email.';
} else if (e.code == 'wrong-password') {
errorMessage = 'Incorrect password.';
}
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(errorMessage)),
);
} catch (e) {
// Catch any other errors
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error: $e')),
);
}
}
Authenticating a User:
- Calls
signInWithEmailAndPassword()
to log the user in. - If
emailVerified
istrue
, it saves the verification state inSharedPreferences
and navigates to theHomeScreen
. - If the email is not verified, a message is shown asking the user to verify their email first.
Error Handling:
- Specific errors (e.g., user not found, wrong password) are handled, and appropriate error messages are displayed.
4. Email Verification Logic
late Timer _verificationCheckTimer;
late Timer _resendCooldownTimer;
int _remainingTime = 60;
bool _canResend = false;
bool _isVerifying = false;
@override
void initState() {
super.initState();
sendVerificationEmail();
startResendCooldown();
startPeriodicVerificationCheck();
}
void startPeriodicVerificationCheck() {
_verificationCheckTimer =
Timer.periodic(const Duration(seconds: 5), (timer) async {
await checkEmailVerification();
});
}
void startResendCooldown() {
setState(() {
_canResend = false;
_remainingTime = 60;
});
_resendCooldownTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (_remainingTime > 0) {
setState(() {
_remainingTime--;
});
} else {
setState(() {
_canResend = true;
});
_resendCooldownTimer.cancel();
}
});
}
Future<void> sendVerificationEmail() async {
setState(() {
_isVerifying = true;
});
try {
final user = FirebaseAuth.instance.currentUser;
await user?.sendEmailVerification();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Verification email sent to ${widget.emailAddress}'),
backgroundColor: Colors.green,
),
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Failed to send verification email: $e'),
backgroundColor: Colors.red,
),
);
} finally {
setState(() {
_isVerifying = false;
});
}
}
Future<void> checkEmailVerification() async {
try {
final user = FirebaseAuth.instance.currentUser;
await user?.reload();
if (user?.emailVerified ?? false) {
// Cancel timers
_verificationCheckTimer.cancel();
_resendCooldownTimer.cancel();
// Save verification state
final prefs = await SharedPreferences.getInstance();
await prefs.setBool('isEmailVerified', true);
await prefs.setString('userEmail', user?.email ?? '');
// Navigate to home screen
Navigator.pushReplacement(context,
MaterialPageRoute(builder: (context) => const Homescreen()));
}
} catch (e) {
print('Error checking email verification: $e');
}
}
@override
void dispose() {
_verificationCheckTimer.cancel();
_resendCooldownTimer.cancel();
super.dispose();
}
Timers and Variables
_verificationCheckTimer
is used to periodically check if the user has verified their email._resendCooldownTimer
is used to implement a cooldown period before allowing the user to resend the verification email._remainingTime
tracks how much time is left before the user can resend the email._canResend
determines if the user can resend the email._isVerifying
is used to show a loading state while sending the verification email.
initState() Execution
- Calls
sendVerificationEmail()
when the verification screen is opened. - Starts the
startResendCooldown()
timer to prevent immediate resending of the email. - Starts
startPeriodicVerificationCheck()
, which checks for email verification every 5 seconds.
Email Verification Flow
sendVerificationEmail()
:
- Sends an email verification request using
user?.sendEmailVerification()
. - Displays a success or failure message using
ScaffoldMessenger
.
startPeriodicVerificationCheck()
:
- Runs every 5 seconds using
Timer.periodic()
to check if the user's email is verified. - Calls
checkEmailVerification()
.
checkEmailVerification()
:
- Calls
user?.reload()
to get updated user data. - If
emailVerified
istrue
, it: - Cancels both timers.
- Saves the verification state in
SharedPreferences
. - Navigates to the
HomeScreen
.
Cleanup
dispose()
cancels both timers when the widget is destroyed, preventing memory leaks.
PART II : Flutter Email Verification: Detailed UI Implementation
1. Main Application UI (main.dart)
import 'package:emailverifcationtestapp/AuthScreen.dart';
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'firebase_options.dart';
void main() async {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Email Verification App',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const Authscreen(),
);
}
}
main.dart
file serves as the entry point of the application.MyApp
class defines the main structure of the app and sets up the theme and home screen.Authscreen
widget is set as the home screen, which determines whether the user is logged in and if their email is verified.Firebase.initializeApp()
, just like we discussed in Part I.Authscreen
as the initial screen, which later decides whether to show the login screen, email verification screen, or home screen based on authentication state.2. Authentication Screen
import 'package:emailverifcationtestapp/Screens/EmailVerify.dart';
import 'package:emailverifcationtestapp/Screens/HomeScreen.dart';
import 'package:emailverifcationtestapp/Screens/LoginScreen.dart';
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
class Authscreen extends StatefulWidget {
const Authscreen({super.key});
@override
State<Authscreen> createState() => _AuthscreenState();
}
class _AuthscreenState extends State<Authscreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: const Center(
child: CircularProgressIndicator(),
),
);
}
}
Authscreen
widget determines the user's authentication state.FirebaseAuth.instance.authStateChanges()
to check if a user is logged in.authStateChanges()
) decide the appropriate screen.3. Login Screen
import 'package:emailverifcationtestapp/Screens/HomeScreen.dart';
import 'package:emailverifcationtestapp/Screens/SignUpScreen.dart';
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:shared_preferences/shared_preferences.dart';
class Loginscreen extends StatefulWidget {
const Loginscreen({super.key});
@override
State<Loginscreen> createState() => _LoginscreenState();
}
class _LoginscreenState extends State<Loginscreen> {
final TextEditingController emailController = TextEditingController();
final TextEditingController passwordController = TextEditingController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Login'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: SingleChildScrollView(
child: Column(
children: [
TextField(
controller: emailController,
decoration: const InputDecoration(
labelText: 'Email',
),
keyboardType: TextInputType.emailAddress,
),
TextField(
controller: passwordController,
decoration: const InputDecoration(
labelText: 'Password',
),
obscureText: true,
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () => loginUser(context),
child: const Text('Login'),
),
const SizedBox(height: 10),
TextButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const SignUpScreen(),
),
);
},
child: const Text('Sign Up'),
),
],
),
),
),
);
}
}
- Takes the user's email and password.
- Calls
signInWithEmailAndPassword()
to authenticate the user. - If authentication is successful:
- It checks whether the email is verified.
- If verified, the user is redirected to the Home Screen.
- If not verified, a message prompts the user to verify their email.
SharedPreferences
to remember login sessions.4. Sign Up Screen
import 'package:emailverifcationtestapp/Screens/EmailVerify.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
class SignUpScreen extends StatefulWidget {
const SignUpScreen({super.key});
@override
State<SignUpScreen> createState() => _SignUpScreenState();
}
class _SignUpScreenState extends State<SignUpScreen> {
final TextEditingController emailController = TextEditingController();
final TextEditingController passwordController = TextEditingController();
final TextEditingController confirmPasswordController =
TextEditingController();
final TextEditingController nameController = TextEditingController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Sign Up'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: SingleChildScrollView(
child: Column(
children: [
TextField(
controller: nameController,
decoration: const InputDecoration(
labelText: 'Name',
),
),
TextField(
controller: emailController,
decoration: const InputDecoration(
labelText: 'Email',
),
keyboardType: TextInputType.emailAddress,
),
TextField(
controller: passwordController,
decoration: const InputDecoration(
labelText: 'Password',
),
obscureText: true,
),
TextField(
controller: confirmPasswordController,
decoration: const InputDecoration(
labelText: 'Confirm Password',
),
obscureText: true,
),
const SizedBox(height: 20),
_isLoading
? const CircularProgressIndicator()
: ElevatedButton(
onPressed: () => (),
signUpUser(context),
child: const Text('Sign Up'),
),
],
),
),
),
);
}
}
The Sign-Up Screen collects:
- Name
- Password
- Confirm Password
When the user signs up:Implements the user registration logic covered in Part I. Automatically redirects users to email verification after signup. Uses error handling to display appropriate messages if registration fails.
- It validates input (password match, empty fields).
- Creates a new user using
createUserWithEmailAndPassword()
. - If successful, the user is redirected to the Email Verification Screen.
- An email verification request is sent.
5. Home Screen
import 'package:flutter/material.dart';
class Homescreen extends StatefulWidget {
const Homescreen({super.key});
@override
State<Homescreen> createState() => _HomescreenState();
}
class _HomescreenState extends State<Homescreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
title: const Text('Home'),
),
body: const Center(
child: Text('Welcome to the Home Screen'),
),
);
}
}
6. Email Verification Screen
import 'dart:async';
import 'package:emailverifcationtestapp/Screens/HomeScreen.dart';
import 'package:emailverifcationtestapp/Screens/LoginScreen.dart';
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:lottie/lottie.dart';
import 'package:percent_indicator/percent_indicator.dart';
class EmailVerificationScreen extends StatefulWidget {
final String? emailAddress;
EmailVerificationScreen({Key? key, required this.emailAddress})
: super(key: key);
@override
_EmailVerificationScreenState createState() =>
_EmailVerificationScreenState();
}
class _EmailVerificationScreenState extends State<EmailVerificationScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
automaticallyImplyLeading: false,
title: const Text('Email Verification'),
backgroundColor: Colors.deepPurple,
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Animated Email Verification Lottie Animation
Lottie.asset(
'assets/emailverify.json',
height: 200,
width: 200,
repeat: true,
),
const SizedBox(height: 30),
Text(
'Verify Your Email',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.deepPurple[800],
),
),
const SizedBox(height: 20),
Text(
'A verification email has been sent to\n${widget.emailAddress}',
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.black54,
fontStyle: FontStyle.italic,
),
),
const SizedBox(height: 30),
// Circular Countdown Timer
CircularPercentIndicator(
radius: 60.0,
lineWidth: 10.0,
percent: _remainingTime / 60,
center: Text(
'$_remainingTime',
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
progressColor: _canResend ? Colors.green : Colors.deepPurple,
backgroundColor: Colors.deepPurple.shade100,
),
const SizedBox(height: 20),
// Resend Email Button
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 20),
child: _canResend
? ElevatedButton(
onPressed: _isVerifying
? null
: () {
sendVerificationEmail();
startResendCooldown();
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.deepPurple,
padding: const EdgeInsets.symmetric(
horizontal: 20, vertical: 15),
),
child: _isVerifying
? const CircularProgressIndicator(
color: Colors.white)
: const Text(
'Resend Verification Email',
style: TextStyle(
fontSize: 16,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
)
: Text(
'Resend in $_remainingTime seconds',
style: const TextStyle(
color: Colors.grey,
fontSize: 16,
),
),
),
const SizedBox(height: 20),
// Wrong Email Option
TextButton(
onPressed: () {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => const Loginscreen()));
},
child: Text(
'Wrong email? Go back to Login',
style: TextStyle(
color: Colors.deepPurple[700],
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
),
],
),
),
),
);
}
}
- An animated verification prompt (using Lottie).
- A countdown timer before resending the verification email.
- A "Resend Verification Email" button.
- A "Wrong Email? Go Back to Login" button.
- Sends a verification email upon entry.
- Periodically checks if the user has verified their email.
- If verified, redirects them to the Home Screen.
Conclusion
Finally, we have successfully implemented email verification in our Flutter application! By integrating Firebase Authentication, we've created a robust method to validate user emails and enhance the security of our app. This implementation ensures that only genuine users can access the application's core features. The process we've developed is clean, efficient, and provides a seamless user experience during the registration flow.