How to handle firebase's onAuthStateChanged function in quasar - javascript

Currently I'm using tailwind css and headlessui for a few components and firebase.
Now I would like to use quasar but the boot files are very mysterious to me.
Currently I manage firebase with config.js, main.js and pinia store.
I replaced my old config.js file with a firebase.js boot file as recommended by Quasar and it seems to work. (but I don't really know if it's good practice)
import { boot } from 'quasar/wrappers'
import { initializeApp } from 'firebase/app'
import { getFirestore } from 'firebase/firestore'
import { getAuth } from 'firebase/auth'
const firebaseConfig = {
apiKey: 'xxxxxxxxxxxxxx',
authDomain: 'xxxxxxxxxxxxxx',
projectId: 'xxxxxxxxxxxxxx',
storageBucket: 'xxxxxxxxxxxxxx',
messagingSenderId: 'xxxxxxxxxxxxxx',
appId: '1:xxxxxxxxxxxxxx'
}
// Init firebase
initializeApp(firebaseConfig)
// Init services
const db = getFirestore()
const auth = getAuth()
export { db, auth }
// "async" is optional;
// more info on params: https://v2.quasar.dev/quasar-cli/boot-files
export default boot(async (/* { app, router, ... } */) => {
// something to do
})
But I don't know what to do with the old mains.js file which is no longer available in Quasar. In main.js there is the following code:
import { createApp, markRaw } from 'vue'
import router from './router/router'
import { createPinia } from 'pinia'
import App from './App.vue'
// firebase
import { auth } from './firebase/config'
import { onAuthStateChanged } from 'firebase/auth'
import './input.pcss'
let app
onAuthStateChanged(auth, () => {
if (!app) {
app = createApp(App)
.use(
createPinia().use(({ store }) => {
store.$router = markRaw(router)
})
)
.use(router)
.mount('#app')
}
})
What should I do with the code above in particular with the onAuthStateChanged function?
Thanks for your help

I've found a solution for this that is suitable for my purposes. For me the requirements were:
Make sure auth is initialized on a refresh, before rendering.
Make sure any data required for the app is also initialized, before rendering.
Detect log-in, log-out, and time outs and act accordingly.
I haven't tested time outs yet but basically I solved this with the following flow.
In your router/index.js file, add a before each function, that checks to see if a listener is active, and calls a store function to create it if not.
Router.beforeEach(async (to, from, next) => {
// Access a store where you check if the auth changes are being handled
const storeAuth = useAuth()
if (!storeAuth.handlingAuth) {
await storeAuth.handleAuth()
}
// Redirects as necessary using to.path and next
next()
})
In the auth store, make a function that returns a promise to await in the beforeEach. Something like:
async handleAuth() {
const auth = getAuth()
return new Promise((resolve) => {
let initialLoad = true
auth.onAuthStateChanged(async (user) => {
if (user) {
await this.initializeUserData()
} else {
await this.clearUserData()
}
// If it is not initial load, use the router to push
// depending on whether the user exists.
// if (user && !initialLoad) this.router.push('/members')
// This would detect a login and go to the members section.
// If it is the initial load, resolve the promise
// so the router proceeds
if (initialLoad) {
initialLoad = false
this.handlingAuth = true
resolve()
}
})
})
}
Don't make the mistake of using useRouter() in the store. useRouter() is only for use in the setup function, or <script setup>. What you need to do is add the router as a plugin to Pinia. So in your stores/index.js import your router, then add this:
pinia.use(({ store }) => { store.router = markRaw(router) })
That way you can use this.router.push() in your store modules.
This might seem a bit messy because of redirects in both the navigation guard and the store action but this seems like the easiest way to get it to load the required data from both refresh and login while only using onAuthStateChanged in one place.
In summary it works like this:
When refreshed or entering a url manually, the app awaits the first state change, and any necessary loading.
Then in the nav guard, you can check whatever store variables you need to check your user login state, and redirect. Like back to the login page, if the user happened to enter a url for a members-only section.
Subsequent navigations (non-refresh) see that the auth handling is already set up, so it is ignored.
When a separate function triggers login or logout, we can see that it is not the initialLoad, and do any other redirects at that point.

Related

(pinia, vuex) Why can't I access states and getters in main.js?

Solved
Although not exactly my solution to this problem, the answer given below from #Po Wen Chen was helpful but it doesn't work exactly as I want it to. data in the form of proxy continues to come, this is not important, the conditions are met.
The main problem was that every time the page was refreshed, the states were null, so their values were flying. After we searched, my states became permanent with the library named pinia-plugin-persistedstate.
Solved
I did as it is written in the documentation, but instead of receiving a user data, I am getting a proxy type data.
Documentation of pinia describing how to use store outside of component
The account store performs the registration and login of the users, that is, the auth processes.
import axios from "axios";
import { defineStore } from "pinia";
const useAccountStore = defineStore("account", {
state: () => ({
user: null,
returnUrl: null
}),
getters: {
isLoggedIn: state => (state.user ? true : false),
getUser: state => state.user,
},
actions: {
async init() {
console.log('run the init')
this.fetchUser()
},
async registerUser(user) {
await axios.post("/account/register", {user})
},
async login(credentials) {
const user = await axios.post("/account/session", credentials)
this.user = user.data
},
async logout() {
await axios.delete("/account/session")
this.user = null
},
async fetchUser() {
const user = await axios.get("/account")
this.user = user.data
},
},
})
export { useAccountStore };
In main.js
import antd from "ant-design-vue"
import "ant-design-vue/dist/antd.css"
import axios from "axios"
import { createPinia } from "pinia"
import { createApp } from "vue"
import App from "./app.vue"
import { router } from "./router"
import { useAccountStore } from "./store/account.store"
// import './assets/main.css'
axios.defaults.baseURL = import.meta.env.VITE_API_URL
axios.defaults.withCredentials = true
createApp(App)
.use(createPinia())
.use(router)
.use(antd)
.mount("#app")
useAccountStore().init()
router.beforeEach(async (to) => {
// redirect to login page if not logged in and trying to access a restricted page
const publicPages = ['/login', '/register'];
const authRequired = !publicPages.includes(to.path);
const authStore = useAccountStore();
console.log('authStore.isLoggedIn', authStore)
if (authRequired && !authStore.user) {
authStore.returnUrl = to.fullPath;
return '/login';
}
});
I press the authStore to console and it comes back to me as a proxy.
return the proxy
Although not exactly my solution to this problem, the answer given below from #Po Wen Chen was helpful but it doesn't work exactly as I want it to. data in the form of proxy continues to come, this is not important, the conditions are met.
The main problem was that every time the page was refreshed, the states were null, so their values were flying. After we searched, my states became permanent with the library named pinia-plugin-persistedstate.

React doesn't call useEffect on firebase (v9) auth update

I'm uploading a user profile image into firebase storage, then updating the photoURL value of the auth user with updateProfile(). After that I want the user to be updated without manually refreshing the page. I've been trying this for days now and the issue gets more weird every time I try to debug it.
The interesting thing is the user object seems to be already updated when I log it with console.log(currentUser) after the then promise of updateProfile() is fulfilled. So the new photoURL is already present in the currentUser object. But it seems to not call a state update or console.log("!!!!currentAuthUserUpdate", user);. So the user image wouldn't refresh in my page.
I even tried it with doing a useEffect with the currentUser as a dependency but it wasn't fired. Still, the currentUser object changed when logging it after updateProfile()
Updating the profile, UpdateUserImage.tsx:
import { useAuth } from "../../contexts/AuthContext";
const { currentUser } = useAuth();
// updating the user profile
updateProfile(currentUser, { photoURL })
AuthContext.tsx:
import { auth } from "./../firebase/firebase";
const [currentUser, setCurrentUser] = useState(null);
const auth = getAuth(app);
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged((user) => {
console.log("!!!!currentAuthUserUpdate", user);
// I tried setting the user as a custom new object: const userData = { ...user };
setCurrentUser(user);
});
return unsubscribe;
}, []);
firebase.js
import { getAuth } from "firebase/auth";
// Initialize Firebase
const app = initializeApp(firebaseConfig);
const auth = getAuth(app);
export {
auth
};
What I tried additionally: (But this wouldn't work as the user is already updated without a state refresh from react, so it's trying to replace the same object)
const reloadUser = async () => {
try {
const res = await currentUser.reload();
const user = auth.currentUser;
console.log("currentUser:", user);
setCurrentUser(auth.currentUser);
console.log("res", res);
} catch (err) {
console.log(err);
}
};
it's not auth.onAuthStateChanged. You need to import onAuthStateChanged from 'firebase/auth'
import { getAuth, onAuthStateChanged } from "firebase/auth"
const auth = getAuth(); // leave getAuth empty if you only have one app
Then in your useEffect it should be
onAuthStateChanged(auth, async (currentUser) => { ... }
The setCurrentUser function returned from useState isn't always the same function in my experience.
You can try passing it as a dependency into the useEffect - but I don't think that's what you want.
React lets you use an old useState setter if you give it an updater function, rather than a value: setCurrentUser(()=>auth.currentUser)
The React docs dispute this though.
Using useStates are good for re-rendering components. However going into utilizing useRefs are best for updating the actual variable and will not cause a re-render of the component.
Declare it like:const currentUser = useRef(null)
Update it like: currentUser.current = updatedUserData
Use in code like: currentUser.current.photoURL

How to use 9.0.1 Firebase methods with VueJS 3

I'm fairly new to Vue and this is the second tutorial I'm following, which integrates firebase backend with Vue. But the tutorial is using Vue 2 and also an older version of firebase, so I thought I could try to do it with Vue 3 and the new Firebase version.
The resources on the firebase 9.0.1 seems to be fairly limited with regards to implementation with Vue at least. This is what I found from the firebase documentation regarding the signInAnonymously
import { getAuth, signInAnonymously } from "firebase/auth";
const auth = getAuth();
signInAnonymously(auth)
.then(() => {
// Signed in..
})
.catch((error) => {
const errorCode = error.code;
const errorMessage = error.message;
// ...
});
From what I understand, firebase 9.0.1 is an import only what you use style? so If I want to use the getAuth and signInAnonymously methods from the firebase/auth, I would do
import { getAuth, signInAnonymously } from 'firebase/auth';
But I am a bit confused as to how to use the methods in my .Vue file
so what I did in my firebase.js file was
export const auth = getAuth();
export {signInAnonymously};
then in my Login.vue file, i did
import { auth, signInAnonymously } from '../firebase'
export default {
data() {
return { auth }
},
methods: {
signInAnonymously
}
}
and I have a button that when clicked triggers the signInAnonymously, which is written like so
<button class="button" #click="signInAnonymously(auth)">Sign In</button>
What I have written seems to work, but I find it a bit convoluted/confusing and want to know
am I doing this correctly or is there a shorter/neater way to write the code?
what happens if I want to modify the signInAnonymously method as shown in the firebase documentation, i.e. adding those signInAnonymously(auth).then(() => {}), because if i were to add the arguments for the signInAnonymously in my export default like below, it doesn't recognize it as the exported method from my firebase.js file?
export default {
...,
methods: {
signInAnonymously(auth) {
...
}
}
Try creating a custom method and using signInAnonymously() within that as shown below:
import { auth } from '../firebase'
import { signInAnonymously } from 'firebase/auth'
// can be imported directly in Login.vue ^^
export default {
methods: {
anonymousLogin() {
// Directly pass 'auth' in this method
signInAnonymously(auth)
.then(() => {
// Signed in..
})
.catch((error) => {
const errorCode = error.code;
const errorMessage = error.message;
// ...
});
},
},
};
Then use this custom method in #click event:
<button class="button" type="button" #click="anonymousLogin">Sign In</button>

Firebase createUserWithEmailAndPassword not doing anything in Next.js

I am trying to use Firebase Authentication on my Next.js website. For some reason, when I press the sign up button, nothing happens and no errors are logged. It just refreshes the page. It doesn't even set any cookies or create a user.
Here are my two files related to authentication:
utils/authProvider.js:
import firebase from 'firebase/app';
import 'firebase/auth';
if(!firebase.apps.length) {
firebase.initializeApp({
// config
});
}
const auth = firebase.auth();
module.exports = { auth };
pages/signup.js:
import { useState } from 'react'
import { LockClosedIcon } from '#heroicons/react/solid'
import { auth } from '../utils/authProvider'
export default function CreateAccount() {
const [emailField, setEmailField] = useState('');
const [passwordField, setPasswordField] = useState('');
// emailField and passwordField are set correctly, I used console.log to test it
const createAccount = () => {
// this event does get triggered, I used console.log to test it
auth.createUserWithEmailAndPassword(emailField, passwordField)
.then((userCredential) => {
window.location.replace('/');
console.log('logged in!');
}).catch((error) => {
console.error(error);
});
}
Edit: After experimenting some more, I saw that the request to https://www.googleapis.com/identitytoolkit/v3/relyingparty/signupNewUser never gets completed. It just shows as red in the network traffic tab of Chrome dev tools. Any reason why this might happen?
Thanks in advance.

React - Firebase handling Auth persistence using a separate auth class

I am building a react app that uses a simple login feature. I am only using google sign in, and am calling the signInWithPopop function to handle that. I have created a separate class to handle all the auth related code. On the navbar of my website I have a login button if the user is not signed in which switches to a profile button when the user has signed in.
This is how I am currently checking if the user is signed in (not working):
console.log(authHandler.getUser());
const[loginState, setLogin] = useState(authHandler.getUser()? true : false);
return(
<div className="navbar">
<div className="nav-options">
<NavItem name="About"></NavItem>
<NavItem name="Listings"></NavItem>
<NavItem name="Dashboard"></NavItem>
{loginState ? <NavItem name="Profile"><DropDown loginState={setLogin}></DropDown></NavItem> : <NavItem name="Login" click={() => authHandler.signIn(setLogin)}></NavItem>}
</div>
</div>
);
This is what I have for my authHandler class:
import firebase from 'firebase';
export default class Auth{
constructor(){
var firebaseConfig = {
...
};
!firebase.apps.length? firebase.initializeApp(firebaseConfig) : firebase.app();
firebase.analytics();
this.provider = new firebase.auth.GoogleAuthProvider();
}
signIn(state){
firebase.auth().setPersistence(firebase.auth.Auth.Persistence.SESSION).then(() => {
return firebase.auth().signInWithPopup(this.provider).then((result) => {
console.log("signed in " + result.user.uid);
this.user = result.user
state(true);
}).catch((error) => {
console.log(error.message);
});
}).catch((error) => {
console.log(error.message);
})
}
getUser(){
return firebase.auth().currentUser;
}
logout(state){
//TODO: logout of firebase
state(false);
}
}
I have tried adding session and local persistence on firebase, but when I refresh the page, the user is signed out. What would be the proper way of maintaining persistence, in a separate class like this? I am trying to build this app with best practices in mind so that the code will be split up properly, and security is maintained.
Thanks!
You're supposed to use an auth state observer to get a callback whenever the user's sign in state changes. When a page first loads, the user is always immediately considered to be signed out. The callback will be invoked some time soon after the user's token has been loaded from persistence and verified. Use this state callback to determine what to render.
firebase.auth().onAuthStateChanged((user) => {
if (user) {
// User is signed in, see docs for a list of available properties
// https://firebase.google.com/docs/reference/js/firebase.User
var uid = user.uid;
// ...
} else {
// User is signed out
// ...
}
});
You might want to show a loading screen until the first callback tells you for sure if the user was previously signed in or is definitely signed out.
I suggest reading this for more information.
The way I implemented the auth state in react :
Auth.provider.tsx
import React, {
FC,
createContext,
useContext,
useEffect,
useState,
} from 'react';
import { User, auth } from 'firebase/app';
interface AuthContext {
user: User | null;
loading: boolean;
}
const defaultAuthContext = { user: null, loading: false };
const AuthUserContext = createContext<AuthContext>({ ...defaultAuthContext });
export const AuthUserProvider: FC = ({ children }) => {
const [authContext, setAuthContext] = useState<AuthContext>({
user: null,
loading: true,
});
useEffect(
() =>
auth().onAuthStateChanged((authUser) =>
setAuthContext({ user: authUser, loading: false }),
),
[],
);
return (
<AuthUserContext.Provider value={authContext}>
{children}
</AuthUserContext.Provider>
);
};
export const useAuthUser = () => useContext(AuthUserContext);
App.tsx
const App: React.FC = () => {
return <AuthUserProvider>
// anything
</AuthUserProvider>;
}
anycomponent.tsx
const { user, loading } = useAuthUser();
return loading ? <Loader /> : !user ? <NotLogged /> : <Logged />;
You could implement the observer in your class but everytime you'll need your user you should implement an useEffect watching the user. Making it global in a provider make it easier to use.
There are many other way but I think this one is the easiest to use.

Categories