import * as React from 'react'; import { ActivityIndicator, Button, Modal, SafeAreaView, StyleSheet, Text, View, } from 'react-native'; import { FirebaseAuthApplicationVerifier, FirebaseRecaptcha } from '.'; import { CustomError } from '../types'; interface Props extends Omit< React.ComponentProps, | 'onVerify' | 'invisible' | 'verify' | 'onVerify' | 'onLoad' | 'onError' | 'onFullChallenge' > { title?: string; cancelLabel?: string; attemptInvisibleVerification?: boolean; } interface State { visible: boolean; visibleLoaded: boolean; invisibleLoaded: boolean; invisibleVerify: boolean; invisibleKey: number; resolve?: (token: string) => void; reject?: (error: Error) => void; } export class FirebaseRecaptchaVerifierModal extends React.Component implements FirebaseAuthApplicationVerifier { static defaultProps = { title: 'reCAPTCHA', cancelLabel: 'Cancel', }; state: State = { visible: false, visibleLoaded: false, invisibleLoaded: false, invisibleVerify: false, invisibleKey: 1, resolve: undefined, reject: undefined, }; static getDerivedStateFromProps(props: Props, state: State) { if (!props.attemptInvisibleVerification && state.invisibleLoaded) { return { invisibleLoaded: false, invisibleVerify: false, }; } return null; } get type(): string { return 'recaptcha'; } async verify(): Promise { return new Promise((resolve, reject) => { if (this.props.attemptInvisibleVerification) { this.setState({ invisibleVerify: true, resolve, reject, }); } else { this.setState({ visible: true, visibleLoaded: false, resolve, reject, }); } }); } // see: https://github.com/expo/expo/issues/14780 _reset(...args: any): void {} private onVisibleLoad = () => { this.setState({ visibleLoaded: true, }); }; private onInvisibleLoad = () => { this.setState({ invisibleLoaded: true, }); }; private onFullChallenge = async () => { this.setState({ invisibleVerify: false, visible: true, }); }; private onError = () => { const { reject } = this.state; if (reject) { const err = new Error('Failed to load reCAPTCHA') as CustomError; err['code'] = 'ERR_FIREBASE_RECAPTCHA_ERROR'; reject(err); } this.setState({ visible: false, invisibleVerify: false, }); }; private onVerify = (token: string) => { const { resolve } = this.state; if (resolve) { resolve(token); } this.setState((state) => ({ visible: false, invisibleVerify: false, invisibleLoaded: false, invisibleKey: state.invisibleKey + 1, })); }; cancel = () => { const { reject } = this.state; if (reject) { const err = new Error('Cancelled by user') as CustomError; err['code'] = 'ERR_FIREBASE_RECAPTCHA_CANCEL'; reject(err); } this.setState({ visible: false, }); }; onDismiss = () => { // onDismiss should be called when the user dismisses the // modal using a swipe gesture. Due to a bug in RN this // unfortunately doesn't work :/ //https://github.com/facebook/react-native/issues/26892 if (this.state.visible) { this.cancel(); } }; render() { const { title, cancelLabel, attemptInvisibleVerification, ...otherProps } = this.props; const { visible, visibleLoaded, invisibleLoaded, invisibleVerify, invisibleKey, } = this.state; return ( {attemptInvisibleVerification && ( )} {title}