Phone Authentication with Firebase and BLoC in Flutter

Mohammad Usama
6 min readOct 11, 2023

--

Without further ado, let’s get started.

First, we need some packages for this project:

  1. firebase_auth
  2. firebase_core
  3. flutter_bloc

Next, setup Firebase with your Flutter project, and don’t forget to add an SHA certificate to your Firebase project.

To add SHA certificate fingerprints, navigate to the android directory of your Flutter project and run gradlew signinReport. Make sure gradle and Java SDK are installed; otherwise, it will throw errors.

Now head over to Firebase console on web, In the left panel under project overview, click on project settings, scroll down and in SDK setup and configuration under SHA certificate fingerprints (SHA-1 and SHA-256), which you get with previous running command

Disclaimer: We are working on the Android project.

Create files and directories; your lib directory should look something like this:

Since we are using Cubit, we don’t need events in it.

Start by creating the state of your project.

  1. Create abstract class AuthState
  2. Next AuthInitialState which extends AuthState

Further, our app has AuthLoadingState, AuthCodeSentState, AuthCodeVerifiedState, AuthLoggedInState, AuthLoggedOutState, and AuthErrorState.

For ease, add the following code to the auth_state.dart file:

import 'package:firebase_auth/firebase_auth.dart';

abstract class AuthState {}

class AuthInitianState extends AuthState {}

class AuthLoadingState extends AuthState {}

class AuthCodeSentState extends AuthState {}

class AuthCodeVerifiedState extends AuthState {}

class AuthLoggedInState extends AuthState {
final User firebaseUser;
AuthLoggedInState(this.firebaseUser);
}

class AuthLoggedOutState extends AuthState {}

class AuthErrorState extends AuthState {
final String error;
AuthErrorState(this.error);
}

Class AuthLoggedInState is taking a user, so we can later check if the user is logged in so they can redirect to the home page or sign-in page.

AuthErrorState will display our custom error message.

Let’s create auth_cubit.dart. Our Cubit has 4 methods: sendOTP, verifyOTP, signInWithPhone, and logOut.

Add the following code:

import 'package:bloc_flutter/phone%20auth/cubit/auth_states.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

class AuthCubit extends Cubit<AuthState> {
final FirebaseAuth _firebaseAuth = FirebaseAuth.instance;

AuthCubit() : super(AuthInitianState());

String? verificationID;

void sendOTP(String phoneNumber) async {
emit(AuthLoadingState());

_firebaseAuth.verifyPhoneNumber(
phoneNumber: phoneNumber,
codeSent: (verificationId, forceResendingToken) {
verificationID = verificationId;
emit(AuthCodeSentState());
},
verificationCompleted: (phoneAuthCredential) {
signInWithPhone(phoneAuthCredential);
},
verificationFailed: (error) {
emit(AuthErrorState(error.message.toString()));
},
codeAutoRetrievalTimeout: (verificationId) {
verificationID = verificationId;
},
);
}

void verifyOTP(String otp) async {
emit(AuthLoadingState());

PhoneAuthCredential credential = PhoneAuthProvider.credential(
verificationId: verificationID!, smsCode: otp);
signInWithPhone(credential);
}

void signInWithPhone(AuthCredential credential) async {
try {
UserCredential userCredential =
await _firebaseAuth.signInWithCredential(credential);

if (userCredential.user != null) {
emit(AuthLoggedInState(userCredential.user!));
}
} on FirebaseAuthException catch (ex) {
emit(AuthErrorState(ex.message.toString()));
}
}

void logOut() async {
emit(AuthLoggedOutState());
_firebaseAuth.signOut();
}
}

Under AuthCubit, we first get an instance of FirebaseAuth.

sendOTP

For sending OPT, we need a phone number, which we get in the function parameter. When code enters the sendOTP function, AuthLoadingState is emitted, so we can show a CircularProgressIndicator.

Before sending the OTP, we will verify the phone number. There is a function from FirebaseAuth that requires the following: [phone number], codeSent (after sending code, it will return a verification ID, which will be used for verification later), verificationCompleted (return phoneCrendential), verificationFailed (will emit an AuthErrorState created in AuthStates and pass in the error message), and codeAutoRetrievalTimeout (verificationId).

verifyOTP

It will take the otp entered by the user, emit an AuthLoadingState, and check the opt using the credential function from PhoneAuthProvider.

signInWithPhone

Get AuthCredential, check if the user is not null, and emit AuthLoggedInState. If there is any error, emit AuthErrorState and pass in the error message.

LogOut

Emit AuthLoggedOutState and sign out using a FirebaseAuth instance.

signInScreen

Done with logic, now create a UI for signInScreen:

import 'package:bloc_flutter/phone%20auth/cubit/auth_cubit.dart';
import 'package:bloc_flutter/phone%20auth/cubit/auth_states.dart';
import 'package:bloc_flutter/phone%20auth/screens/verify_phone_number.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

class SignInScreen extends StatelessWidget {
const SignInScreen({super.key});

@override
Widget build(BuildContext context) {
TextEditingController phoneController = TextEditingController();

return Scaffold(
appBar: AppBar(
centerTitle: true,
title: const Text('Sign In with Phone'),
),
body: SafeArea(
child: ListView(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextField(
controller: phoneController,
maxLength: 10,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
border: OutlineInputBorder(),
hintText: 'Phone Number',
counterText: "",
),
),
const SizedBox(
height: 10,
),
BlocConsumer<AuthCubit, AuthState>(
listener: (context, state) {
if (state is AuthCodeSentState) {
Navigator.push(
context,
CupertinoPageRoute(
builder: (context) => const VerifyPhoneNumber(),
),
);
}
},
builder: (context, state) {
if (state is AuthLoadingState) {
return const Center(
child: CircularProgressIndicator(),
);
}

return SizedBox(
width: MediaQuery.of(context).size.width,
child: CupertinoButton(
colour: Colors.blue,
child: const Text('Sign In'),
onPressed: () {
// Replace (+92) with the country code for your country.
String phoneNumber = "+92${phoneController.text}";
BlocProvider.of<AuthCubit>(context)
.sendOTP(phoneNumber);
},
),
);
},
),
],
),
),
],
),
),
);
}
}

A simple Flutter UI with two children in Column

  1. TextField, which take phone number
  2. CupertinoButton, which is Consuming BlocConsumer

Under BlocConsumer in listener, we check if there is an AuthCodeSentState, then navigate to the VerifyPhoneNumber screen.

In builder, we check if this is an AuthLoadingState and return a CircularProgressIndicator; otherwise, it will display our CupertinoButton.

On press, it will call out the sendOTP function and pass in the phone number with the country code.

VerifyPhoneNumber

Create UI for VerifyPhoneNumber

import 'package:bloc_flutter/phone%20auth/cubit/auth_cubit.dart';
import 'package:bloc_flutter/phone%20auth/cubit/auth_states.dart';
import 'package:bloc_flutter/phone%20auth/screens/home_screen.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

class VerifyPhoneNumber extends StatelessWidget {
const VerifyPhoneNumber({super.key});

@override
Widget build(BuildContext context) {
TextEditingController optController = TextEditingController();

return Scaffold(
appBar: AppBar(
centerTitle: true,
title: const Text('Verify Number'),
),
body: SafeArea(
child: ListView(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextField(
controller: optController,
maxLength: 6,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
border: OutlineInputBorder(),
hintText: '6 Digit OTp',
counterText: "",
),
),
const SizedBox(
height: 10,
),
BlocConsumer<AuthCubit, AuthState>(
listener: (context, state) {
if (state is AuthLoggedInState) {
Navigator.popUntil(context, (route) => route.isFirst);

Navigator.pushReplacement(
context,
CupertinoPageRoute(
builder: (context) => const HomeScreen(),
),
);
} else if (state is AuthErrorState) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.error),
duration: const Duration(milliseconds: 600),
backgroundColor: Colors.red,
),
);
}
},
builder: (context, state) {
if (state is AuthLoadingState) {
return const Center(
child: CircularProgressIndicator(),
);
}
return SizedBox(
width: MediaQuery.of(context).size.width,
child: CupertinoButton(
colour: Colors.blue,
child: const Text('Verify'),
onPressed: () {
BlocProvider.of<AuthCubit>(context)
.verifyOTP(optController.text);
},
),
);
},
),
],
),
)
],
)),
);
}
}

Its like the same as previous signInScreen, just a little change. Under listener in BlocConsumer, first we will check if this is AuthLoggedInState, then pop all previous routes and navigate to HomeScreen.

Otherwise, if this is AuthErrorState, it will display the error message in a SnackBar.

When the user taps on verify OTP, it will call the verifyOTP function from AuthCubit and pass in the OTP entered by the user.

HomeScreen

import 'package:bloc_flutter/phone%20auth/cubit/auth_cubit.dart';
import 'package:bloc_flutter/phone%20auth/cubit/auth_states.dart';
import 'package:bloc_flutter/phone%20auth/screens/signin_screen.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: const Text('Home'),
),
body: SafeArea(
child: Center(
child: BlocConsumer<AuthCubit, AuthState>(
listener: (context, state) {
if (state is AuthLoggedOutState) {
Navigator.popUntil(context, (route) => route.isFirst);
Navigator.pushReplacement(
context,
CupertinoPageRoute(
builder: (context) => const SignInScreen(),
),
);
}
},
builder: (context, state) {
return CupertinoButton(
color: Colors.blue,
onPressed: () {
BlocProvider.of<AuthCubit>(context).logOut();
},
child: const Text('Log Out'),
);
},
),
),
),
);
}
}

The HomeScreen has a single button; by pressing the logout function, AuthCubit will call and redirect to the SignInScreen.

Main

import 'package:bloc_flutter/phone%20auth/cubit/auth_cubit.dart';
import 'package:bloc_flutter/phone%20auth/cubit/auth_states.dart';
import 'package:bloc_flutter/phone%20auth/screens/home_screen.dart';
import 'package:bloc_flutter/phone%20auth/screens/signin_screen.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(const MainApp());
}

class MainApp extends StatelessWidget {
const MainApp({super.key});

@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => AuthCubit(),
child: MaterialApp(
debugShowCheckedModeBanner: false,
home: BlocBuilder<AuthCubit, AuthState>(
buildWhen: (previous, current) {
return previous is AuthInitianState;
},
builder: (context, state) {
if (state is AuthLoggedInState) {
return const HomeScreen();
} else if (state is AuthLoggedOutState) {
return const SignInScreen();
} else {
return const SignInScreen();
}
},
),
),
);
}
}

In void main initialise Firebase await Firebase.initializeApp();

Wrap MaterialApp with BlocProvider, extend home: with BlocBuilder and check if this is AuthLoggedInState Navigate to HomeScreen; else, move to SignInScreen.

Booyah!!

This is from my side; if you have any suggestions, feedback, or queries, feel free to reach out.

Follow me on Twitter: codewith-usama

Let’s connect on LinkedIn at (in/codewithusama)

Stackademic

Thank you for reading until the end. Before you go:

  • Please consider clapping and following the writer! 👏
  • Follow us on Twitter(X), LinkedIn, and YouTube.
  • Visit Stackademic.com to find out more about how we are democratizing free programming education around the world.

--

--