Can't set user data after firebase auuthentication - reactjs

I'm working on a react app on firebase. The issue that fire store doesn't set up userdata after fire authentication although it's been successfully authenticated on Google. Please help me if you have some clues to solve that. And I can't get error texts here.
login = () => {
const provider = new firebase.auth.GoogleAuthProvider();
firebase
.auth()
.signInWithRedirect(provider)
.then(result => {
const user = result.user;
const userRef = firebase
.firestore()
.collection("users")
.doc(user.uid);
userRef.get().then(function(doc) {
if (doc.exists) {
console.log("User data:", doc.data());
} else {
// doc.data() will be undefined in this case
userRef.set({
uid: user.uid,
name: user.displayName,
photoURL: user.photoURL
});
}
});
});
this.props.history.push("/");
};
Additional
'user' is not defined.
'userRef' is not defined
I have tried the code. But user and userRef could not be defined here:
if (doc.exists) {
console.log("Doc for user " + user.uid + " already exists");
throw new Error("Doc for user " + user.uid + " already exists");
} else {
return userRef.set({
uid: user.uid,
name: user.displayName,
photoURL: user.photoURL
});
}
Additional 2
I don't know why but that would not work then on Firebase Authentication.
login = () => {
let user;
const provider = new firebase.auth.GoogleAuthProvider();
firebase
.auth()
.signInWithRedirect(provider)
.then(result => {
console.log("result", result);
// result has been skipped

Actually, the signInWithRedirect() method returns a promise with no return value (i.e. Promise<void>). Therefore doing
firebase
.auth()
.signInWithRedirect(provider)
.then(result => {...});
will not work.
You need to use the getRedirectResult() method, as explained in the doc:
Authenticates a Firebase client using a full-page redirect flow. To
handle the results and errors for this operation, refer to
firebase.auth.Auth.getRedirectResult.
Therefore, in your login function, you should just have two lines:
login = () => {
const provider = new firebase.auth.GoogleAuthProvider();
firebase
.auth()
.signInWithRedirect(provider);
}
And somewhere else in your code (I don't know exactly where as I don't know reactjs...) you need to have the following code (note how the different Promises are chained):
let user;
firebase
.auth()
.getRedirectResult()
.then(result => {
user = result.user;
const userRef = firebase
.firestore()
.collection('users')
.doc(user.uid);
return userRef.get();
})
.then(doc => {
if (doc.exists) {
console.log('Doc for user ' + user.uid + ' already exists');
throw new Error('Doc for user ' + user.uid + ' already exists');
} else {
return doc.ref.set({
uid: user.uid,
name: user.displayName,
photoURL: user.photoURL
});
}
})
.then(() => {
this.props.history.push('/');
})
.catch(error => {
console.log(error);
this.props.history.push('/errorPage');
});
Note that in case several users are able to sign-in with the same Google Account, you may need to use a Transaction when checking the non-existence of the doc at userRef.

Related

When sign in with google firebase always register user as new

Tech: Firebase, Next.js, Google Sign in, Firebase Stripe exstension
Bug reproduction:
When login with Google
Subscribe on stripe
Stripe saves subscription data for that user in firestore
Logout
Login in with Google and old data are overide with new one, and Subscription is lost
Does anyone had similar problem?
Maybe my implementation of Sign-in is bad, here is the Google Sign in code:
const handleGoogleLogin = () => {
signInWithPopup(auth, googleProvider)
.then(async result => {
if (!result.user) return;
const { displayName, email, uid, providerData, photoURL, phoneNumber } =
result.user;
const name = splitName(displayName as string);
const providerId =
(providerData.length && providerData[0]?.providerId) || '';
const data = {
firstName: name?.firstName || '',
lastName: name?.lastName || '',
email,
photoURL,
phoneNumber,
providerId,
};
await updateUser(uid, data);
})
.catch(error => {
console.log('Google login error: ', error);
});
};
Update user function:
export const updateUser = async (uid: string, data: UpdateUserParams) => {
try {
if (!uid) {
return;
}
await setDoc(doc(firestore, 'users', uid), {
account: {
...data,
initials: `${data.firstName[0]}${data.lastName[0]}`,
},
});
} catch (error) {
console.error('Error updating user: ', error);
}
};
setDoc is overwriting the contents of the document with each sign-in. You should instead use set with merge to prevent overwriting the fields you don't want to lose, or check first if the document exists before creating it.
See also:
https://firebase.google.com/docs/firestore/manage-data/add-data#set_a_document
Difference Between Firestore Set with {merge: true} and Update

How do I use a Firebase refresh token to persist authentication?

I have been trying to figure this out for weeks and either can't seem to understand the documentation, or something. I appreciate any help you can give.
I am using the Firebase SDK
I have my server-side route, in which I can access the token and could send it to the front:
const admin = require("firebase-admin")
admin.initializeApp()
exports.loginRoute = (req, res) => {
const user = {
email: req.body.email,
password: req.body.password
}
const { valid, errors } = validateLoginData(user)
if (!valid) {
return res.status(400).json(errors)
}
admin
.auth()
.signInWithEmailAndPassword(user.email, user.password)
.then((data) => {
console.log(data.user.refreshToken, "refresh token")
return data.user.getIdToken(true)
})
.then((token) => {
return res.json({ token })
})
.catch((err) => {
console.error(err)
if (err.code === "auth/user-not-found") {
return res.status(400).json({ general: "User not found" })
} else if (err.code === "auth/wrong-password") {
return res
.status(400)
.json({ password: "User credentials don't match" })
} else {
res.status(500).json({
error: "Something went wrong, please try again."
})
}
})
}
Here is where I could use the refresh token (on the front end) to fetch a new authentication token, but I can't figure out how to create a route to do this:
if (token) {
const decodedToken = jwtDecode(token)
if (decodedToken.exp * 1000 < Date.now()) {
localStorage.setItem("Authentication", false)
//axios request to persist authentication would go here
}
}
Does anyone have a route that would work, or advice on what to do?
EDIT
const login = async (credentials) => {
let token
await axios
.post("/api/login", credentials)
.then((res) => {
token = res.data.token
const FBIdToken = `Bearer ${token}`
localStorage.setItem("token", token)
localStorage.setItem("FBIdToken", FBIdToken)
localStorage.setItem("Authentication", true)
context.setAuthenticated((prev) => true)
})
.then(() => {
context.getUserData()
})
.then(() => {
context.setUserState((prevUserState) => ({
...prevUserState,
token
}))
})
.catch((err) => {
context.setUserErrors((prev) => ({
...prev,
errors: err.response.data
}))
})
history.push("/")
}
Observer (client-side):
firebase.auth().onAuthStateChanged((user) => {
if (user) {
firebase
.auth()
.currentUser.getIdToken(/* forceRefresh */ true)
.then((idToken) => {
const FBIdToken = `Bearer ${idToken}`
localStorage.setItem("FBIdToken", FBIdToken)
})
.catch((err) => {
console.log(err)
})
} else {
localStorage.removeItem("FBIdToken")
}
})
If you sign in with the Firebase Authentication JavaScript SDK in the client-side code, it already persists the user's sign-in state, and tries to restore it when you reload the page. You shouldn't have to do anything for that yourself.
It seems like you were using the same SDK in a server-side environment though, which is quite unusual. If you want to mint tokens yourself in a server-side environment, you should use the Firebase Admin SDK to do so. You can then send that token back to the client, and use it to sign in to Firebase Authentication there.
But for the vast majority of use-cases, I recommend using the Firebase Authentication SDK in your client-side code, so that the SDK managed refreshing of the token for you. If you then want to pass the token to the server, you can use getIdToken() as you do now. You can also monitor ID token generation, or more commonly monitor if a user's sign-in session is restored as shown in the first example of the documentation on detecting the current user.

How can I properly handle request errors with React front end?

Currently I have my backend set up as such on the '/register' route:
registerRouter.post('/', async (req, res) => {
// Validate submitted registration form
const { error } = registerValidation(req.body)
if(error) {
return res.status(400).send(error.details[0].message)
}
try {
// Check if email exists already
const user = await User.findOne({ email: req.body.email })
if(user) {
return res.status(400).send('Email already exists')
}
// If not, begin registering user by hashing the password
const hashedPassword = await bcrypt.hash(req.body.password, 10)
const newUser = new User({
firstName: req.body.firstName,
lastName: req.body.lastName,
email: req.body.email,
password: hashedPassword
})
const savedUser = await newUser.save()
res.send(savedUser)
} catch(error) {
res.sendStatus(500)
}
})
Using Postman I get the proper responses when I make correct/incorrect requests. But when I make requests on my frontend, if it is an incorrect request, e.g. not long enough password, missing a required field, then I just get a 400 response. How can I use the error response to, for example, display the error on-screen for the user to see?
This is my current onSubmit function for the form:
const register = async event => {
event.preventDefault()
axios
.post('/register', newUser)
.then(res => console.log(res))
.catch(err => console.log(err))
}
try to use:
axios
.post('/register', newUser)
.catch(function (error) {
console.log(error.toJSON()); // or maybe exist .toText()
});
(https://github.com/axios/axios#handling-errors)
also convert it on server side:
return res.status(400).send('Email already exists')
to
return res.status(400).send({ error: 'Email already exists' });

React Native (expo) and Firebase Firestore - Issue when a user is created within the app (Undefined is not an object...evaluating...)

I am having trouble with firebase and expo. When we do the signup process, and the user is generated through firebase, we sometimes get "undefined is not an object (evaluating 0.docs[0].data)" and the user is redirected to the entry point of the application instead of going to the next screen.
Most of the users will be able to go through the process without any problem. But few reported the same issues, and I have seen that with one account.
Here below the function that does not work properly
const createUser = async () => {
//Check if user already exists
await props.firebase.db
.collection('users')
.where('email', '==', props.userProfile.email)
.get()
.then(async (snapShot) => {
//if no document is found, save user to db
if (snapShot.docs.length === 0) {
await saveUserToDataBase()
.then(() => {
setShowLoader(false);
props.navigation.navigate('NotificationPermission');
})
.catch((err) => Alert.alert('An error occurred', err.message));
}
//else log an error, TODO: add error text in app.
else {
setShowLoader(false);
}
})
.catch((err) => {
Alert.alert('An error occurred', err.message);
});
};
const saveUserToDataBase = async () => {
//finds the correct collection and creates a new document within in containing the profile data.
await props.firebase.db
.collection('users')
.add({
first_name: props.userProfile.firstName,
email: props.userProfile.email.toLowerCase(),
phone_number: props.userProfile.phoneNumber,
organization: props.userProfile.organization,
profileId: props.userProfile.profileId,
})
.then(async (docRef) => {
await props.firebase.db
.collection('profile')
.doc(props.userProfile.profileId)
.update({
user_id: docRef.id,
})
.then(async () => {
await uploadProfilePhoto(docRef.id);
if (props.accessToken) {
props.navigation.navigate('NotificationPermission');
} else {
props.navigation.navigate('NotificationPermission');
}
})
.catch((err) => {
Alert.alert(
'An error occurred updating the users profile ',
err.message,
);
});
})
.catch((err) => {
Alert.alert('An error occurred creating the user', err.message);
});
};
I have used the Alert component to check the error directly, the error is consistent with few email addresses. I have upgraded the firebase package, did no do anything.
I feel like the onAuthStateChanged could be responsible of that but I am not sure how to handle that? Your help or suggestions will be greatly appreciated.

Sending verification email with Firebase and React Native

I am trying to send the validation email upon the account registration, using firebase. The registration is being done successfully but whenever I try to code email verification it gives me an error. Probably because I don't know where to place it. All my firebase methods are on Fire.js, which are the following:
import firebaseKeys from './Config';
import firebase from 'firebase';
require("firebase/firestore");
class Fire {
constructor() {
if (!firebase.apps.length) {
firebase.initializeApp(firebaseKeys);
}
}
addPost = async ({ text, localUri }) => {
const remoteUri = await this.uploadPhotoAsync(localUri, 'photos/${this.uid}/${Date.now()}');
return new Promise((res, rej) => {
this.firestore.collection('posts').add({
text,
uid: this.uid,
timestamp: this.timestamp,
image: remoteUri
})
.then(ref => {
res(ref);
})
.catch(error => {
rej(error);
});
});
}
uploadPhotoAsync = async (uri, filename) => {
return new Promise(async (res, rej) => {
const response = await fetch(uri);
const file = await response.blob();
let upload = firebase
.storage()
.ref(filename)
.put(file);
upload.on(
"state_changed",
snapshot => {},
err => {
rej(err);
},
async () => {
const url = await upload.snapshot.ref.getDownloadURL();
res(url);
}
);
});
}
createUser = async user => {
let remoteUri = null
try {
await firebase.auth().createUserWithEmailAndPassword(user.email, user.password)
//I tried to code it here with user.sendEmailVerification();
let db = this.firestore.collection("users").doc(this.uid)
db.set({
name: user.name,
email: user.email,
avatar: null
})
if (user.avatar) {
remoteUri = await this.uploadPhotoAsync(user.avatar, 'avatars/${this.uid}')
db.set({avatar: remoteUri}, {merge: true});
}
} catch (error) {
alert("Error: ", error);
}
};
get firestore() {
return firebase.firestore();
}
get uid() {
return (firebase.auth().currentUser || {}).uid;
}
get timestamp() {
return Date.now();
}
}
Fire.shared = new Fire();
export default Fire;
The createUserWithEmailAndPassword() method returns a Promise which resolves with a UserCredential AND (as the the doc indicates) "on successful creation of the user account, this user will also be signed in to your application."
So you can easily get the signed in user by using the user property of the UserCredential, and call the sendEmailVerification() method, as follows:
try {
const userCredential = await firebase.auth().createUserWithEmailAndPassword(user.email, user.password);
await userCredential.user.sendEmailVerification();
//In the next line, you should most probably use userCredential.user.uid as the ID of the Firestore document (instead of this.uid)
cont db = this.firestore.collection("users").doc(this.uid);
//...
} catch (...)
Note that you may pass an ActionCodeSettings object to the sendEmailVerification() method, see the doc.

Resources