问题
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:
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 givestoken
string from response of callback.On application login screen we show
WebBrowser
with "Captcha" page and listening url change event byLinking
methods. On new url, we extract token string from it.Then we create fake
firebase.auth.ApplicationVerifier
object withtoken
and pass it tofirebase.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