Merge multiple accounts using #react-native-firebase/auth - firebase

In Firebase Ive set "One account per email address" to true.
This means that when a user has already created a Firebase account using Google sign in....and then they try and also register with Facebook (same email address) then I catch the error with error.code === 'auth/account-exists-with-different-credential'. Alternatively If they try to register using email/password I catch with err.code.toString() === 'auth/email-already-in-use'
Right now I just alert and tell them an account already exists with that email....however I want to link the accounts together instead.
There is so much conflicting info on line about how to do this.....and the Firebase documentation on linking accounts does not appear to work for #react-native-firebase. code below....once I catch the error I want to know what code I can use to link accounts
import auth from '#react-native-firebase/auth';
export const signUpUserWithEmail = (email, password) => {
auth()
.createUserWithEmailAndPassword(email, password)
.then(user => {
console.log(User created in firebase);
})
.catch(err => {
if (err.code.toString() === 'auth/email-already-in-use') {
Alert.alert(
'Email already registered',
'You have already created an account using that email. You have used either Google, Facebook, or email/password as credentials',
);
}
export const _facebookSignin = async () => {
try {
// Attempt login with permissions
const result = await LoginManager.logInWithPermissions([
'public_profile',
'email',
]);
const data = await AccessToken.getCurrentAccessToken();
const firebaseCredential = await auth.FacebookAuthProvider.credential(
data.accessToken,
);
const fbUserObj = await auth().signInWithCredential(firebaseCredential);
} catch (error) {
if (error.code === 'auth/account-exists-with-different-credential') {
Alert.alert(
'Email already registered',
'You have already created an account using that email. You have used either Google, Facebook, or email/password as credentials',
);

Related

How can I log in a user right after his/her email has been verified using firebase/auth and react-native without creating a whole landing page?

Notice: I have seen this question, but creating a whole landing page just to verify a user seems a bit much.
I added a login functionality to my react-native app using firebase/auth with email and password. This works well so far and I have no issues doing that.
I then continued to send a verification email to a new user and only allow him/her to use the app, once the email is verified. Again, no issues here.
The next step would be to login the user right after the email was verified. This is where I'm stuck, since the onAuthStateChanged eventhandler doesn't update after the user pressed the verification link in the email.
Is there any way to listen to the emailVerified state in real-time? I tried to use polling with setInterval() but this is not great since there is a notable delay between verification and login. I read about a continueLink you can pass to sendEmailVerification, but I couldn't figure out how to make that work in react-native.
I'm using Expo and therefore the Firebase SDK, not the Firebase react native package.
Here is the code I use for the signup:
export const signUp = async (username: string, email: string, password: string) => {
try {
const auth = getAuth();
if (email && password && username) {
// sign up
const userCredential = await createUserWithEmailAndPassword(auth, email, password);
// save username in firestore
await setUserName(userCredential, username);
// send Email Verification
await sendEmailVerification(userCredential.user);
return true;
}
} catch (error) {
onError(error);
}
};
And this is my onAuthStateChanged handler:
auth.onAuthStateChanged(authenticatedUser => {
try {
if (authenticatedUser?.emailVerified) {
setUser(authenticatedUser)
} else {
setUser(null)
}
} catch (error) {
console.log(error);
}
});
So in the end I did follow this question, but I changed it a bit to fit my needs. I'll post my steps for anyone who's doing the same.
Create a simple static website with firebase init and host it on firebase or somewhere else (check the hosting tab in your firebase console to get started)
Follow this guide to create the appropriate handlers on the website
Add the following to your verificationHandler to update the user (don't forget to import firestore) (I send the userId via the continueURL, but there are probably better ways)
// You can also use realtime database if you want
firebase.firestore().collection("users").doc(userId).set({
emailVerified: true
}, {merge: true}).then(() => {
message.textContent = "Your email has been verified.";
}).catch((error) => {
message.textContent = "The verification was invalid or is expired. Please try to send another verification email from within the app.";
});
Got to authentication -> templates in your firebase console and change the action url to your hosted website's url
Add a listener to the firestore doc to your react-native app
const onUserDataChanged = (uid, callback) => {
onSnapshot(doc(firestore, "users", uid), doc => callback(doc.data()));
}
Use the data from the callback to update the login state in the app
// As an example
auth.onAuthStateChanged(authenticatedUser => {
if (authenticatedUser && !authenticatedUser.emailVerified) {
unsubscribeFirestoreListener?.();
unsubscribeFirestoreListener = onUserDataChanged(authenticatedUser.uid, (data: any) => {
if (data?.emailVerified) {
setUser(authenticatedUser);
unsubscribeFirestoreListener?.();
}
});
}
}
use the codes below for your authentication context. for user id, you should use 'user.uid'
import React, { useState, createContext } from "react";
import * as firebase from "firebase";
import { loginRequest } from "./authentication.service";
export const AuthenticationContext = createContext();
export const AuthenticationContextProvider = ({ children }) => {
const [isLoading, setIsLoading] = useState(false);
const [user, setUser] = useState(null);
const [error, setError] = useState(null);
firebase.auth().onAuthStateChanged((usr) => {
if (usr) {
setUser(usr);
setIsLoading(false);
} else {
setIsLoading(false);
}
});
const onLogin = (email, password) => {
setIsLoading(true);
firebase.auth().signInWithEmailAndPassword(email, password)
.then((u) => {
setUser(u);
setIsLoading(false);
})
.catch((e) => {
setIsLoading(false);
setError(e.toString());
});
};
const onRegister = (email, password, repeatedPassword) => {
setIsLoading(true);
if (password !== repeatedPassword) {
setError("Error: Passwords do not match");
return;
}
firebase
.auth()
.createUserWithEmailAndPassword(email, password)
.then((u) => {
setUser(u);
setIsLoading(false);
})
.catch((e) => {
setIsLoading(false);
setError(e.toString());
});
};
const onLogout = () => {
setUser(null);
firebase.auth().signOut();
};
return (
<AuthenticationContext.Provider
value={{
isAuthenticated: !!user,
user,
isLoading,
error,
onLogin,
onRegister,
onLogout,
}}
>
{children}
</AuthenticationContext.Provider>
);
};

React Native: firebase auth/network-request-failed error when trying to create a new user, but not on login?

So I have a simple React Native application where I added firebase into. I've added a firestore and setup Authorization on my firebase account. When trying to login with the set account, the login succeeds using the following code:
const onButtonPress = () => {
setErrorMessage(null);
const auth = getAuth();
signInWithEmailAndPassword(auth, email, password)
.then((res) => {
navigation.navigate('Main'); // When going to the same Stack.Screen you're already on, it won't do anything
})
.catch((error) => {
setErrorMessage('Error: ' + error.message);
});
}
However, when I try to register in the same manner, it fails with the following error:
Firebase: Error (auth/network-request-failed)
This is my register code:
const onButtonPress = () => {
if (!email || !password || !passwordRepeat) {
setErrorMessage('Please fill in the entire form');
} else if (password === passwordRepeat) {
const auth = getAuth();
createUserWithEmailAndPassword(auth, email, password)
.then((res) => {
setErrorMessage(null);
navigation.navigate('Main');
})
.catch((error) => {
setErrorMessage('Error: ' + error.message);
});
} else {
setErrorMessage('Error: passwords don\'t match');
}
}
The "funny" thing is, when I try to login with a wrong account/password combination with my login-code, it gives the same error.
So since login works, but not with wrong account/pw combination and getting the same error as Register, I'm guessing my database setup etc is correct (since I can login), but during the create-account, something goes wrong?
Does anyone know what could be causing this and how I can fix it so that I can create new users in my app?
EDIT: I'm also using Expo in this application and using "expo start" to test it on my personal Android device.
I was facing the same issue up until now, turns out I using onChange in the instead of onChangeText, and using it to set the state for the email and password, which in turn was setting the email and password as an object instead of a string.
Example:
<TextInput
placeholder="Password"
value={password}
onChange={newText => {setPassword(newText)} //This should be onChangeText
placeholderTextColor="black"
style={styles.textInput}
secureTextEntry
/>
Try to console.log() the values for your email and password before creating the user, perhaps you're doing something similar.

Handle facebook login with same account used with Google using firebase

I'm working on a react native project and I've came to a part where initially I implemented google sign in my project using react-native-google-signin and later on Facebook sign in using react-native-fbsdk packages with the help of firebase and both worked like a charm "individually".
The Problem
Let's say the user logged in using google account and it worked but later logged in using Facebook with the same account (I'm allowing only one email per user in firebase), I get an error
auth/account-exists-with-different-credentials
I want the user to be able to login using Facebook from the login screen or to be more specific to link his account from the login screen.
What have I tried?
I searched online and found some answers and got up with this solution or piece of code:
facebookSignin: async () => {
const result = await LoginManager.logInWithPermissions([
'public_profile',
'email',
]);
if (result.isCancelled) {
alert('User cancelled the login process');
this.setState({loginInProcess: false});
}
const data = await AccessToken.getCurrentAccessToken();
if (!data) {
alert('Something went wrong obtaining access token');
this.setState({loginInProcess: false});
}
const facebookCredential = auth.FacebookAuthProvider.credential(
data.accessToken,
);
await auth()
.signInWithCredential(facebookCredential)
// The problem starts here from the catch block
.catch((error) => {
if (
error.code === 'auth/account-exists-with-different-credential'
) {
var pendingCred = error.credential;
var email = error.email;
auth()
.fetchSignInMethodsForEmail(email)
.then(async (methods) => {
if (methods[0] === 'google.com') {
const {idToken} = await GoogleSignin.signIn();
const googleCredential = auth.GoogleAuthProvider.credential(
idToken,
);
auth()
.signInWithCredential(googleCredential)
.then((user) => {
user.linkWithCredential(pendingCred);
})
.catch((error) => console.log(error));
}
});
}
});
}
This code implements a function when triggered, if there is no user with the same email, it proceeds normally, however if there is an error (mentioned above), it will grant the user with a list of google accounts that are present in the user phone (google thing) and when he chooses his account (linked with google account) it doesn't work. The email isn't linked.
To be more specific, I would like somehow to not grant the user with all his google accounts but only with the email to be linked var email = error.email; (in the code snippet above) and for the Facebook provider to be linked successfully.
After a little of hard work, I've managed to make it work in react native and I'm gonna leave the answer here for peeps who are facing the same issue. Be ware that I used react-native-prompt-android to ask the user for confirming his password when trying to link with Facebook.
The user tries to sign with Facebook and gets this error:
auth/account-exists-with-different-credentials
This is how I handled it:
.catch((error) => {
// Catching the error
if (
error.code === 'auth/account-exists-with-different-credential'
) {
const _responseInfoCallback = (error, result) => {
if (error) {
alert('Error fetching data: ' + error.toString());
} else {
setEmail(result.email);
}
};
// Getting the email address instead of error.email from Facebook
const profileRequest = new GraphRequest(
'/me?fields=email',
null,
_responseInfoCallback,
);
new GraphRequestManager().addRequest(profileRequest).start();
if (email) {
auth()
.fetchSignInMethodsForEmail(email)
.then(async (methods) => {
// Checking the method
if (methods[0] === 'password') {
// Prompting the user to confirm/input his password for linking
const AsyncAlert = () => {
return new Promise((resolve, reject) => {
prompt(
'Password Confirmation',
'The email address is already linked with password account. Enter your password to process',
[
{
text: 'Cancel',
style: 'cancel',
},
{
text: 'Continue',
onPress: (password) =>
resolve(setPassword(password)),
},
],
{
type: 'secure-text',
cancelable: false,
placeholder: 'Password',
},
);
});
};
// Here the linking goes
await AsyncAlert().then(async () => {
await auth()
.signInWithEmailAndPassword(email, password)
.then(() => {
return auth().currentUser.linkWithCredential(
facebookCredential,
);
})
.catch(() => alert('Something went wrong'));
});
} else if (methods[0] === 'google.com') {
const {idToken} = await GoogleSignin.signIn(email);
const googleCredential = auth.GoogleAuthProvider.credential(
idToken,
);
await auth()
.signInWithCredential(googleCredential)
.then(() => {
return auth().currentUser.linkWithCredential(
facebookCredential,
);
});
}
});
} else {
alert('Something went wrong');
}
}
});

How to use Firebase's 'verifyPhoneNumber()' to confirm phone # ownership without using # to sign-in?

Im using react-native-firebase v5.6 in a project.
Goal: In the registration flow, I have the user input their phone number, I then send a OTP to said phone number. I want to be able to compare the code entered by the user with the code sent from Firebase, to be able to grant entry to the next steps in registration.
Problem: the user gets the SMS OTP and everything , but the phoneAuthSnapshot object returned by firebase.auth().verifyPhoneNumber(number).on('state_changed', (phoneAuthSnapshot => {}), it doesn't give a value for the code that firebase sent, so there's nothing to compare the users entered code with. However, there's a value for the verificationId property. Here's the object return from the above method:
'Verification code sent', {
verificationId: 'AM5PThBmFvPRB6x_tySDSCBG-6tezCCm0Niwm2ohmtmYktNJALCkj11vpwyou3QGTg_lT4lkKme8UvMGhtDO5rfMM7U9SNq7duQ41T8TeJupuEkxWOelgUiKf_iGSjnodFv9Jee8gvHc50XeAJ3z7wj0_BRSg_gwlN6sumL1rXJQ6AdZwzvGetebXhZMb2gGVQ9J7_JZykCwREEPB-vC0lQcUVdSMBjtig',
code: null,
error: null,
state: 'sent'
}
Here is my on-screen implementation:
firebase
.firestore()
.collection('users')
.where('phoneNumber', '==', this.state.phoneNumber)
.get()
.then((querySnapshot) => {
if (querySnapshot.empty === true) {
// change status
this.setState({ status: 'Sending confirmation code...' });
// send confirmation OTP
firebase.auth().verifyPhoneNumber(this.state.phoneNumber).on(
'state_changed',
(phoneAuthSnapshot) => {
switch (phoneAuthSnapshot.state) {
case firebase.auth.PhoneAuthState.CODE_SENT:
console.log('Verification code sent', phoneAuthSnapshot);
this.setState({ status: 'Confirmation code sent.', confirmationCode: phoneAuthSnapshot.code });
break;
case firebase.auth.PhoneAuthState.ERROR:
console.log('Verification error: ' + JSON.stringify(phoneAuthSnapshot));
this.setState({ status: 'Error sending code.', processing: false });
break;
}
},
(error) => {
console.log('Error verifying phone number: ' + error);
}
);
}
})
.catch((error) => {
// there was an error
console.log('Error during firebase operation: ' + JSON.stringify(error));
});
How do I get the code sent from Firebase to be able to compare?
As #christos-lytras had in their answer, the verification code is not exposed to your application.
This is done for security reasons as providing the code used for the out of band authentication to the device itself would allow a knowledgeable user to just take the code out of memory and authenticate as if they had access to that phone number.
The general flow of operations is:
Get the phone number to be verified
Use that number with verifyPhoneNumber() and cache the verification ID it returns
Prompt the user to input the code (or automatically retrieve it)
Bundle the ID and the user's input together as a credential using firebase.auth.PhoneAuthProvider.credential(id, code)
Attempt to sign in with that credential using
firebase.auth().signInWithCredential(credential)
In your source code, you also use the on(event, observer, errorCb, successCb) listener of the verifyPhoneNumber(phoneNumber) method. However this method also supports listening to results using Promises, which allows you to chain to your Firebase query. This is shown below.
Sending the verification code:
firebase
.firestore()
.collection('users')
.where('phoneNumber', '==', this.state.phoneNumber)
.get()
.then((querySnapshot) => {
if (!querySnapshot.empty) {
// User found with this phone number.
throw new Error('already-exists');
}
// change status
this.setState({ status: 'Sending confirmation code...' });
// send confirmation OTP
return firebase.auth().verifyPhoneNumber(this.state.phoneNumber)
})
.then((phoneAuthSnapshot) => {
// verification sent
this.setState({
status: 'Confirmation code sent.',
verificationId: phoneAuthSnapshot.verificationId,
showCodeInput: true // shows input field such as react-native-confirmation-code-field
});
})
.catch((error) => {
// there was an error
let newStatus;
if (error.message === 'already-exists') {
newStatus = 'Sorry, this phone number is already in use.';
} else {
// Other internal error
// see https://firebase.google.com/docs/reference/js/firebase.firestore.html#firestore-error-code
// see https://firebase.google.com/docs/reference/js/firebase.auth.PhoneAuthProvider#verify-phone-number
// probably 'unavailable' or 'deadline-exceeded' for loss of connection while querying users
newStatus = 'Failed to send verification code.';
console.log('Unexpected error during firebase operation: ' + JSON.stringify(error));
}
this.setState({
status: newStatus,
processing: false
});
});
Handling a user-sourced verification code:
codeInputSubmitted(code) {
const { verificationId } = this.state;
const credential = firebase.auth.PhoneAuthProvider.credential(
verificationId,
code
);
// To verify phone number without interfering with the existing user
// who is signed in, we offload the verification to a worker app.
let fbWorkerApp = firebase.apps.find(app => app.name === 'auth-worker')
|| firebase.initializeApp(firebase.app().options, 'auth-worker');
fbWorkerAuth = fbWorkerApp.auth();
fbWorkerAuth.setPersistence(firebase.auth.Auth.Persistence.NONE); // disables caching of account credentials
fbWorkerAuth.signInWithCredential(credential)
.then((userCredential) => {
// userCredential.additionalUserInfo.isNewUser may be present
// userCredential.credential can be used to link to an existing user account
// successful
this.setState({
status: 'Phone number verified!',
verificationId: null,
showCodeInput: false,
user: userCredential.user;
});
return fbWorkerAuth.signOut().catch(err => console.error('Ignored sign out error: ', err);
})
.catch((err) => {
// failed
let userErrorMessage;
if (error.code === 'auth/invalid-verification-code') {
userErrorMessage = 'Sorry, that code was incorrect.'
} else if (error.code === 'auth/user-disabled') {
userErrorMessage = 'Sorry, this phone number has been blocked.';
} else {
// other internal error
// see https://firebase.google.com/docs/reference/js/firebase.auth.Auth.html#sign-inwith-credential
userErrorMessage = 'Sorry, we couldn\'t verify that phone number at the moment. '
+ 'Please try again later. '
+ '\n\nIf the issue persists, please contact support.'
}
this.setState({
codeInputErrorMessage: userErrorMessage
});
})
}
API References:
verifyPhoneNumber() - React Native or Firebase
PhoneAuthProvider.credential(id, code) - Firebase
signInWithCredential() - React Native or Firebase
Suggested code input component:
react-native-confirmation-code-field
Firebase firebase.auth.PhoneAuthProvider won't give you the code for to compare, you'll have to use verificationId to verify the verificationCode that the user enters. There is a basic example in firebase documentation than uses firebase.auth.PhoneAuthProvider.credential and then tries to sign in using these credentials with firebase.auth().signInWithCredential(phoneCredential):
firebase
.firestore()
.collection('users')
.where('phoneNumber', '==', this.state.phoneNumber)
.get()
.then((querySnapshot) => {
if (querySnapshot.empty === true) {
// change status
this.setState({ status: 'Sending confirmation code...' });
// send confirmation OTP
firebase.auth().verifyPhoneNumber(this.state.phoneNumber).on(
'state_changed',
(phoneAuthSnapshot) => {
switch (phoneAuthSnapshot.state) {
case firebase.auth.PhoneAuthState.CODE_SENT:
console.log('Verification code sent', phoneAuthSnapshot);
// this.setState({ status: 'Confirmation code sent.', confirmationCode: phoneAuthSnapshot.code });
// Prompt the user the enter the verification code they get and save it to state
const userVerificationCodeInput = this.state.userVerificationCode;
const phoneCredentials = firebase.auth.PhoneAuthProvider.credential(
phoneAuthSnapshot.verificationId,
userVerificationCodeInput
);
// Try to sign in with the phone credentials
firebase.auth().signInWithCredential(phoneCredentials)
.then(userCredentials => {
// Sign in successfull
// Use userCredentials.user and userCredentials.additionalUserInfo
})
.catch(error => {
// Check error code to see the reason
// Expect something like:
// auth/invalid-verification-code
// auth/invalid-verification-id
});
break;
case firebase.auth.PhoneAuthState.ERROR:
console.log('Verification error: ' + JSON.stringify(phoneAuthSnapshot));
this.setState({ status: 'Error sending code.', processing: false });
break;
}
},
(error) => {
console.log('Error verifying phone number: ' + error);
}
);
}
})
.catch((error) => {
// there was an error
console.log('Error during firebase operation: ' + JSON.stringify(error));
});
In order to use Multi-factor Authentication, you must create Phone Sign-in provider in the background alongside primary (in your case) email sign-in provider either initially or later while user choose to update settings and enables MFA. And then link it while user is logged using email sign-in provider as follows;
const credential = auth.PhoneAuthProvider.credential(verificationId, code);
let userData = await auth().currentUser.linkWithCredential(credential);
This is not supported by firebase unfortunately. Logging in and out after signInWithCredential can work, but is very confusing
I was facing the same difficulty. My aim was only to verify users' phone numbersenter image description here and then register them using email and password. After a long intense trial and error methodology, I have arrived at the solution. But the point is I am using firebase in my android application. what it did was
I first tried matching the OTP with the user entered OTP, but the OTP that firebase provides us in the backend is encrypted with some logic and the logic is nowhere in the documentation so I could not decrypt it.
The second approach worked for me. What I did was, I signed in the user using the phone authorization and when the task was successful I deleted the newly created user there itself and then signed in the user using email id and password.

Firebase Google login not staying persistence

I am developing an application, with an feature of Google Login through Firebase. I am trying to login via Google with the help of an library, known as react-native-google-signin. It is well known library in the field of ReactNative for Google Login.
My problem is not with this library, but the problem is that while I am using react-native-google-signin library with firebase to login via google. Firebase User is not staying persistence, I mean to say that when I am opening app after close FirebaseUser is getting null. Below the code I am using to login via firebase,
GoogleSignin.signIn().then(data => {
const credentials = firebase.auth.GoogleAuthProvider.credential(data.idToken, data.accessToken);
firebase.auth().setPersistence(firebase.auth.Auth.Persistence.LOCAL)
.then() => {
return firebase.auth().signInWithCredential(credentials);
}).catch(error => {
console.log('Error', error);
})
}).then(user => {
console.log('user', firebase.auth().currentUser);
}).catch(error => {
console.log('Error', error);
})
I also checked Firebase Docs, tried by using setPersistence() method but still I am getting null user after open app again.
You can try this
when you first time open your app you get user null, but after login one time then reopen your app and you will get previously logged in user in your console
async _setupGoogleSignin() {
try {
await GoogleSignin.hasPlayServices({ autoResolve: true });
await GoogleSignin.configure({
webClientId: 'YOUR WEBCLIENTID',
offlineAccess: false
});
const user = await GoogleSignin.currentUserAsync();
console.log("user",user); // HERE YOU GET LOGGED IN USER IN YOUR CONSOLE FIRST TIME IT WILL BE NULL BUT AFTER YOU GET PREVIOUSLY LOGGED IN USER
this.setState({user});
}
catch(err) {
console.log("Play services error", err.code, err.message);
}}
then
_signIn() {
GoogleSignin.signIn()
.then((user) => {
console.log(user);
this.setState({user: user});
const credential = firebase.auth.GoogleAuthProvider.credential(user.idToken, user.accessToken);
// console.log(credential);
return firebase.auth().signInAndRetrieveDataWithCredential(credential);
})
.catch((err) => {
console.log('WRONG SIGNIN', err);
})
.done();}
it is worked for me...
hope it will help you...

Resources