AlejoDev
AlejoDev

Reputation: 3202

Issues with Phone Authentication in Ionic/Angular with Capacitor on iOS using @capacitor-firebase/authentication and @angular/fire

I am developing an Ionic/Angular app with Capacitor V6, and I've been struggling to deploy it on iOS. I initially used the @angular/fire library for phone authentication, but encountered issues implementing reCAPTCHA on iOS. While it works fine on the web, I can't pass an ApplicationVerifier object on iOS.

I then explored the @capacitor-firebase/authentication library as an alternative and followed the example in this repository: capacitor-firebase-authentication-demo, which supposedly works with Ionic and Capacitor. However, despite following the instructions, I still can’t get it to work on iOS.

Here are the specific issues I’m facing with @capacitor-firebase/authentication library:

When running the app on the web (using ng serve) and selecting the "Phone" option in the authentication flow, I receive the following error: "recaptchaVerifier must be provided and must be an instance of RecaptchaVerifier." When I open the app in Xcode and run it on an iOS device, the phone authentication doesn’t work either. Has anyone else encountered this issue and successfully implemented phone authentication in an Ionic/Angular app with Capacitor on iOS? I've been trying different solutions for weeks, but still haven’t found one that works.

Many Thanks!

Upvotes: 1

Views: 201

Answers (1)

Zeros-N-Ones
Zeros-N-Ones

Reputation: 1074

You should try the solution below, it handles both web and iOS platforms correctly:

import { Injectable } from '@angular/core';
import { Platform } from '@ionic/angular';
import { FirebaseAuthentication } from '@capacitor-firebase/authentication';
import { initializeApp } from 'firebase/app';
import { 
  getAuth, 
  RecaptchaVerifier, 
  signInWithPhoneNumber,
  ApplicationVerifier 
} from 'firebase/auth';
import { BehaviorSubject } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class PhoneAuthService {
  private recaptchaVerifier: RecaptchaVerifier | null = null;
  private auth = getAuth();
  private confirmationResult: any = null;
  private verificationId = new BehaviorSubject<string>('');

  constructor(private platform: Platform) {}

  async initRecaptcha(buttonId: string) {
    if (this.platform.is('capacitor')) {
      // Native platform (iOS/Android) - no need for web recaptcha
      return;
    }

    // Web platform - initialize recaptcha
    if (!this.recaptchaVerifier) {
      this.recaptchaVerifier = new RecaptchaVerifier(this.auth, buttonId, {
        size: 'invisible',
        callback: () => {
          // reCAPTCHA solved
        },
        'expired-callback': () => {
          // Reset reCAPTCHA
          this.recaptchaVerifier?.render().then((widgetId) => {
            grecaptcha.reset(widgetId);
          });
        }
      });
    }
  }

  async startPhoneAuth(phoneNumber: string, buttonId: string): Promise<void> {
    try {
      if (this.platform.is('capacitor')) {
        // Native platform authentication
        const result = await FirebaseAuthentication.signInWithPhoneNumber({ 
          phoneNumber 
        });
        this.verificationId.next(result.verificationId);
      } else {
        // Web platform authentication
        await this.initRecaptcha(buttonId);
        if (!this.recaptchaVerifier) {
          throw new Error('RecaptchaVerifier not initialized');
        }
        
        this.confirmationResult = await signInWithPhoneNumber(
          this.auth,
          phoneNumber,
          this.recaptchaVerifier as ApplicationVerifier
        );
      }
    } catch (error) {
      console.error('Error starting phone auth:', error);
      throw error;
    }
  }

  async verifyCode(code: string): Promise<any> {
    try {
      if (this.platform.is('capacitor')) {
        // Native platform verification
        const result = await FirebaseAuthentication.signInWithPhoneNumber({
          verificationId: this.verificationId.getValue(),
          verificationCode: code
        });
        return result;
      } else {
        // Web platform verification
        if (!this.confirmationResult) {
          throw new Error('No confirmation result available');
        }
        const result = await this.confirmationResult.confirm(code);
        return result;
      }
    } catch (error) {
      console.error('Error verifying code:', error);
      throw error;
    }
  }

  cleanup() {
    if (this.recaptchaVerifier) {
      this.recaptchaVerifier.clear();
      this.recaptchaVerifier = null;
    }
    this.confirmationResult = null;
    this.verificationId.next('');
  }
}

To use this service, you'll also need to set up your component properly. Here's how to implement it:

import { Component, OnDestroy } from '@angular/core';
import { PhoneAuthService } from './phone-auth.service';

@Component({
  selector: 'app-phone-auth',
  template: `
    <ion-content>
      <form (ngSubmit)="startAuth()">
        <ion-item>
          <ion-label position="floating">Phone Number</ion-label>
          <ion-input type="tel" [(ngModel)]="phoneNumber" name="phone"></ion-input>
        </ion-item>
        
        <ion-button id="sign-in-button" type="submit" expand="block">
          Send Code
        </ion-button>
      </form>

      <form *ngIf="codeSent" (ngSubmit)="verifyCode()">
        <ion-item>
          <ion-label position="floating">Verification Code</ion-label>
          <ion-input type="text" [(ngModel)]="verificationCode" name="code"></ion-input>
        </ion-item>
        
        <ion-button type="submit" expand="block">
          Verify Code
        </ion-button>
      </form>
    </ion-content>
  `
})
export class PhoneAuthComponent implements OnDestroy {
  phoneNumber = '';
  verificationCode = '';
  codeSent = false;

  constructor(private phoneAuthService: PhoneAuthService) {}

  async startAuth() {
    try {
      await this.phoneAuthService.startPhoneAuth(
        this.phoneNumber,
        'sign-in-button'
      );
      this.codeSent = true;
    } catch (error) {
      console.error('Error starting authentication:', error);
      // Handle error appropriately
    }
  }

  async verifyCode() {
    try {
      const result = await this.phoneAuthService.verifyCode(this.verificationCode);
      // Handle successful authentication
      console.log('Successfully authenticated:', result);
    } catch (error) {
      console.error('Error verifying code:', error);
      // Handle error appropriately
    }
  }

  ngOnDestroy() {
    this.phoneAuthService.cleanup();
  }
}

To make it work, you need to ensure your iOS configuration is correct. Follow these steps:

In your capacitor.config.ts:

import { CapacitorConfig } from '@capacitor/cli';

const config: CapacitorConfig = {
  plugins: {
    FirebaseAuthentication: {
      skipNativeAuth: false,
      providers: ['phone']
    }
  }
};

export default config;

In your iOS project, make sure you've:

  • Added the required Firebase pods
  • Configured your GoogleService-Info.plist
  • Updated your AppDelegate.swift to initialize Firebase

Enable Phone Authentication in your Firebase Console:

  • Go to Authentication > Sign-in methods
  • Enable Phone Number sign-in
  • Add your test phone numbers if you're in development

This implementation handles both web and native iOS platforms automatically; uses the appropriate authentication flow for each platform; properly manages the reCAPTCHA verifier on web; cleanly separates the authentication logic into a service; and provides proper error handling and cleanup.

Differences from your original approach:

  • Platform-specific code paths that handle web and iOS differently
  • Proper initialization and cleanup of reCAPTCHA
  • Structured error handling
  • Clear separation of concerns between service and component

Hope this helps!

Upvotes: 0

Related Questions