How to use Firebase's 'verifyPhoneNumber()' to confirm phone # ownership without using # to sign-in?

As @christos-lytras had in their answer, the verification code is not exposed to your application.

This is done for security reasons as providing the code used for the out of band authentication to the device itself would allow a knowledgeable user to just take the code out of memory and authenticate as if they had access to that phone number.

The general flow of operations is:

  1. Get the phone number to be verified
  2. Use that number with verifyPhoneNumber() and cache the verification ID it returns
  3. Prompt the user to input the code (or automatically retrieve it)
  4. Bundle the ID and the user's input together as a credential using firebase.auth.PhoneAuthProvider.credential(id, code)
  5. Attempt to sign in with that credential using firebase.auth().signInWithCredential(credential)

In your source code, you also use the on(event, observer, errorCb, successCb) listener of the verifyPhoneNumber(phoneNumber) method. However this method also supports listening to results using Promises, which allows you to chain to your Firebase query. This is shown below.

Sending the verification code:

firebase
  .firestore()
  .collection('users')
  .where('phoneNumber', '==', this.state.phoneNumber)
  .get()
  .then((querySnapshot) => {
    if (!querySnapshot.empty) {
      // User found with this phone number.
      throw new Error('already-exists');
    }

    // change status
    this.setState({ status: 'Sending confirmation code...' });

    // send confirmation OTP
    return firebase.auth().verifyPhoneNumber(this.state.phoneNumber)
  })
  .then((phoneAuthSnapshot) => {
    // verification sent
    this.setState({
      status: 'Confirmation code sent.',
      verificationId: phoneAuthSnapshot.verificationId,
      showCodeInput: true // shows input field such as react-native-confirmation-code-field
    });
  })
  .catch((error) => {
    // there was an error
    let newStatus;
    if (error.message === 'already-exists') {
      newStatus = 'Sorry, this phone number is already in use.';
    } else {
      // Other internal error
      // see https://firebase.google.com/docs/reference/js/firebase.firestore.html#firestore-error-code
      // see https://firebase.google.com/docs/reference/js/firebase.auth.PhoneAuthProvider#verify-phone-number
      // probably 'unavailable' or 'deadline-exceeded' for loss of connection while querying users
      newStatus = 'Failed to send verification code.';
      console.log('Unexpected error during firebase operation: ' + JSON.stringify(error));
    }

    this.setState({
      status: newStatus,
      processing: false
    });
  });

Handling a user-sourced verification code:

codeInputSubmitted(code) {
  const { verificationId } = this.state;

  const credential = firebase.auth.PhoneAuthProvider.credential(
    verificationId,
    code
  );

  // To verify phone number without interfering with the existing user
  // who is signed in, we offload the verification to a worker app.
  let fbWorkerApp = firebase.apps.find(app => app.name === 'auth-worker')
                 || firebase.initializeApp(firebase.app().options, 'auth-worker');
  fbWorkerAuth = fbWorkerApp.auth();  
  fbWorkerAuth.setPersistence(firebase.auth.Auth.Persistence.NONE); // disables caching of account credentials

  fbWorkerAuth.signInWithCredential(credential)
    .then((userCredential) => {
      // userCredential.additionalUserInfo.isNewUser may be present
      // userCredential.credential can be used to link to an existing user account

      // successful
      this.setState({
        status: 'Phone number verified!',
        verificationId: null,
        showCodeInput: false,
        user: userCredential.user;
      });

      return fbWorkerAuth.signOut().catch(err => console.error('Ignored sign out error: ', err);
    })
    .catch((err) => {
      // failed
      let userErrorMessage;
      if (error.code === 'auth/invalid-verification-code') {
        userErrorMessage = 'Sorry, that code was incorrect.'
      } else if (error.code === 'auth/user-disabled') {
        userErrorMessage = 'Sorry, this phone number has been blocked.';
      } else {
        // other internal error
        // see https://firebase.google.com/docs/reference/js/firebase.auth.Auth.html#sign-inwith-credential
        userErrorMessage = 'Sorry, we couldn\'t verify that phone number at the moment. '
          + 'Please try again later. '
          + '\n\nIf the issue persists, please contact support.'
      }
      this.setState({
        codeInputErrorMessage: userErrorMessage
      });
    })
}

API References:

  • verifyPhoneNumber() - React Native or Firebase
  • PhoneAuthProvider.credential(id, code) - Firebase
  • signInWithCredential() - React Native or Firebase

Suggested code input component:

  • react-native-confirmation-code-field

Firebase firebase.auth.PhoneAuthProvider won't give you the code for to compare, you'll have to use verificationId to verify the verificationCode that the user enters. There is a basic example in firebase documentation than uses firebase.auth.PhoneAuthProvider.credential and then tries to sign in using these credentials with firebase.auth().signInWithCredential(phoneCredential):

firebase
  .firestore()
  .collection('users')
  .where('phoneNumber', '==', this.state.phoneNumber)
  .get()
  .then((querySnapshot) => {
    if (querySnapshot.empty === true) {
      // change status
      this.setState({ status: 'Sending confirmation code...' });
      // send confirmation OTP
      firebase.auth().verifyPhoneNumber(this.state.phoneNumber).on(
        'state_changed',
        (phoneAuthSnapshot) => {
          switch (phoneAuthSnapshot.state) {
            case firebase.auth.PhoneAuthState.CODE_SENT:
              console.log('Verification code sent', phoneAuthSnapshot);
              // this.setState({ status: 'Confirmation code sent.', confirmationCode: phoneAuthSnapshot.code });

              // Prompt the user the enter the verification code they get and save it to state
              const userVerificationCodeInput = this.state.userVerificationCode;
              const phoneCredentials = firebase.auth.PhoneAuthProvider.credential(
                phoneAuthSnapshot.verificationId, 
                userVerificationCodeInput
              );

              // Try to sign in with the phone credentials
              firebase.auth().signInWithCredential(phoneCredentials)
                .then(userCredentials => {
                  // Sign in successfull
                  // Use userCredentials.user and userCredentials.additionalUserInfo 
                })
                .catch(error => {
                  // Check error code to see the reason
                  // Expect something like:
                  // auth/invalid-verification-code
                  // auth/invalid-verification-id
                });

              break;
            case firebase.auth.PhoneAuthState.ERROR:
              console.log('Verification error: ' + JSON.stringify(phoneAuthSnapshot));
              this.setState({ status: 'Error sending code.', processing: false });
              break;
          }
        },
        (error) => {
          console.log('Error verifying phone number: ' + error);
        }
      );
    }
  })
  .catch((error) => {
    // there was an error
    console.log('Error during firebase operation: ' + JSON.stringify(error));
  });