expoでFirebaseRecaptchaを使って電話番号認証する

5 min読了の目安(約5000字TECH技術記事

公式のサンプル動かなかったので動いたコード置いときます

  • ページ間の値の受け渡しはReact Navigationでやってますがよしなに
  • UIライブラリはNativeBase使ってますがよしなに

電話番号入力ページ

import React, { useState } from 'react'
import { useNavigation } from '@react-navigation/native'
import {
  Button,
  Text,
  Form,
  Item,
  Label,
  Input,
  Card,
} from 'native-base'
import parsePhoneNumber from 'libphonenumber-js/mobile'

const PhoneNumber: React.FC = () => {
  const navigation = useNavigation()
  const [phoneNumber, setPhoneNumber] = useState('+81')
  const [errorMessage, setErrorMessage] = useState<string | null>()

  return (
    <>
      <Card>
        <Form>
          <Item last error={!!errorMessage}>
            <Label>電話番号</Label>
            <Input
              autoFocus
              autoCompleteType={'tel'}
              keyboardType={'numeric'}
              value={phoneNumber}
              onChangeText={(text) => {
                setPhoneNumber(text)
              }}
              maxLength={13}
            />
          </Item>
        </Form>
      </Card>
      {errorMessage && <Text>{errorMessage}</Text>}
      <Card>
        <Button
          block
          onPress={() => {
            const _phoneNumber = parsePhoneNumber(phoneNumber, 'JP')
            if (_phoneNumber?.isValid()) {
              navigation.navigate('Recaptcha', {
                phoneNumber: phoneNumber,
              })
              setErrorMessage(null)
            } else {
              setErrorMessage('電話番号を正しく入力してください')
            }
          }}
        >
          <Text>次へ</Text>
        </Button>
      </Card>
    </>
  )
}

export default PhoneNumber

Recaptchaを表示するページ

import React, { useEffect } from 'react'
import { useNavigation } from '@react-navigation/native'
import { useRoute, RouteProp } from '@react-navigation/native'
import {
  FirebaseRecaptcha,
  FirebaseRecaptchaVerifier,
} from 'expo-firebase-recaptcha'
import * as firebase from 'firebase'
import { firebaseConfig } from '../FirebaseConnection'

type Params = {
  phoneNumber: { phoneNumber: string }
}

const Recaptcha: React.FC = () => {
  const phoneNumberRoute = useRoute<RouteProp<Params, 'phoneNumber'>>()
  const phoneNumber = phoneNumberRoute.params.phoneNumber
  const [recaptchaToken, setRecaptchaToken] = React.useState('')
  const navigation = useNavigation()

  useEffect(() => {
    let unmounted = false
    const f = async () => {
      if (phoneNumber && recaptchaToken) {
        const applicationVerifier = new FirebaseRecaptchaVerifier(
          recaptchaToken
        )
        const phoneProvider = new firebase.auth.PhoneAuthProvider()
        const verificationId = await phoneProvider.verifyPhoneNumber(
          phoneNumber,
          applicationVerifier
        )
        navigation.navigate('VerificationCode', {
          verificationId: verificationId,
        })
      }
    }
    f()
    const cleanup = () => {
      unmounted = true
    }
    return cleanup()
  }, [phoneNumber, recaptchaToken])

  return (
    <FirebaseRecaptcha
      firebaseConfig={firebaseConfig}
      onVerify={(token) => {
        setRecaptchaToken(token)
      }}
    />
  )
}

export default Recaptcha

確認コードを入力するページ

import React, { useState } from 'react'
import { useRoute, RouteProp } from '@react-navigation/native'
import { Button, Text, Form, Item, Label, Input, Card } from 'native-base'
import * as firebase from 'firebase'

type Params = {
  verificationId: { verificationId: string }
}

const VerificationCode: React.FC = () => {
  const phoneNumberRoute = useRoute<RouteProp<Params, 'verificationId'>>()
  const verificationId = phoneNumberRoute.params.verificationId
  const [verificationCode, setConfirmationCode] = useState('')
  const [errorMessage, setErrorMessage] = useState<string | null>()

  const logIn = async () => {
    if (!verificationCode) {
      setErrorMessage('コードを正しく入力してください')
      return
    }
    const credential = firebase.auth.PhoneAuthProvider.credential(
      verificationId,
      verificationCode
    )
    try {
      await firebase.auth().signInWithCredential(credential)
      setErrorMessage(null)
    } catch (error) {
      console.log(error)
      setErrorMessage('コードを正しく入力してください')
    }
  }

  return (
    <>
      <Card>
        <Form>
          <Item last error={!!errorMessage}>
            <Label>確認コード</Label>
            <Input
              autoFocus
              keyboardType={'numeric'}
              value={verificationCode}
              onChangeText={(text) => {
                setConfirmationCode(text)
              }}
              maxLength={6}
            />
          </Item>
        </Form>
      </Card>
      {errorMessage && <Text>{errorMessage}</Text>}
      <Card>
        <Button block onPress={() => logIn()}>
          <Text>ログイン</Text>
        </Button>
      </Card>
    </>
  )
}

export default VerificationCode