2 votes

Est-ce que expo supporte l'authentification téléphonique de Firebase ?

J'essaie d'implémenter l'authentification téléphonique de Firebase pour Expo. J'ai suivi de nombreuses ressources sur Internet mais je n'ai pas réussi. Pourriez-vous me faire savoir si c'est possible/disponible ? Si c'est possible, veuillez partager quelques ressources utiles pour expo.

en vous remerciant pour l'anticipation.

8voto

Rinat Points 398

J'ai eu le même problème, mais j'ai trouvé la solution. Donc, comment ça marche :

  1. Nous avons une page web statique spéciale "Captcha", hébergée sur le domaine, qui est autorisée sur notre projet Firebase. Elle affiche simplement firebase.auth.RecaptchaVerifier . L'utilisateur résout le captcha et cela donne token chaîne de la réponse du callback.

  2. Sur l'écran de connexion de l'application, nous affichons WebBrowser avec la page "Captcha" et écouter l'événement de changement d'url par Linking méthodes. Sur une nouvelle url, nous extrayons la chaîne de caractères du jeton.

  3. Ensuite, nous créons de faux firebase.auth.ApplicationVerifier avec token et le transmettre à firebase.auth().signInWithPhoneNumber (avec numéro de téléphone). Le code SMS sera envoyé.

J'ai écrit le code le plus simple testé ci-dessous. Vous pouvez directement le "copier-coller". Il suffit d'ajouter la configuration de firebase (cette configuration doit être la même pour les deux) et de définir l'url de la page "Captcha". N'oubliez pas que le téléphone doit être saisi au format international. Dans ce code la page "Captcha" est hébergée sur l'hébergement firebase, donc elle s'initialise automatiquement en incluant init.js et autorisé par défaut.

"Page "Captcha (hébergé sur un hébergement firebase) :

<!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>

Ecran d'authentification dans le projet expo :

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>
            )
    }
}

5voto

Damien Buchet Points 51

Voici ma solution, basée sur celle de @Rinat. Le principal problème avec le code précédent est firebase.auth().signInWithPhoneNumber ne se déclenche jamais car il n'est pas dans le webView, et firebase >6.3.3 exige un domaine valide pour l'authentification. J'ai décidé d'utiliser React Native Webview pour faciliter la communication entre WebView et Native.

Côté React-Native

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>

1voto

Emmanuel Orozco Points 109

Ok, la réponse de @Rinat était presque parfaite.

Il y a un problème avec cette fonction dans la page captcha.

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

Cela fonctionne avec iOS (Safary), mais il s'avère que Chrome n'autorise pas

location.href

des URLs personnalisées (nous essayions de rediriger l'utilisateur vers une URL personnalisée, exp://192.12.12.31)

Voici donc la nouvelle fonction :

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";
}

Bien sûr, vous devez ajouter un bouton dans le HTML, afin de pouvoir cliquer dessus.

Voici le code complet :

<!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>

Cela m'a pris presque 7 heures pour trouver la solution, alors j'espère que cela aidera quelqu'un !

Montage après production :

N'oubliez pas d'ajouter "scheme" : "appName" , vers app.json de l'application expo ou le navigateur ne s'ouvre pas à cause du problème de lien profond.

Lire la suite

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

1voto

J'ai fait l'équivalent fonctionnel avec des crochets d'état basés sur la version de Damian Buchet. La page captcha est la même. Le module React natif :

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;

0voto

user3556287 Points 183

Merci pour les solutions Cela a fonctionné pour moi . Mais pour une meilleure solution, nous pouvons sauter le captcha en ajoutant un vérificateur de captcha invisible.

// Côté React Native

     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,
    },
});

// Côté Captcha . J'ai utilisé Firebase Hosting pour héberger ce fichier

<!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>

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X