onAuthStateChanged being called too early - javascript

I'm tyring to create a new user with some additional data (eg: subscribeToEmail field etc.). From what I have read online, the way to go about doing this is to authenticate a user (eg: by using createUserWithEmailAndPassword), and then using the uid that I obtain from that to create a new document in a users collection. That is what I'm trying to do below:
const handleSignup = async (formData) => {
const {email, password, ...otherUserData} = formData;
const {user} = await auth.createUserWithEmailAndPassword(email, password);
console.log("generating user doc at signup...");
await generateUserDocument(user, otherUserData); // creates user doc (called 2nd)
}
The generateUserDocument will create a user document (if it doesn't already exist) in the users collection using the uid of the user obtained from the createUserWithEmailAndPassword function call. I also have also set up an auth state change event handler:
auth.onAuthStateChanged(async (userAuth) => { // get the current user
if (userAuth) {
const user = await generateUserDocument(userAuth); // user (should) already exists, so generateUserDocument will fetch the user rather than creating a new user doc, but this doesn't happen, as it is called 1st and not 2nd
dispatch(login(user)); // login logic
} else {
dispatch(logout());
}
});
The issue here is, when I call createUserWithEmailAndPassword the onAuthStateChanged callback triggers, which then calls generateUserDocument before I have actually created a user document with generateUserDocument(user, otherUserData); inside of the handleSignup method. In other words, the fetch user method: generateUserDocument inside of .onAuthStateChange() is being invoked before the user is actually created, which is done by the generateUserDocument inside of the handleSignup method. As a result, the user data I'm fetching inside of authStateChange doesn't include the details I'm after.
Is there a way to fix this so that my function call after the auth.createuserWithEmailAndPassword() is called before the onAuthStateChange event handler is executed (rather than after)? I have thaught about using something like .onSnapshot() perhaps, but I'm thinking that this might be a little overkill as the user data table shouldn't really need to be continously be listened too, as it will rarely changed. Preferably there is a lifecycle method that gets invoked before onAuthStateChanged that I could use to populate my users collection, but I haven't been able to find much on that.
For reference, I have been following this article regarding associating additional user data with a auth-user record.

Is there a way to fix this so that my function call after the
auth.createuserWithEmailAndPassword() is called before the
onAuthStateChange event handler is executed (rather than after)?
No, there is no out-of-the-box way, because
On successful creation of the user account with createuserWithEmailAndPassword() the user will also be signed in to your application (see the doc), and
The onAuthStateChange() observer is triggered on sign-in or sign-out.
So you indeed need to wait the user Firestore doc is created before proceeding. In my opinion, the best approach is the one you have mentioned, i.e. setting a listener to the user document.
You can do that in such a way you cancel the listener right after the first time you get the data from the user doc, as shown below. This way the user doc is not continuously being listened to.
auth.onAuthStateChanged((userAuth) => { // No more need to be async
// get the current user
if (userAuth) {
const userDocRef = firestore.collection('users').doc(userAuth.uid);
const listener = userDocRef.onSnapshot((doc) => {
if (doc.exists) {
console.log(doc.data());
// Do any other action you need with the user's doc data
listener(); // Calling the unsubscribe function that cancels the listener
dispatch(login(user));
}
});
} else {
dispatch(logout());
}
});
Another approach could be to use a Callable Cloud Function which, in the backend, creates the user in the Auth service AND the document in Firestore. Your handleSignup function would be as follows:
const handleSignup = async (formData) => {
const createUser = firebase.functions().httpsCallable('createUser');
await createUser({formData});
// The user is created in the Auth service
// and the user doc is created in Firestore
// We then need to signin the user, since the call to the Cloud Function did not do it!
const {email, password, ...otherUserData} = formData;
await auth.signInWithEmailAndPassword(email, password);
// The onAuthStateChanged listener is triggered and the Firestore doc does exist
}

Related

Firebase how to createUserWithEmailAndPassword and a user record at the same time?

Using Firebase, I'd like to do the following:
Create a new user using FirebaseAuth (using createUserWithEmailAndPassword)
Create a record for this new user in FireStore, where the document ID = user ID (adding fields such as name, role, gender etc.)
The problem is that once createUserWithEmailAndPassword is successful, an authStateChanges is triggered, and It's impossible to create the user record now...
How can I achieve this rather basic flow using Firebase's API?
Can I only achieve that using the Firebase Admin SDK?
The problem is that once createUserWithEmailAndPassword is successful, an authStateChanges is triggered, and It's impossible to create the user record now...
Generally you would unsubscribe from the observer if you want to run some actions after registration like this:
const unsub = onAuthStateChanged(...);
const register = async () => {
unsub(); // observer will not trigger on state changes
// create user
// add document to Firestore
}
Alternatively, you can use Firebase Authentication Triggers to create document for user using Cloud Functions.

Data from firestore it's being fetched multiple times when login and logout (vanilla JS)

Well I made this Library app, where an user can login and add books. So, when a user login the app fetch data from a firestore collection, cool. The problem exists when the user login once, logout and then login again without refreshing the app. If the user do this twice, the fetch twice, if thrice, the fetch thrice. The code that executes multiple times its the fetchBooks(), the signInWithGoogle() only executes once. Here's the code involved:
function signInWithGoogle(){
const provider = new firebase.auth.GoogleAuthProvider()
auth.signInWithPopup(provider)
.then(result => {
// Create the new user document in firestore
createNewUserDocument(result.user)
// fetch feed data
auth.onAuthStateChanged(user =>{
user? fetchBooks() : null
})
}).catch(err => {
console.log(err)
})
signUpForm.reset()
signUpModal.hide()
signInForm.reset()
signInModal.hide()
}
function fetchBooks() {
const docRef = db.collection('users').doc(auth.currentUser.uid).collection('books')
docRef.get().then(querySnapshot =>{
console.log(querySnapshot)
querySnapshot.forEach(doc => {
const data = doc.data()
console.log(doc.data());
addCardToHTML(data.title, data.author, data.pages, data.description, data.read)
})
})
}
onAuthStateChanged is a subscription that triggers itself when there's a change in the user's authentication state.
So it will trigger when you log in, when you log out, etc.
So ideally you'd want to wait until the user logs in, and then call the fetchBooks() function, but if you keep doing it inside of the subscriber the function will trigger any time the subscriber emits a new value.
I would recommend starting with a restructure of your code to have functions that do individual things. Right now, you have a function signInWithGoogle. That function should only sign the user in with Google and return a promise with the result of that sign in. Instead, you have it signing in the user, fetching books (which itself is also fetching books AND modifying the DOM), and calling methods on your signUp elements.
Restructuring this to have some other top-level function would likely help you handle your problem easier. Specifically, try something like this:
function handleSignIn() {
signInWithGoogle()
.then(fetchBooks)
.then(books => {
books.forEach(book => addCardToHTML(...))
})
}
This is a good start because now it's clear what each individual function is doing. So now to handle your specific issue, I'll assume that the problem you're facing is that you're seeing the books be added multiple times. In that case, I would think what you'd want to happen is that:
When a user is signed in, you want to load their books and display them on the page.
When they log out, you want the books to be unloaded from the screen
When they log back in, the books are re-loaded and displayed.
If all of those assumptions are correct, then your problem wouldn't be with the code you have, but rather the signout functionality. When the user signs out, you need to add a function that will remove the books from the HTML. That way, when they sign back in after signing out, the handleSignIn function will kick off again and the addCardToHTML function will be running on a blank HTML page rather than a page that already has the cards.
Example:
function handleSignOut() {
signOut()
.then(clearBookCards)
}
function clearBookCards() {
// Manipulate DOM to remove all of the card HTML nodes
}

Bulk Update using Batched Write of Firebase with conditions (using WHERE function)

The main goal of my system is to update the name of the user who posted on my forum if the authenticated user change or rename his or her account name.
The whole process is error-free but unfortunately, the other user who posted in the forum also updated their name.
So this is the output:
I try the following:
I use the WHERE function in Firebase to filter the post made by the user (log in user itself). I dont know why the whole process is failed.
This is the snippet code.
async updateAll(username) {
const batch = this.afs.firestore.batch();
// cUser is the USER ID
const userRef = this.afs
.collection('post', (ref) => ref.where('postedById', '==', this.cUser))
.ref.get();
(await userRef).forEach((element) => {
batch.update(element.ref, {
postedBy: username,
});
});
return batch.commit();
}
You end your query with .ref.get(). The .ref in there, actually returns the collection on which you run the query, so you end up loading the entire post collection.
You'll want to subscribe to snapshotChanges instead, or just use the regular JavaScript SDK to accomplish this (as you're not accessing the UI directly, I typically find that easier):
const userRef = firebase.firestore()
.collection('post').where('postedById', '==', this.cUser).get();
(await userRef).forEach((element) => {
batch.update(element.ref, {
postedBy: username,
});
});

query to firestore to check collection and document

i am a little bit new to firebase / firestore. I am using the stripe api.
Once the user hits start trial on the prebuilt stripe checkout page, then it should go to firestore and create a new collection called subscriptions with all the users information. It seems to be doing this, however, I created a page called successPage, and it basically checks to make sure that it created it.
please find the code below:
const successPage = props => {
firebase.auth().onAuthStateChanged((user) => {
if(user) {
console.log("calling success page : " + user.uid)
//checking if user is paying for subscription
firestore.collection('customers').doc(user.uid).collection('subscriptions')
.where('status', 'in', ['trialing', 'active']).get()
.then(activeSubscriptions => {
// if this is true, the user has no active subscription.
if (activeSubscriptions.empty === true) {
console.log(user.uid)
firestore.collection('customers').doc(user.uid)
.get().then(
doc => {
if (doc.exists) {
firestore.collection('customers').doc(user.uid).collection('subscriptions').get().
then(sub => {
if (sub.docs.length > 0) {
var activityStatus = "canceled"
createCheckoutSession(activityStatus)
console.log('subcollection exists');
} else {
alert("Your account has been created, but your payment details we're not successfully created. You will now be redirected to the checkout page")
createCheckoutSession()
console.log(user.uid)
console.log("does not exist!")
}
});
}
});
} else if (activeSubscriptions.size > 1){
alert("you have more then one active subscription. please manage your subscriptions and cancel one of your subscriptions to access the application")
} else {
firestore.collection("profiledata").doc(user.uid).update({
accountStatus: "active"
}).then (() => {
firestore
.collection("roger#x.ca")
.add({
to: user.email,
message: {
},
})
.then(() => console.log("email out for delivery!"));
props.history.push('/clients')
})
}
});
}
})
return (
<input type="hidden"></input>
)
}
it checks the subscriptions collection where status = to either trialing, or active, and then it checks everything inside subscriptions to see what is going on, but it for some reason it keeps redirecting to the stripe page (createCheckoutSession) even though the subscriptions collection has been created. is this a timing issue?
Stripe triggers a Webhook to your server/cloud functions when a new subscription is created and after that the document is created in Firestore. This process might take some time and meanwhile your user may have been redirected to the success page. If the document has not been created yet then you won't be able to show the transaction status.
Here's a workaround that you can do:
While creating a Stripe Checkout session on your server, you can actually create the subscriptions document but set a field called "stripe_response" to false and also add the new subscription document ID as a query parameter in the stripe success_url. So you url maybe looks something like: https://domain.ext/paymentSuccess?id=<that_document_id>,
Now when the user is on the success page, look for that specific subscription document with the ID mentioned in the query parameter. If the "stripe_response" is still false, then maybe the webhook has not done it's job yet. Just retry the request after 5 seconds and then show the status to user. Make sure you set the stripe_response to true in the webhook.
To simply step 2, you can just attach a realtime listener on the document so when the status is updated you get the response asap and don't need to rely on polling.
Coming to the 'keeps redirecting' part, can you please specify what the exact redirect situation? Where is it redirecting and all that?
It's definitely a race condition but if you follow the steps above it should take care of it. Please let me know if you need more assistance fixing this issue.

Firebase Anonymous User Authentication flow

I have the following js code where I try to login on the blog_root object and then use my user_root to listen for any changes with user_root.on("child_added",...)
var root = new Firebase( /*my firebase url*/);
var blog_root = root.child("blog");
var user_root;
console.log("====");
blog_root.authAnonymously(function(err, authData) {
if(err){
console.log("user not authenticated?"+err);
}else{
console.log("user authenticated?"+authData.uid);
if(authData){
user_root = blog_root.child(authData.uid);
user_root.set(authData);
}
}
},{
remember: "sessionOnly"
});
user_root.on(
"child_added", //event_name
function(snapshot) {
//called for each existing element and each new element
}
Using this code specifically my user_root object is shown to be undefined. I check my data and the anonymous user has been successfully added to the blog_root, but I cannot seem to be able to listen to changes on user_root.
What I gather is that the user_root.on("child_added") event is fired on my authentication statement, but this makes no sense to me because user_root is initialized inside the authentication statement.
Any clues?
The authAnonymously method is asynchronous, and the callback is invoked after you first get to user_root.on('child_added', ...). Try moving this listener into the callback from authAnonymously.
Also, note that each time you call authAnonymously(), you're creating a new session. Sessions are automatically persisted by default. To check whether or not the user is already authenticated, try ref.getAuth(), and only invoke authAnonymously() if the user is unauthenticated (ref.getAuth() === null).

Categories

Resources