Step-by-Step Guide to Setting Up Firebase Phone Authentication in Flutter for Android and iOS (APNs, Push Notification and Xcode Setup)

Search for a command to run...

No comments yet. Be the first to comment.
While I generally write about Flutter. This problem has been bugging me for a while and there seem to be limited resources to find a solution. So for anyone who is looking for a solution. Here you go: Type edge:flags into the address bar and open it...
In this tutorial, we’ll go through covering interceptor, which is a very crucial feature of Dio. You can find the entire source code for the project here: dio_tasker Understanding Interceptors: What is an interceptor? An interceptor in Dio is a powe...

In this part, we’ll enhance our Task Manager app by parsing JSON responses and handling different types of errors. We’ll use Dio’s built-in error handling mechanisms. You can find the source code of the entire app here: dio_tasker Parsing JSON Respon...

While building apps in Flutter is extremely fluid and wonderfully intuitive. We obviously know that we can’t build the next TikTok or Twitter by only building offline apps. Which is why we absolutely need internet capabilities in our Flutter apps. Th...

Understanding when to use setState in Flutter is crucial for managing our app’s state effectively. Here’s a detailed guide: When to use setState ? Updating the UI: Use setState when we need to update the UI in response to changes in the internal sta...

Setting up Phone Verification allows various forms of User Credentials.
Let's see how to setup it up. Also make sure that the app is already connected to the firebase. You can read the following article on how to setup Firebase on your app : Flutter on Fire

In our scenario, we'll be selecting kaida:




This is my final sign-in methods:


If you want to learn how to generate SHA-1 and SHA-256 for our app. Make sure to check out the following link: Registering Android App Checkout Step 5 inside the link.
First open the app in Xcode by clicking on iOS folder and then selecting "Open in Xcode" (Similar options on both VSCode as well as Android Studio).

Go to the info tab and click on URL Types

We need couple of url Types:
You can find it under our Runner in GoogleService-Info File:

So let's go ahead and add our Reverse Client Id. Just copy the value and then create an object in URL types:

The next step is to copy the Encoded App ID. We can find it on our Firebase Project Settings:
We are using Encode App ID for reCAPTCHA verification

Now we just have to past the value in our URL Types:

Finally we just have to paste the bundle ID. we can get the Bundle ID from the general tab:

Let's paste it in the URL Types:

Perfect, now with the URL Types added. Let's add APNs and other things to avoid Captcha in real devices.
In order for the OTP to work without Captcha. We need to upload our APN key to our Firebase Project.
So first let's go to our Apple Developer Member Center and the go ahead and click on Keys:

Keys Page:

Click on Create a Key.
And then enable Apple Push Notification Service

After you click continue. You are taken to the new key registration page and just click on register.

Make sure to download your key and store it in a secure place. As it cannot be redownloaded again.

Now that the key has been downloaded. Let's upload it to Firebase console.

Make sure to enter the Key ID as well as the Team ID when you upload the APN key:

Perfect now we can finally see our APN key in our Firebase console

Before our app can start to receive messages. We must explicitly enable "Push Notifications" and "Background Modes" within Xcode
You can do it in Android Studio by clicking on iOS module and then from the menu (Tools --> Flutter --> open iOS / macOS module in Xcode)

So we get Xcode:

So follow the steps below:
Select Project
Select the Project target.
Select the "Signing & Capabilities" tab.

Next the "Push Notifications" capability needs to be added to the project. This can be done via the "Capability" option on the "Signing & Capabilities" tab:
Click on the "+ Capabilities" button.
Search for "Push Notifications"

Once selected, the capability will be shown below the other enabled capabilities. If no option appears when searching, the capability may already be enabled.

Next the "Background Modes" capability needs to be enabled, along with "Background fetch" and "Remote notifications" sub-modes. This can be again added via the "Capability" option on the "Signing & Capabilities: tab:
Click on the "+ Capabilities" button.
Search for "Background Modes".

Once selected we can see the "Background Modes" with other capabilities too.
Now ensure that both the "Background fetch" and the "Remote notifications" sub-modes are enabled:

If you want, you can enable Background processing too.
While we have already added it to our Kaida before. Let's make sure to get the latest version into our app.
flutter pub add firebase_auth
AuthRepository:
I have created an AuthRepository in Kaida. So we are going to add this to our AuthRepository
import 'package:firebase_auth/firebase_auth.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:google_sign_in/google_sign_in.dart';
import 'package:kaida/src/features/auth/domain/models/app_user.dart';
class AuthRepository {
final FirebaseAuth _firebaseAuth;
final FirebaseFirestore _firestore;
AuthRepository(this._firebaseAuth, this._firestore);
// Existing methods...
Future<void> verifyPhoneNumber(
String phoneNumber,
PhoneVerificationCompleted verificationCompleted,
PhoneVerificationFailed verificationFailed,
PhoneCodeSent codeSent,
PhoneCodeAutoRetrievalTimeout codeAutoRetrievalTimeout) async {
await _firebaseAuth.verifyPhoneNumber(
phoneNumber: phoneNumber,
verificationCompleted: verificationCompleted,
verificationFailed: verificationFailed,
codeSent: codeSent,
codeAutoRetrievalTimeout: codeAutoRetrievalTimeout,
);
}
Future<UserCredential> signInWithPhoneNumber(String verificationId, String smsCode) async {
final credential = PhoneAuthProvider.credential(verificationId: verificationId, smsCode: smsCode);
return await _firebaseAuth.signInWithCredential(credential);
}
}
AuthNotifier:
Now let's add the verify phoneNumber() in AuthNotifier
class AuthNotifier extends StateNotifier<AuthState> {
final AuthRepository _authRepository;
AuthNotifier(this._authRepository) : super(const AuthStateInitial());
// Existing methods...
Future<void> verifyPhoneNumber(
String phoneNumber,
PhoneVerificationCompleted verificationCompleted,
PhoneVerificationFailed verificationFailed,
PhoneCodeSent codeSent,
PhoneCodeAutoRetrievalTimeout codeAutoRetrievalTimeout,
) async {
try {
await _authRepository.verifyPhoneNumber(
phoneNumber,
verificationCompleted,
verificationFailed,
codeSent,
codeAutoRetrievalTimeout,
);
} catch (e) {
print('VerifyPhoneNumber Error: $e');
throw e;
}
}
Future<void> signInWithPhoneNumber(String verificationId, String smsCode) async {
try {
await _authRepository.signInWithPhoneNumber(verificationId, smsCode);
} catch (e) {
print('SignInWithPhoneNumber Error: $e');
throw e;
}
}
}
PhoneVerificationPage:
We are creating a basic Phone Verification Page that'll take a code and verify it. Make sure to create separate forms for both phone text form field and verification code text form field
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:kaida/src/features/auth/provider/auth_providers.dart';
import '../../../../constants/routes.dart';
class PhoneVerificationPage extends HookConsumerWidget {
const PhoneVerificationPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final phoneFormKey = useMemoized(() => GlobalKey<FormState>());
final codeFormKey = useMemoized(() => GlobalKey<FormState>());
final phoneController = useTextEditingController();
final codeController = useTextEditingController();
final verificationId = useState<String?>(null);
final errorMessage = useState<String?>(null);
final authNotifier = ref.read(authNotifierProvider.notifier);
String formatPhoneNumber(String phoneNumber) {
if(phoneNumber.startsWith('+1')){
return phoneNumber;
} else {
return '+1$phoneNumber';
}
}
return Scaffold(
appBar: AppBar(
title: const Text('Verify Phone Number'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Form(
key: phoneFormKey,
child: Column(
children: [
TextFormField(
controller: phoneController,
decoration:
const InputDecoration(labelText: 'Phone Number'),
keyboardType: TextInputType.phone,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your phone number';
}
if (!RegExp(r'^\+?\d{10,}$').hasMatch(value)) {
return 'Please enter a valid phone number';
}
return null;
},
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () async {
if (phoneFormKey.currentState?.validate() == true) {
try {
await authNotifier
.verifyPhoneNumber(formatPhoneNumber(phoneController.text.trim()),
(PhoneAuthCredential credential) async {
await FirebaseAuth.instance
.signInWithCredential(credential);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Phone number verified')));
context.go(Routes.profile);
}, (FirebaseAuthException e) {
errorMessage.value = e.toString();
}, (String mVerificationId, int? resendToken) {
verificationId.value = mVerificationId;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Verification code sent')));
}, (String mVerificationId) {
verificationId.value = mVerificationId;
});
} catch (e) {
errorMessage.value = e.toString();
}
}
},
child: const Text('Send Verification Code')),
],
),
),
const SizedBox(height: 20),
if (verificationId.value != null)
Form(
key: codeFormKey,
child: Column(
children: [
TextFormField(
controller: codeController,
decoration:
const InputDecoration(labelText: 'Verification Code'),
keyboardType: TextInputType.number,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter the verification code';
}
return null;
},
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () async {
if (codeFormKey.currentState?.validate() == true) {
try {
await authNotifier.signInWithPhoneNumber(
verificationId.value!, codeController.text);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Phone Number Verified')));
context.go(Routes.profile);
} catch (e) {
errorMessage.value = e.toString();
}
}
},
child: const Text('Verify Code')),
],
),
),
if (errorMessage.value != null)
Padding(
padding: const EdgeInsets.only(top: 20),
child: Text(
errorMessage.value!,
style: const TextStyle(color: Colors.red),
),
),
],
),
),
);
}
}
UserProfilePage:
For now I am just adding a button to go the Phone Verification Page from our existing user_profile_page.dart
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:kaida/src/features/auth/provider/auth_providers.dart';
import 'package:kaida/src/constants/routes.dart';
import 'package:go_router/go_router.dart';
class UserProfilePage extends HookConsumerWidget {
const UserProfilePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final authState = ref.watch(authNotifierProvider);
if (authState is! AuthStateAuthenticated) {
// Redirect to sign-in if not authenticated
WidgetsBinding.instance.addPostFrameCallback((_) {
context.go(Routes.signIn);
});
return const SizedBox.shrink();
}
return Scaffold(
appBar: AppBar(
title: const Text('User Profile'),
actions: [
IconButton(
icon: const Icon(Icons.edit),
onPressed: () {
context.go(Routes.editProfile);
},
),
IconButton(
icon: const Icon(Icons.lock),
onPressed: () {
context.go(Routes.changePassword);
},
),
IconButton(
icon: const Icon(Icons.phone),
onPressed: () {
context.go(Routes.phoneVerification);
},
),
IconButton(
icon: const Icon(Icons.logout),
onPressed: () {
ref.read(authNotifierProvider.notifier).signOut();
},
),
],
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('User ID: ${authState.userId}'),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () {
context.go(Routes.editProfile);
},
child: const Text('Edit Profile'),
),
ElevatedButton(
onPressed: () {
context.go(Routes.changePassword);
},
child: const Text('Change Password'),
),
ElevatedButton(
onPressed: () {
context.go(Routes.phoneVerification);
},
child: const Text('Verify Phone Number'),
),
ElevatedButton(
onPressed: () {
ref.read(authNotifierProvider.notifier).signOut();
},
child: const Text('Logout'),
),
],
),
),
);
}
}
Finally let's test the app. We are going to test it on an iPhone in order for the app to send a code to the user and also to ensure that our verification works on actual device.

Perfect. Finally the app is working as intended. You can find the code here: https://github.com/khkred/kaida