Pinia firebase user emailVerified property loses reactivity - firebase

Currently I'm building an app that on user signup navigates the user to an email verification page. This page then watches the firebase user object inside of a Pinia store waiting for the emailVerified user property to update before directing them to a new page.
When I update the user object manually using vue devtools I can observe my console.log. When I receive the email verification email and use the link provided by firebase my watcher does not react to the user update. I can refresh the pinia store using my vue devtools and I see emailVerified inside my firebase user object has been updated to true but my watcher was never hit.
Any ideas on why I am losing reactivity when going through the email flow?
testStore.js
export const useTestStore = defineStore('test', () => {
const auth = getAuth()
const {user} = useAuth(auth)
return {
user: user,
}
})
emailVerification.js
<script setup>
const { user } = storeToRefs(testStore)
watch(user, () => {
console.log('Direct user to new page')
}, { deep:true })
</script>

For some reason when replacing my watcher with setInterval it seems to works... although this is not the ideal solution
setInterval(function () {
if(user){
if(user.value?.emailVerified) {
console.log('Direct user to new page');
}
}
}, 5000);

Related

Firebase Auth not loading persisted user from local storage

I'm using Firebase Auth in a Vue app. I want signed-in users to be persisted through reloading and closing the browser, so I'm using the 'local' persistence setting, which saves logged-in user tokens into the browser's localStorage.
The docs state that Firebase will automatically detect this localStorage entry, and publish a change in auth state, which fires onAuthStateChange handlers.
However, in my app the user credential is not being retrieved after a refresh.
The token is stored properly and can be viewed in the dev tools, so the browserLocalPersistence is working.
When I reload the page, however, the onAuthStateChange hook fires only one time, printing that the current user is null.
Code
Firebase init
const app = initializeApp(firebaseConfig);
const auth = initializeAuth(app);
setPersistence(auth, browserLocalPersistence)
Pinia store w/ action to update state:
export const useAuthStore = defineStore({
id: "auth",
state: (): UserState => ({
user: null,
}),
actions: {
init() {
return new Promise<User | null>((resolve, reject) => {
onAuthStateChanged(
getAuth(firebaseApp),
async (user) => {
console.log(user);
this.user = user;
resolve(user);
},
reject
);
});
},
},
});
Initializing onAuthStateChanged hook within Pinia store from setup function of top-level Vue component:
const authStore = useAuthStore();
authStore.init().then(console.log);
With this code, I'm getting null printed to the console. That's understandable upon immediate page load, before Firebase has verified the persisted authentication, but I would expect the callback to fire a second time with the correct credentials loaded.
Everything works properly after an initial sign in -- it's only after a refresh that the credentials are not loaded from local storage.
Any help is appreciated!

Firebase Authentication JS does not populate `providerData`array

in a VueJS / QuasarJS application Im using firebase-js-sdk [1] together with firebaseui-web [2] to handle authentication.
After successful auth with any of the configured providers (e.g. password, google, apple, etc) I want to check which provider the user used. But immediately after successful authentication the user.providerData[] array that should contain the information is empty.
BUT if I reload my app the user.providerData[] array is suddenly populated correctly.
Iยดm checking for user data with something like this
import { getAuth } from "firebase/auth";
const auth = getAuth();
const user = auth.currentUser;
if (user) {
console.log(user.providerData)
}
After that the user object is fully populated (incl auth tokens, etc) but the user.providerData[] array is empty. Only after a page reload (CTRL-R) does the array get populated.
I searched both projects issues pages and documentation and didnt find anything that could explain this.
Im thankful for ANY idea where to look next!
EDIT
As suggested by #aside Im using onAuthStateChanged to check for updates of the user state.
onAuthStateChanged(
fbAuth,
(user) => {
if (user) {
console.log("onAuthStateChanged: user found");
console.log("onAuthStateChanged: user.providerData", user.providerData);
console.log("onAuthStateChanged: user", user);
} else {
console.log("onAuthStateChanged: no user found");
}
},
function (error) {
console.log("onAuthStateChanged:", error);
}
);
But even if I wait minutes after authentication is completed, still the user.providerData array is only populated after a page reload.
Here is a full demo: https://codesandbox.io/s/github/perelin/firebase-auth-providerdata-test
Thanks in advance :)
Im using
"firebase": "9.6.1",
"firebaseui": "6.0.0",
[1] https://github.com/firebase/firebase-js-sdk
[2] https://github.com/firebase/firebaseui-web
Your app should call getAuth().currentUser.reload() to refresh the local user data after login.
This could be done either in beforeRouteEnter() nav guard of the LoggedIn view:
// LoggedIn.vue
import { getAuth, signOut } from "firebase/auth";
export default {
async beforeRouteEnter(to, from, next) {
await getAuth().currentUser?.reload() ๐Ÿ‘ˆ
next()
},
}
demo 1
Or in the onAuthStateChanged callback:
// main.js
onAuthStateChanged(
fbAuth,
async (user) => {
await user?.reload() ๐Ÿ‘ˆ
},
)
demo 2
Your code is only running once instead of running every time the auth state is updated.
If you want to listen to any changes to the auth state, use a callback along with onAuthStateChanged as described here.
https://firebase.google.com/docs/auth/web/manage-users#get_the_currently_signed-in_user
import { getAuth, onAuthStateChanged } from "firebase/auth";
const auth = getAuth();
onAuthStateChanged(auth, (user) => {
if (user) {
// Check used provider here
const providerData = user.providerData;
// ...
} else {
// User is signed out
// ...
}
});
The reason checking/requesting the user object right after authentication does not work is that it might take firebase a second to update the providerData array. signInWithX might therefore return before the property is updated.

Firebase and VueJS: How to handle users that login with Google and don't register in my app? User profile management

I have an app with a LOGIN page and a REGISTER page. Both pages have a "Sign in with Google" button, as well as a regular login and password input form for those that don't want to sign in with Google. I am also using FireStore to create user profile documents for registered users. When the user also logs in, the app will query the user's profile for use throughout the app. This all works fine.
I noticed that a google user does not need to "register"...he can still click the login button and it will "sign him up" automatically because that's how Google Auth Provider works. However, since he did not "register", he does not yet have a profile. In this case, I had to write some logic so a profile would be created for a Google user. Although this logic works, I just wonder if this is the best way to do this. Are there best practices for handling Google/Social logins for people skipping the traditional "registering" pages? I know most people would probably head to the register page and register, but there will undoubtedly be some people that will skip that and go start to the LOGIN page and sign in via Google that way.
Here's how I'm handling the login page with Google login button:
login.vue
async logInWithGoogle() {
try {
const provider = new this.$fireAuthObj.GoogleAuthProvider()
const userCredentials = await this.$fireAuth.signInWithRedirect(
provider
) ....
Then in my Store (in my case, Vuex state management pattern), I have the following actions:
store.js
First, this onAuthStateChanged observer will notice the new user state and do the following code:
async onAuthStateChangedAction({ commit, dispatch }, { authUser }) {
if (authUser) {
console.log('user committing from onAuthStateChangedAction...')
commit('SET_CURRENT_USER', authUser)
console.log(
'fetchUserProfile action dispatching from onAuthStateChangedAction...'
)
await dispatch('fetchUserProfile', authUser)
} else {
dispatch('logOutUser')
}
}
That onAuthStateChanged observer will fetch the user's profile (and this is the logic I am concerned with...not sure if this is an ideal way to handle user's logging in via Google for first time and bypassing registration:
async fetchUserProfile({ commit }, user) {
try {
const docRef = this.$fireStore.collection('users').doc(user.uid)
const profile = await docRef.get()
if (profile.exists) {
commit('SET_USER_PROFILE', await profile.data())
console.log(
'user profile EXISTS and set from fetchUserProfile action'
)
} else {
console.log('profile does not exist! Creating...')
await docRef.set({
displayName: user.displayName,
email: user.email,
uid: user.uid,
photoUrl: user.photoURL,
providerId: user.providerData[0].providerId,
createdAt: this.$fireStoreObj.FieldValue.serverTimestamp()
})
const p = await docRef.get()
commit('SET_USER_PROFILE', await p.data())
console.log('user profile set')
}
} catch (error) {
console.log('can not fetch profile', error)
}
},
Thanks for any tips or assurances that I am on the right (or wrong) path on handling this. Thank you!
Why not create an empty document with the user's uid and prompt them to "complete their profile"? Until they do so, force redirect them back to the profile page indefinitely.

How to know if Firebase Auth is currently retrieving user?

Background
I am using GoogleAuthProvider, with the default LOCAL persistence.
When I navigate to the page, I do:
firebase.initializeApp(firebaseConfig)
firebase.auth().currentUser // this is always null
firebase.auth().onAuthStateChanged(user => {
console.log("authStateChanged", user)
})
If the user is logged in, the callback is called once, with the user.
If the user is not logged in, the callback is also called once, with null.
This suggests I could wait until the first callback after navigating to the page to get the real login state before deciding what view to display, for instance. (I originally thought that it would not get called with null, and so I could end up waiting indefinitely)
Question
Would that be idiomatic usage? Does it seem like it will be robust against updates to firebase? Where can I find this discussed in the official documentation?
2022 Edit: in firebase web SDK 9, it's
import { getAuth, onAuthStateChanged } from "firebase/auth";
const auth = getAuth();
export const isReady = new Promise(resolve => {
const unsubscribe = onAuthStateChanged(auth, (/* user */) => {
resolve(/* user */)
unsubscribe()
})
})
P.S: The reason I don't resolve with the user is because it is available at auth.currentUser, while the promise would retain an outdated value.
Looking at similar questions such as Pattern for Firebase onAuthStateChanged and Navigation Guards - Quasar app it seems this is indeed the way it's done.
So I have come up with the following to differentiate the initial condition:
export const isReady = new Promise(resolve => {
const unsubscribe = firebase.auth().onAuthStateChanged(() => {
resolve()
unsubscribe()
})
})
I export this Promise from the module where I wrap firebase, so I can begin other initialization while waiting for an authoritative authentication state.
this worked for me instead. NB: For those user Quasar
export default async ({ app, router, store }) => {
return new Promise(resolve => {
const unsubscribe = auth.onAuthStateChanged((user) => {
auth.authUser = user
resolve()
unsubscribe()
})
})
}

Using Firebase user UID with vuefire manual binding

In a simple SPA with Vue and Firebase, there are two routes: Login and Chat.
Upon login, the user is redirected to the Chat route where Firebase database bindings are done manually using vuefire's $bindAsArray(), inside the created() life-cycle hook. This is because the bindings require the uid assigned by the Firebase authentication to be available.
This works fine, until the user refreshes the page. If auth().currentUser is used to get the uid, it returns null. If the auth().onAuthStateChanged() watcher is used, Vue attempts to render the component before the Firebase database bindings are done. What am I missing?
I come across this scenario, as workaround I use component wrapper that has UID as property, if UID is null show a waiting message/animation else show your original component.
My real scenario is a little more complex to post it here (firebase, routing, vuex) but basically that wrapper component should look similar to this
<template>
<component :is="CurrentComponent" />
</template>
<script>
import App from './App';
import WaitingAnimation from './WaitingAnimation';
export default {
data() {
return {
Uid: null,
}
},
computed: {
CurrentComponent() {
return this.Uid == null ? WaitingAnimation : App;
}
}
beforeMount() {
//While Firebase is initializing `Firebase.auth().currentUser` will be null
let currentUser = Firebase.auth().currentUser;
//Check currentUser in case you previously initialize Firebase somewhere else
if (currentUser) {
//if currentUser is ready we just finish here
this.Uid = currentUser.uid;
} else {
// if currentUser isn't ready we need to listen for changes
// onAuthStateChanged takes a functions as callback and also return a function
// to stop listening for changes
let authListenerUnsuscribe = Firebase.auth().onAuthStateChanged(user => {
//onAuthStateChanged can pass null when logout
if (user) {
this.Uid = user.uid;
authListenerUnsuscribe(); //Stop listening for changes
}
});
}
}
}
</script>

Resources