Search code examples
iosfirebaseionic-frameworkcapacitor

Why is signInWithPhoneNumber from @capacitor-firebase/authentication doesn't works Ionic app for iOS?


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:

  • Phone authentication is enabled in the Firebase Console.
  • The GoogleService-Info.plist file is properly configured and synced with the project.

Environment:

  • Ionic: 8.x
  • Angular: ^18.x
  • @capacitor-firebase/authentication: ^6.3.1
  • Capacitor: 6.x

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:

  1. What could cause signInWithPhoneNumber do not generate any event and Firebase doesn't sends a SMS code?

  2. 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

Solution

  • Verify GoogleService-Info.plist

    • Ensure your GoogleService-Info.plist contains REVERSED_CLIENT_ID.
    • If it’s missing, enable the Google login option in the Firebase Console, download the latest GoogleService-Info.plist, and replace the old one in your project.

    Add 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>
    

    AppDelegate.swift

    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)
    }
    

    capacitor.config.ts

    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;
    

    firebase.service.ts

    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);
      }
    }
    

    app.component.ts

    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
      }
    }