Does expo support firebase phone auth?

回眸只為那壹抹淺笑 提交于 2019-12-18 13:48:09

问题


I am trying to implement firebase phone auth for expo. I have followed many resources on internet but succeeded. Could you please let me know is it possible/available? if it is possible please share some useful resources for expo

thanking you for anticipation.


回答1:


I had the same problem, but I found the solution. So, how it works:

  1. We have special static "Captcha" web page, hosted on domain, that authorized on our Firebase project. It simply shows firebase.auth.RecaptchaVerifier. User resolves captcha and it gives token string from response of callback.

  2. On application login screen we show WebBrowser with "Captcha" page and listening url change event by Linking methods. On new url, we extract token string from it.

  3. Then we create fake firebase.auth.ApplicationVerifier object with token and pass it to firebase.auth().signInWithPhoneNumber (with phone number). SMS code will be sent.

I wrote tested simplest code below. You can directly "copy-paste" it. Just add firebase config (this config must be same for both) and set correct "Captcha" page url. Don't forget that phone must be entered in international format. In this code "Captcha" page hosted on firebase hosting, so it automatically initializing by including init.js and authorized by default.

"Captcha" page (hosted on firebase hosting):

<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
    <title>Entering captcha</title>
</head>
<body>
    <p style="text-align: center; font-size: 1.2em;">Please, enter captcha for continue<p/>

    <script src="/__/firebase/5.3.1/firebase-app.js"></script>
    <script src="/__/firebase/5.3.1/firebase-auth.js"></script>
    <script src="/__/firebase/init.js"></script>
    <script>

        function getToken(callback) {
            var container = document.createElement('div');
            container.id = 'captcha';
            document.body.appendChild(container);
            var captcha = new firebase.auth.RecaptchaVerifier('captcha', {
                'size': 'normal',
                'callback': function(token) {
                    callback(token);
                },
                'expired-callback': function() {
                    callback('');
                }
            });
            captcha.render().then(function() {
                captcha.verify();
            });
        }

        function sendTokenToApp(token) {
            var baseUri = decodeURIComponent(location.search.replace(/^\?appurl\=/, ''));
            location.href = baseUri + '/?token=' + encodeURIComponent(token);
        }

        document.addEventListener('DOMContentLoaded', function() {
            getToken(sendTokenToApp);
        });

    </script>
</body>
</html>

Auth screen in expo project:

import * as React from 'react'
import {Text, View, ScrollView, TextInput, Button} from 'react-native'
import {Linking, WebBrowser} from 'expo'
import firebase from 'firebase/app'
import 'firebase/auth'

const captchaUrl = `https://my-firebase-hosting/captcha-page.html?appurl=${Linking.makeUrl('')}`

firebase.initializeApp({
    //firebase config
});

export default class App extends React.Component {
    constructor(props) {
        super(props)
        this.state = {
            user: undefined,
            phone: '',
            confirmationResult: undefined,
            code: ''
        }
        firebase.auth().onAuthStateChanged(user => {
            this.setState({user})
        })
    }

    onPhoneChange = (phone) => {
        this.setState({phone})
    }
    onPhoneComplete = async () => {
        let token = null
        const listener = ({url}) => {
            WebBrowser.dismissBrowser()
            const tokenEncoded = Linking.parse(url).queryParams['token']
            if (tokenEncoded)
                token = decodeURIComponent(tokenEncoded)
        }
        Linking.addEventListener('url', listener)
        await WebBrowser.openBrowserAsync(captchaUrl)
        Linking.removeEventListener('url', listener)
        if (token) {
            const {phone} = this.state
            //fake firebase.auth.ApplicationVerifier
            const captchaVerifier = {
                type: 'recaptcha',
                verify: () => Promise.resolve(token)
            }
            try {
                const confirmationResult = await firebase.auth().signInWithPhoneNumber(phone, captchaVerifier)
                this.setState({confirmationResult})
            } catch (e) {
                console.warn(e)
            }

        }
    }
    onCodeChange = (code) => {
        this.setState({code})
    }
    onSignIn = async () => {
        const {confirmationResult, code} = this.state
        try {
            await confirmationResult.confirm(code)
        } catch (e) {
            console.warn(e)
        }
        this.reset()
    }
    onSignOut = async () => {
        try {
            await firebase.auth().signOut()
        } catch (e) {
            console.warn(e)
        }
    }
    reset = () => {
        this.setState({
            phone: '',
            phoneCompleted: false,
            confirmationResult: undefined,
            code: ''
        })
    }

    render() {
        if (this.state.user)
            return (
                <ScrollView style={{padding: 20, marginTop: 20}}>
                    <Text>You signed in</Text>
                    <Button
                        onPress={this.onSignOut}
                        title="Sign out"
                    />
                </ScrollView>
            )

        if (!this.state.confirmationResult)
            return (
                <ScrollView style={{padding: 20, marginTop: 20}}>
                    <TextInput
                        value={this.state.phone}
                        onChangeText={this.onPhoneChange}
                        keyboardType="phone-pad"
                        placeholder="Your phone"
                    />
                    <Button
                        onPress={this.onPhoneComplete}
                        title="Next"
                    />
                </ScrollView>
            )
        else
            return (
                <ScrollView style={{padding: 20, marginTop: 20}}>
                    <TextInput
                        value={this.state.code}
                        onChangeText={this.onCodeChange}
                        keyboardType="numeric"
                        placeholder="Code from SMS"
                    />
                    <Button
                        onPress={this.onSignIn}
                        title="Sign in"
                    />
                </ScrollView>
            )
    }
}



回答2:


Here is my solution, based on @Rinat's one. Main issue with the previous code is firebase.auth().signInWithPhoneNumber never trigger as it's not in the webView, and firebase >6.3.3 requires a valid domain for authentication. I decided to use React Native Webview to make communication between WebView and Native more easy.

React-Native side

import React from 'react'
import { KeyboardAvoidingView  } from 'react-native';
import { TextInput, Button } from 'react-native-paper';
import { WebView } from 'react-native-webview';

import firebase from 'firebase/app';
import 'firebase/auth';

firebase.initializeApp({
    //...your firebase config
});

const captchaUrl = 'https://yourfirebasehosting/captcha.html';

export default class App extends React.Component {
    constructor(props) {
        super(props)
        this.state = {
            phoneNumber: '',
            phoneSubmitted: false,
            promptSmsCode: false,
            smsCode: '',
            smsCodeSubmitted: false
        }
        firebase.auth().onAuthStateChanged(this.onAuthStateChanged);
    }

    onAuthStateChanged = async user => {
        if (user) {
            const token = await firebase.auth().currentUser.getIdToken();
            if (token) {
                // User is fully logged in, with JWT in token variable
            }
        }
    }

    updatePhoneNumber = phoneNumber => this.setState({phoneNumber});
    updateSmsCode = smsCode => this.setState({smsCode});

    onSubmitPhoneNumber = () => this.setState({phoneSubmitted: true});

    onGetMessage = async event => {
        const { phoneNumber } = this.state;
        const message = event.nativeEvent.data;

        switch (message) {
            case "DOMLoaded":
                this.webviewRef.injectJavaScript(`getToken('${phoneNumber}')`);
                return;
            case "ErrorSmsCode":
                // SMS Not sent or Captcha verification failed. You can do whatever you want here
                return;
            case "":
                return;
            default: {
                this.setState({
                    promptSmsCode: true,
                    verificationId: message,
                })
            }
        }
    }

    onSignIn = async () => {
        this.setState({smsCodeSubmitted: true});
        const { smsCode, verificationId } = this.state;
        const credential = firebase.auth.PhoneAuthProvider.credential(verificationId, smsCode);
        firebase.auth().signInWithCredential(credential);
    }

    render() {

        const { phoneSubmitted, phoneNumber, promptSmsCode, smsCode, smsCodeSubmitted } = this.state;

        if (!phoneSubmitted) return (
            <KeyboardAvoidingView style={styles.container} behavior="padding" enabled>
                <TextInput
                    label='Phone Number'
                    value={phoneNumber}
                    onChangeText={this.updatePhoneNumber}
                    mode="outlined"
                />
                <Button mode="contained" onPress={this.onSubmitPhoneNumber}>
                    Send me the code!
                </Button>
            </KeyboardAvoidingView >
        );

        if (!promptSmsCode) return (
            <WebView
                ref={r => (this.webviewRef = r)}
                source={{ uri: captchaUrl }}
                onMessage={this.onGetMessage}
            />
        )

        return (
            <KeyboardAvoidingView style={styles.container} behavior="padding" enabled>
                <TextInput
                    label='Verification code'
                    value={smsCode}
                    onChangeText={this.updateSmsCode}
                    mode="outlined"
                    disabled={smsCodeSubmitted}
                    keyboardType='numeric'
                />
                <Button mode="contained" onPress={this.onSignIn} disabled={smsCodeSubmitted}>
                    Send
                </Button>
            </KeyboardAvoidingView >
        );
    }
}

captcha.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
    <title>Entering captcha</title>
</head>
<body>
    <script src="/__/firebase/6.3.3/firebase-app.js"></script>
    <script src="/__/firebase/6.3.3/firebase-auth.js"></script>
    <script src="/__/firebase/init.js"></script>
    <script>
        function getToken(phoneNumber) {
            var container = document.createElement('div');
            container.id = 'captcha';
            document.body.appendChild(container);
            window.recaptchaVerifier = new firebase.auth.RecaptchaVerifier('captcha', {
                'size': 'normal',
                'callback': function(response) {
                    var appVerifier = window.recaptchaVerifier;
                    firebase.auth().signInWithPhoneNumber(phoneNumber, appVerifier)
                        .then(function (confirmationResult) {
                            window.ReactNativeWebView.postMessage(confirmationResult.verificationId);
                        }).catch(function (error) {
                            window.ReactNativeWebView.postMessage('ErrorSmsCode');
                        });
                }
            });

            window.recaptchaVerifier.render().then(function() {
                window.recaptchaVerifier.verify();
            });
        }

        document.addEventListener('DOMContentLoaded', function() {
            window.ReactNativeWebView.postMessage('DOMLoaded');
        });

    </script>
</body>
</html>



回答3:


Ok @Rinat's answer was almost perfect.

There is problem with this function in the captcha page

function sendTokenToApp(token) {
  var baseUri = decodeURIComponent(location.search.replace(/^\?appurl\=/, ''));
  location.href = baseUri + '/?token=' + encodeURIComponent(token);
}

It works with iOS (Safary), but it turns that Chrome does not allow

location.href

of custom URLs (we were trying to redirect the user to a custom URL, exp://192.12.12.31)

So this is the new function:

function sendTokenToApp(token) {
            var baseUri = decodeURIComponent(location.search.replace(/^\?appurl\=/, ''));
            const finalUrl = location.href = baseUri + '/?token=' + encodeURIComponent(token);
            const continueBtn = document.querySelector('#continue-btn');
            continueBtn.onclick = (event)=>{
                window.open(finalUrl,'_blank')
            }
            continueBtn.style.display = "block";
}

Of course you have to add a button in the HTML, so you can click on it.

This is the full code:

<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
    <title>Entering captcha</title>
</head>
<body>
    <p style="text-align: center; font-size: 1.2em;">Please, enter captcha for continue<p/>
    <button id="continue-btn" style="display:none">Continue to app</button>

    <script src="/__/firebase/5.3.1/firebase-app.js"></script>
    <script src="/__/firebase/5.3.1/firebase-auth.js"></script>
    <script src="/__/firebase/init.js"></script>
    <script>

        function getToken(callback) {
            var container = document.createElement('div');
            container.id = 'captcha';
            document.body.appendChild(container);
            var captcha = new firebase.auth.RecaptchaVerifier('captcha', {
                'size': 'normal',
                'callback': function(token) {
                    callback(token);
                },
                'expired-callback': function() {
                    callback('');
                }
            });
            captcha.render().then(function() {
                captcha.verify();
            });
        }

        function sendTokenToApp(token) {
            var baseUri = decodeURIComponent(location.search.replace(/^\?appurl\=/, ''));
            const finalUrl = location.href = baseUri + '/?token=' + encodeURIComponent(token);
            const continueBtn = document.querySelector('#continue-btn');
            continueBtn.onclick = (event)=>{
                window.open(finalUrl,'_blank')
            }
            continueBtn.style.display = "block";
        }

        document.addEventListener('DOMContentLoaded', function() {
            getToken(sendTokenToApp);
        });

    </script>
</body>
</html>

This took me almost 7 hours to figure it out so I hope it helps someone!

Edit after production:

Don't forget to add "scheme": "appName", to app.json of expo app or browser wont open since deep link problem.

Read this

https://docs.expo.io/versions/latest/workflow/linking#in-a-standalone-app




回答4:


I made the functional equivelant with state hooks based on Damian Buchet version. The captcha page is same. The React native module:

import React, {useState} from 'react'
import {Text, View, TextInput, Button,StyleSheet, KeyboardAvoidingView} from 'react-native'
import { WebView } from 'react-native-webview';
import firebase from '../../components/utils/firebase'  /contains firebase initiation


const captchaUrl = 'https://my-domain.web.app/captcha-page.html';

const LoginScreenPhone = props => {
const [phoneNumber, setPhoneNumber] = useState();
const [step, setStep] = useState('initial');
const [smsCode, setSmsCode] = useState();
const [verificationId, setVerificationId]=useState();

const onAuthStateChanged = async user => {
    if (user) {
        const token = await firebase.auth().currentUser.getIdToken();
        if (token) {
            // User is fully logged in, with JWT in token variable
        }
    }
}

firebase.auth().onAuthStateChanged(onAuthStateChanged);



const onGetMessage = async event => {

    const message = event.nativeEvent.data;
    console.log(message);
    switch (message) {
        case "DOMLoaded":

            return;
        case "ErrorSmsCode":
            // SMS Not sent or Captcha verification failed. You can do whatever you want here
            return;
        case "":
            return;
        default: {
            setStep('promptSmsCode');
            setVerificationId(message);
            }
        }
}


const onSignIn = async () => {
    setStep('smsCodeSubmitted');
    const credential = firebase.auth.PhoneAuthProvider.credential(verificationId, smsCode);
    firebase.auth().signInWithCredential(credential);
    props.navigation.navigate('Home');
}

return (
    <View>
    {step==='initial' && (
        <KeyboardAvoidingView  behavior="padding" enabled>
            <TextInput
                label='Phone Number'
                value={phoneNumber}
                onChangeText={phone =>setPhoneNumber(phone)}
                mode="outlined"
            />
            <Button mode="contained" onPress={()=>setStep('phoneSubmitted')} title=' Send me the code!'>

            </Button>
        </KeyboardAvoidingView >
    )}

  {step==='phoneSubmitted' && (
        <View style={{flex:1, minHeight:800}}>
            <Text>{`getToken('${phoneNumber}')`}</Text>
        <WebView
         injectedJavaScript={`getToken('${phoneNumber}')`}          
            source={{ uri: captchaUrl }}
            onMessage={onGetMessage}
        />
        </View>
    )}

   {step==='promptSmsCode' && (<KeyboardAvoidingView behavior="padding" enabled>
            <TextInput
                label='Verification code'
                value={smsCode}
                onChangeText={(sms)=>setSmsCode(sms)}
                mode="outlined"
                keyboardType='numeric'
            />
            <Button mode="contained" onPress={onSignIn} title='Send'>

            </Button>
    </KeyboardAvoidingView >)}
    </View>
);
}


export default LoginScreenPhone;



回答5:


Hi Thanks For solutions .. Worked for me .. But for more better way we can skip captcha by adding invisible captcha verifier

// React Native Side

     import * as React from 'react'
    import { View, ScrollView, TextInput, Button, StyleSheet, WebView } from 'react-native';
    import { Text } from "galio-framework";
    import { Linking } from 'expo';
    import * as firebase from 'firebase';
    import OTPInputView from '@twotalltotems/react-native-otp-input'
    import theme from '../constants/Theme';

    const captchaUrl = `your firebase host /index.html?appurl=${Linking.makeUrl('')}`

    firebase.initializeApp({
        //firebase config

    });

    export default class PhoneAUth extends React.Component {
        constructor(props) {
            super(props)
            this.state = {
                user: undefined,
                phone: '',
                confirmationResult: undefined,
                code: '',
                isWebView: false
            }
            firebase.auth().onAuthStateChanged(user => {
                this.setState({ user })
            })
        }

    onPhoneChange = (phone) => {
        this.setState({ phone })
    }
    _onNavigationStateChange(webViewState) {
        console.log(webViewState.url)
        this.onPhoneComplete(webViewState.url)
    }
    onPhoneComplete = async (url) => {
        let token = null
        console.log("ok");
        //WebBrowser.dismissBrowser()
        const tokenEncoded = Linking.parse(url).queryParams['token']
        if (tokenEncoded)
            token = decodeURIComponent(tokenEncoded)

        this.verifyCaptchaSendSms(token);


    }
    verifyCaptchaSendSms = async (token) => {
        if (token) {
            const { phone } = this.state
            //fake firebase.auth.ApplicationVerifier
            const captchaVerifier = {
                type: 'recaptcha',
                verify: () => Promise.resolve(token)
            }
            try {
                const confirmationResult = await firebase.auth().signInWithPhoneNumber(phone, captchaVerifier)
                console.log("confirmationResult" + JSON.stringify(confirmationResult));
                this.setState({ confirmationResult, isWebView: false })
            } catch (e) {
                console.warn(e)
            }

        }
    }

    onSignIn = async (code) => {
        const { confirmationResult } = this.state
        try {
            const result = await confirmationResult.confirm(code);
            this.setState({ result });

        } catch (e) {
            console.warn(e)

        }
    }
    onSignOut = async () => {
        try {
            await firebase.auth().signOut()
        } catch (e) {
            console.warn(e)
        }
    }
    reset = () => {
        this.setState({
            phone: '',
            phoneCompleted: false,
            confirmationResult: undefined,
            code: ''
        })
    }

    render() {
    if (this.state.user)
        return (
            <ScrollView style={{padding: 20, marginTop: 20}}>
                <Text>You signed in</Text>
                <Button
                    onPress={this.onSignOut}
                    title="Sign out"
                />
            </ScrollView>
        )
        else if (this.state.isWebView)
            return (
                <WebView
                    ref="webview"
                    source={{ uri: captchaUrl }}
                    onNavigationStateChange={this._onNavigationStateChange.bind(this)}
                    javaScriptEnabled={true}
                    domStorageEnabled={true}
                    injectedJavaScript={this.state.cookie}
                    startInLoadingState={false}
                />

            )
        else if (!this.state.confirmationResult)
            return (
                <ScrollView style={{ padding: 20, marginTop: 20 }}>
                    <TextInput
                        value={this.state.phone}
                        onChangeText={this.onPhoneChange}
                        keyboardType="phone-pad"
                        placeholder="Your phone"
                    />
                    <Button
                        onPress={this.onPhoneComplete}
                        title="Next"
                    />
                </ScrollView>
            )
        else
            return (
                <ScrollView style={{padding: 20, marginTop: 20}}>
                    <TextInput
                        value={this.state.code}
                        onChangeText={this.onCodeChange}
                        keyboardType="numeric"
                        placeholder="Code from SMS"
                    />
                    <Button
                        onPress={this.onSignIn}
                        title="Sign in"
                    />
                </ScrollView>
            )
    }
}
const styles = StyleSheet.create({
    borderStyleBase: {
        width: 30,
        height: 45
    },

    borderStyleHighLighted: {
        borderColor: theme.COLORS.PRIMARY,
    },

    underlineStyleBase: {
        width: 30,
        height: 45,
        borderWidth: 0,
        borderBottomWidth: 1,
    },

    underlineStyleHighLighted: {
        borderColor: theme.COLORS.PRIMARY,
    },
});

// Captcha Side . I used Firebase Hosting to host this file

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>Firebase Phone Authentication</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">

    <script src="https://www.gstatic.com/firebasejs/4.3.1/firebase.js"></script>
    <script>
        // Your web app's Firebase configuration
        var firebaseConfig = {
    // config
        };
        // Initialize Firebase
        firebase.initializeApp(firebaseConfig);
    </script>
    <script src="https://cdn.firebase.com/libs/firebaseui/2.3.0/firebaseui.js"></script>
    <link type="text/css" rel="stylesheet" href="https://cdn.firebase.com/libs/firebaseui/2.3.0/firebaseui.css" />
    <link href="style.css" rel="stylesheet" type="text/css" media="screen" />

</head>

<body>
    <script>

        function getToken(callback) {
            var container = document.createElement('div');
            container.id = 'captcha';
            document.body.appendChild(container);
            var captcha = new firebase.auth.RecaptchaVerifier('captcha', {

        /****************
         I N V I S I B L E  
        **********************/
                'size': 'invisible',
                'callback': function (token) {
                    callback(token);
                },
                'expired-callback': function () {
                    callback('');
                }
            });
            captcha.render().then(function () {
                captcha.verify();
            });
        }

        function sendTokenToApp(token) {
            var baseUri = decodeURIComponent(location.search.replace(/^\?appurl\=/, ''));
            location.href = 'http://www.google.com + '/?token=' + encodeURIComponent(token);
        }

        document.addEventListener('DOMContentLoaded', function () {
            getToken(sendTokenToApp);
        });

    </script>
    <h2>Verification Code is Sending !! </h2>
    <h3>Please Wait !!</h3>

</body>

</html>


来源:https://stackoverflow.com/questions/51218315/does-expo-support-firebase-phone-auth

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!