FlutterFire x Bloc: Phone Authentication
Authenticate user with phone number using Firebase 🔥

Search for a command to run...
Authenticate user with phone number using Firebase 🔥

No comments yet. Be the first to comment.
Lessons from Code, Content, and Consistency

Reliability basics: Outbox pattern + why it matters

Introducing Kafka/Redpanda + move to event-driven workflow

Connect Orders ↔ Inventory (first working service-to-service flow)

Inventory service + gRPC + proto contracts

A basic starter project is shown above. The following dependencies are included in it:
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.2
flutter_bloc: ^8.0.1 # for state management
firebase_core: ^1.12.0 # for firebase
firebase_auth: ^3.3.7 # for firebse authentication
equatable: ^2.0.3
country_code_picker: ^2.0.2 # for picking up different countries with its code

bloc folder contains all the business logic-related files. data folder contains a repository for phone authentication in which we will write methods of verifying the phone number.presentation folder contains 3 screens. In which the auth folder has a phone_auth screen, and the home is just an empty screen that the user will be able to see only after the successful authentication.widget has two files. One for OTP Textfield and the second for Phone Number Textfield.
cd android/
gradlew signingReport


google_services.json file in your project.



void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(const MyApp());
}
bloc \ phone_auth \ phone_auth_event.dart file.This event will be triggered when the user enters the phone number and presses the Send OTP button on the UI. Once Send Otp button is pressed we will send the phone number with the event to the bloc.
class SendOtpToPhoneEvent extends PhoneAuthEvent {
final String phoneNumber;
const SendOtpToPhoneEvent({required this.phoneNumber});
@override
List<Object> get props => [phoneNumber];
}
Once the user requested the OTP after entering the phone number, the OTP field will get appeared on the screen. After the OTP is entered by the user this event will get fired.
class VerifySentOtpEvent extends PhoneAuthEvent {
final String otpCode;
final String verificationId;
const VerifySentOtpEvent(
{required this.otpCode, required this.verificationId});
@override
List<Object> get props => [otpCode, verificationId];
}
After the user request the OTP from the server, the Firebase will send the OTP to the user's phone number. Firebase will send the token, verificationId with the response. This verificationId is then used to log in to the user.
class OnPhoneOtpSent extends PhoneAuthEvent {
final String verificationId;
final int? token;
const OnPhoneOtpSent({
required this.verificationId,
required this.token,
});
@override
List<Object> get props => [verificationId];
}
This event will be triggered when an error occurs while sending the OTP to the user's phone number. This can be due to network issues or firebase's error.
class OnPhoneAuthErrorEvent extends PhoneAuthEvent {
final String error;
const OnPhoneAuthErrorEvent({required this.error});
@override
List<Object> get props => [error];
}
class OnPhoneAuthVerificationCompleteEvent extends PhoneAuthEvent {
final AuthCredential credential;
const OnPhoneAuthVerificationCompleteEvent({
required this.credential,
});
}
bloc \ phone_auth \ phone_auth_state.dart file.class PhoneAuthInitial extends PhoneAuthState {}
This state is used to show the error message.
class PhoneAuthError extends PhoneAuthState {
final String error;
const PhoneAuthError({required this.error});
@override
List<Object> get props => [error];
}
class PhoneAuthVerified extends PhoneAuthState {}
class PhoneAuthCodeSentSuccess extends PhoneAuthState {
final String verificationId;
const PhoneAuthCodeSentSuccess({
required this.verificationId,
});
@override
List<Object> get props => [verificationId];
}
In this file, we will handle all the events triggered during the authentication.
class PhoneAuthBloc extends Bloc<PhoneAuthEvent, PhoneAuthState> {
final PhoneAuthRepository phoneAuthRepository;
final auth = FirebaseAuth.instance;
PhoneAuthBloc({required this.phoneAuthRepository})
: super(PhoneAuthInitial()) {
// When user clicks on send otp button then this event will be fired
on<SendOtpToPhoneEvent>(_onSendOtp);
// After receiving the otp, When user clicks on verify otp button then this event will be fired
on<VerifySentOtpEvent>(_onVerifyOtp);
// When the firebase sends the code to the user's phone, this event will be fired
on<OnPhoneOtpSent>((event, emit) =>
emit(PhoneAuthCodeSentSuccess(verificationId: event.verificationId)));
// When any error occurs while sending otp to the user's phone, this event will be fired
on<OnPhoneAuthErrorEvent>(
(event, emit) => emit(PhoneAuthError(error: event.error)));
// When the otp verification is successful, this event will be fired
on<OnPhoneAuthVerificationCompleteEvent>(_loginWithCredential);
}
}
_onSendOtp.FutureOr<void> _onSendOtp(
SendOtpToPhoneEvent event, Emitter<PhoneAuthState> emit) async {
emit(PhoneAuthLoading());
try {
await phoneAuthRepository.verifyPhone(
phoneNumber: event.phoneNumber,
verificationCompleted: (PhoneAuthCredential credential) async {
// On [verificationComplete], we will get the credential from the firebase and will send it to the [OnPhoneAuthVerificationCompleteEvent] event to be handled by the bloc and then will emit the [PhoneAuthVerified] state after successful login
add(OnPhoneAuthVerificationCompleteEvent(credential: credential));
},
codeSent: (String verificationId, int? resendToken) {
// On [codeSent], we will get the verificationId and the resendToken from the firebase and will send it to the [OnPhoneOtpSent] event to be handled by the bloc and then will emit the [OnPhoneAuthVerificationCompleteEvent] event after receiving the code from the user's phone
add(OnPhoneOtpSent(
verificationId: verificationId, token: resendToken));
},
verificationFailed: (FirebaseAuthException e) {
// On [verificationFailed], we will get the exception from the firebase and will send it to the [OnPhoneAuthErrorEvent] event to be handled by the bloc and then will emit the [PhoneAuthError] state in order to display the error to the user's screen
add(OnPhoneAuthErrorEvent(error: e.code));
},
codeAutoRetrievalTimeout: (String verificationId) {},
);
} catch (e) {
emit(PhoneAuthError(error: e.toString()));
}
}
onVerifyOtp
FutureOr<void> _onVerifyOtp(
VerifySentOtpEvent event, Emitter<PhoneAuthState> emit) async {
try {
emit(PhoneAuthLoading());
// After receiving the otp, we will verify the otp and then will create a credential from the otp and verificationId and then will send it to the [OnPhoneAuthVerificationCompleteEvent] event to be handled by the bloc and then will emit the [PhoneAuthVerified] state after successful login
PhoneAuthCredential credential = PhoneAuthProvider.credential(
verificationId: event.verificationId,
smsCode: event.otpCode,
);
add(OnPhoneAuthVerificationCompleteEvent(credential: credential));
} catch (e) {
emit(PhoneAuthError(error: e.toString()));
}
}
_loginWithCredential
FutureOr<void> _loginWithCredential(
OnPhoneAuthVerificationCompleteEvent event,
Emitter<PhoneAuthState> emit) async {
// After receiving the credential from the event, we will login with the credential and then will emit the [PhoneAuthVerified] state after successful login
try {
await auth.signInWithCredential(event.credential).then((user) {
if (user.user != null) {
emit(PhoneAuthVerified());
}
});
} on FirebaseAuthException catch (e) {
emit(PhoneAuthError(error: e.code));
} catch (e) {
emit(PhoneAuthError(error: e.toString()));
}
}
verifyPhone(). We need to implement this method in the Phone Auth Repository. Let's do it.Head over to lib \ data \ repository \ phone_auth_repository.dart file
class PhoneAuthRepository {
FirebaseAuth auth = FirebaseAuth.instance;
Future<void> verifyPhone({
required String phoneNumber,
required Function(PhoneAuthCredential) verificationCompleted,
required Function(FirebaseAuthException) verificationFailed,
required Function(String, int?) codeSent,
required Function(String) codeAutoRetrievalTimeout,
}) async {
await auth.verifyPhoneNumber(
phoneNumber: phoneNumber,
verificationCompleted: verificationCompleted,
verificationFailed: verificationFailed,
codeSent: codeSent,
codeAutoRetrievalTimeout: codeAutoRetrievalTimeout,
);
}
}
auth which we have initialized with the FirebaseAuth instance has a method called verifyPhoneNumber. phone_auth_bloc file.phone_auth_state.Head over to phone_auth_page.dart and wrap the body of the Scaffold around the BlocBuilder.
@override
Widget build(BuildContext context) {
return Scaffold(
body: BlocListener<PhoneAuthBloc, PhoneAuthState>(
listener: (context, state) {
// If Phone Otp Verified. Send User to Home Screen
if (state is PhoneAuthVerified) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (_) => const HomePage(),
),
);
}
// Show error message if any error occurs while verifying phone number and otp code
if (state is PhoneAuthError) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.error),
),
);
}
},
child: BlocBuilder<PhoneAuthBloc, PhoneAuthState>(
builder: (context, state) {
if (state is PhoneAuthLoading) {
return const Center(
child: CircularProgressIndicator(),
);
}
return Center(
child: Padding(
padding: const EdgeInsets.all(18.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
"Firebase x Flutter: \nPhone Authentication",
style: TextStyle(fontSize: 24),
textAlign: TextAlign.center,
),
const Divider(
height: 30,
endIndent: 20,
indent: 20,
thickness: 1.5,
color: Colors.grey,
),
const SizedBox(
height: 30,
),
// Here we are conditionally rendering the OtpWidget. When the user presses the Send OTP button we will update the PhoneNumberWidget with the OtpWidget. So that user can enter the OTP.
if (state is! PhoneAuthCodeSentSuccess)
PhoneNumberWidget(
phoneNumberController: _phoneNumberController,
)
else
OtpWidget(
codeController: _codeController,
verificationId: state.verificationId,
),
],
),
));
},
),
),
);
}
Now head over to otp_field.dart file. Here we need to implement the _verifyOtp method. This method is called when the user has entered the OTP and presses the Verify button.
void _verifyOtp({required BuildContext context}) {
context.read<PhoneAuthBloc>().add(VerifySentOtpEvent(
otpCode: codeController.text, verificationId: verificationId));
codeController.clear();
}
Also in the phone_number_field.dart file we need to implement _sendOtp function.
void _sendOtp({required String phoneNumber, required BuildContext context}) {
final phoneNumberWithCode = "${_countryCode.dialCode}$phoneNumber";
context.read<PhoneAuthBloc>().add(
SendOtpToPhoneEvent(
phoneNumber: phoneNumberWithCode,
),
);
setState(() {
widget.phoneNumberController.clear();
});
}
In order to provide the PhoneAuthRepository and PhoneAuthBloc we need to wrap the parent with the RepositoryProvider and BlocProvider.
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Firebase Phone Auth Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: RepositoryProvider(
create: (context) => PhoneAuthRepository(),
child: BlocProvider(
create: (context) => PhoneAuthBloc(
phoneAuthRepository:
RepositoryProvider.of<PhoneAuthRepository>(context),
),
child: const PhoneAuthPage(),
),
),
);
}
}

