I am developing an Ionic/Angular application using the @capacitor-firebase/authentication library for user authentication via phone number.
I implemented the following method to sign in with a phone number in my service:
public async signInWithPhoneNumber(options: SignInWithPhoneNumberOptions,
loading: HTMLIonLoadingElement
): Promise<void> {
try {
console.log("Attempting to sign in with phone number:", options.phoneNumber);
const user = await FirebaseAuthentication.signInWithPhoneNumber(options);
console.log('Sign-in successful');
} catch (error) {
console.error('Error during phone number verification:', error);
} finally {
await loading.dismiss();
}
}
My service constructor:
constructor(private readonly ngZone: NgZone) {
FirebaseAuthentication.removeAllListeners().then(() => {
FirebaseAuthentication.addListener('phoneCodeSent', async (event) => {
this.ngZone.run(() => {
console.log("phoneCodeSent"+ event.verificationId);
});
});
FirebaseAuthentication.addListener(
'phoneVerificationCompleted',
async (event) => {
this.ngZone.run(() => {
console.log("phoneVerificationCompleted"+ event.user?.uid);
});
},
);
FirebaseAuthentication.addListener(
'phoneVerificationFailed',
async (event) => {
this.ngZone.run(() => {
console.log("phoneVerificationFailed"+ event.message);
});
},
);
});
}
Expected Behavior
I expect the call to FirebaseAuthentication.signInWithPhoneNumber(options)
Firebase sends a SMS code and the signInWithPhoneNumber(options)
to return an object with user information or a verificationId
, which I can use to verify the SMS code. In summary LET SOME EVENT OCCUR.
Actual Behavior
When running this code on an iOS device, the Xcode console logs the following:
⚡️ [log] - Attempting to sign in with phone number: +573218116768
⚡️ To Native -> FirebaseAuthentication signInWithPhoneNumber 110818811
⚡️ TO JS undefined
⚡️ [log] - Sign-in successful
The response from the native layer (TO JS
) is undefined
, and no error is thrown. Also Firebase doesn't sends a SMS message. Finally in the constructor of my service the listeners for all possible events produced by my method FirebaseAuthentication.signInWithPhoneNumber(options)
are configured but nothing is printed in the console, as if no event occurred
Additional Details
Firebase Configuration:
Environment:
What I’ve Checked:
Firebase configuration is correct (except for APNs, which I didn’t set up because I understand it’s not required for phone sign-in).
The phone number is in the international format.
Capacitor plugins are installed and synced correctly.
Key Questions:
What could cause signInWithPhoneNumber
do not generate any event and Firebase doesn't sends a SMS code?
Is it necessary to configure APNs certificates for signInWithPhoneNumber to work on iOS, even if I’m only using phone authentication?
I would appreciate any guidance or additional debugging steps to help resolve this issue.
Edited: (Additional Info)
App Delegate:
import UIKit
import Capacitor
import FirebaseCore
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
FirebaseApp.configure()
return true
}
func applicationWillResignActive(_ application: UIApplication) {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
}
func applicationDidEnterBackground(_ application: UIApplication) {
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
}
func applicationWillEnterForeground(_ application: UIApplication) {
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
}
func applicationDidBecomeActive(_ application: UIApplication) {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
}
func applicationWillTerminate(_ application: UIApplication) {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
// Called when the app was launched with a url. Feel free to add additional processing here,
// but if you want the App API to support tracking app url opens, make sure to keep this call
return ApplicationDelegateProxy.shared.application(app, open: url, options: options)
}
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
// Called when the app was launched with an activity, including Universal Links.
// Feel free to add additional processing here, but if you want the App API to support
// tracking app url opens, make sure to keep this call
return ApplicationDelegateProxy.shared.application(application, continue: userActivity, restorationHandler: restorationHandler)
}
}
Podfile:
require_relative '../../node_modules/@capacitor/ios/scripts/pods_helpers'
platform :ios, '13.0'
use_frameworks!
# workaround to avoid Xcode caching of Pods that requires
# Product -> Clean Build Folder after new Cordova plugins installed
# Requires CocoaPods 1.6 or newer
install! 'cocoapods', :disable_input_output_paths => true
def capacitor_pods
pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
pod 'CapacitorFirebaseAuthentication', :path => '../../node_modules/@capacitor-firebase/authentication'
pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app'
pod 'CapacitorCamera', :path => '../../node_modules/@capacitor/camera'
pod 'CapacitorHaptics', :path => '../../node_modules/@capacitor/haptics'
pod 'CapacitorKeyboard', :path => '../../node_modules/@capacitor/keyboard'
pod 'CapacitorPreferences', :path => '../../node_modules/@capacitor/preferences'
pod 'CapacitorStatusBar', :path => '../../node_modules/@capacitor/status-bar'
end
target 'App' do
capacitor_pods
# Add your Pods here
end
post_install do |installer|
assertDeploymentTarget(installer)
end
GoogleService-Info.plist
GoogleService-Info.plist
contains REVERSED_CLIENT_ID
.GoogleService-Info.plist
, and replace the old one in your project.CFBundleURLTypes
Register your custom URL scheme by adding the CFBundleURLTypes
key with your REVERSED_CLIENT_ID
in the Info.plist
file:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>YOUR_REVERSED_CLIENT_ID</string>
</array>
</dict>
</array>
verify that this function is included in your app's AppDelegate.swift
import FirebaseAuth
import FirebaseCore
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
FirebaseApp.configure()
ApplicationDelegate.shared.application(application, didFinishLaunchingWithOptions: launchOptions)
return true
}
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
// Called when the app was launched with a url. Feel free to add additional processing here,
// but if you want the App API to support tracking app url opens, make sure to keep this call
if Auth.auth().canHandle(url) {
return true
}
return ApplicationDelegateProxy.shared.application(app, open: url, options: options)
}
Make sure that you have added the phone
option in the providers list under Firebase Authentication.
import { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
appId: 'io.ionic.starter',
appName: 'Ionic App',
webDir: 'dist',
plugins: {
FirebaseAuthentication: {
skipNativeAuth: true,
providers: ["phone"], // make sure you have added this line
},
},
};
export default config;
initialize firebase auth properly for native and web app:
import { Platform } from '@ionic/angular';
import { getAuth, initializeAuth, indexedDBLocalPersistence } from 'firebase/auth';
constructor(private plt: Platform) {
this.initializeApp();
this.firebaseAuthListener();
}
private async initializeApp(): Promise<void> {
const app = initializeApp(environment.firebaseConfig);
this.firebaseAuth = this.firebaseAuthInitialization(app);
}
private firebaseAuthInitialization(app: FirebaseApp) {
if (this.plt.is('capacitor')) {
// use IndexedDB persistence for native platforms
return initializeAuth(app, { persistence: indexedDBLocalPersistence });
} else {
// use default persistence for web platforms
return getAuth(app);
}
}
Here’s the logic for handling phone number login specifically for native devices:
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { FirebaseAuthentication } from "@capacitor-firebase/authentication";
import { getAuth, PhoneAuthProvider, PhoneAuthCredential, signInWithCredential } from "firebase/auth";
@Component({
selector: 'app-root',
imports: [RouterOutlet],
styleUrl: './app.component.scss',
templateUrl: './app.component.html',
})
export class AppComponent {
// +911234567890
phoneNumber!: string; // with county code and '+' icon
verificationId!: string;
constructor(){
FirebaseAuthentication.addListener(
"phoneCodeSent",
async ({ verificationId } ) => {
this.verificationId = verificationId;
await FirebaseAuthentication.removeAllListeners();
}
);
FirebaseAuthentication.addListener(
"phoneVerificationFailed",
async (error: any) => {
console.log(error);
await FirebaseAuthentication.removeAllListeners();
}
);
}
async sendOtp(){
await FirebaseAuthentication.signInWithPhoneNumber({
phoneNumber: this.phoneNumber,
});
}
// verificationCode - 6 digit otp
async verifyOtp(verificationCode: string){
const credential: PhoneAuthCredential = PhoneAuthProvider.credential(
this.verificationId,
verificationCode
);
const auth = getAuth();
const response = await signInWithCredential(auth, credential);
console.log('response: ', response); // here you will get the response
}
}