Vue + Pinia + Firebase Authentication: Fetch currentUser before Route Guard - firebase

Recently I started to use Pinia as a global store for my Vue 3 Project. I use Firebase for the user authentication and am trying to load the current user before Vue is initialized. Ideally everything auth related should be in a single file with a Pinia Store. Unfortunately (unlike Vuex) the Pinia instance needs to be passed to the Vue instance before I can use any action and I believe that is the problem. On first load the user object in the store is empty for a short moment.
This is the store action that is binding the user (using the new Firebase Web v9 Beta) in auth.js
import { defineStore } from "pinia";
import { firebaseApp } from "#/services/firebase";
import {
getAuth,
onAuthStateChanged,
getIdTokenResult,
} from "firebase/auth";
const auth = getAuth(firebaseApp);
export const useAuth = defineStore({
id: "auth",
state() {
return {
user: {},
token: {},
};
},
actions: {
bindUser() {
return new Promise((resolve, reject) => {
onAuthStateChanged(
auth,
async (user) => {
this.user = user;
if (user) this.token = await getIdTokenResult(user);
resolve();
},
reject()
);
});
},
// ...
}})
and this is my main.js file
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import { createPinia } from "pinia";
import { useAuth } from "#/store/auth";
(async () => {
const app = createApp(App).use(router).use(createPinia());
const auth = useAuth();
auth.bindUser();
app.mount("#app");
})();
How can I set the user before anything else happens?

I figured it out. Had to register the router after the async stuff
//main.js
(async () => {
const app = createApp(App);
app.use(createPinia());
const { bindUser } = useAuth();
await bindUser();
app.use(router);
app.mount("#app");
})();

Related

provider.credential' is undefined in Expo Firebase Google authentication

I'm very new and trying to set Firebase Google authentication in Expo RN app,
Expo docs code snippet below:
https://docs.expo.dev/guides/authentication/#google
import * as React from 'react';
import * as WebBrowser from 'expo-web-browser';
import { ResponseType } from 'expo-auth-session';
import * as Google from 'expo-auth-session/providers/google';
import { initializeApp } from 'firebase/app';
import { getAuth, GoogleAuthProvider, signInWithCredential } from 'firebase/auth';
import { Button } from 'react-native';
// Initialize Firebase
initializeApp({
/* Config */
});
WebBrowser.maybeCompleteAuthSession();
export default function App() {
const [request, response, promptAsync] = Google.useIdTokenAuthRequest(
{
clientId: 'Your-Web-Client-ID.apps.googleusercontent.com',
},
);
React.useEffect(() => {
if (response?.type === 'success') {
const { id_token } = response.params;
const auth = getAuth();
const provider = new GoogleAuthProvider();
const credential = provider.credential(id_token);
signInWithCredential(auth, credential);
}
}, [response]);
return (
<Button
disabled={!request}
title="Login"
onPress={() => {
promptAsync();
}}
/>
);
}
I'm getting error something like that:
TypeError: provider.credential is not a function. (In 'provider.credential(_id_token)', 'provider.credential' is undefined)
Any solution?
Thanks in advance
Replace the following code:
const provider = new GoogleAuthProvider();
const credential = provider.credential(id_token);
with:
const credential = GoogleAuthProvider.credential(idToken);
I believe the expo docs need to be updated to reflect this.

Vue Reactivity: Creating reactive data with provide and inject

My aim is to validate a users' credentials from vue frontend, fetch their data, store the data in a globally available variable and use them in any component. After some work, I'm able to use Vue's reactive() method with inject and provide. Here's my current code:
In store/index.js
import { reactive, inject } from 'vue'
// global states
export const stateSymbol = Symbol('state')
export const createState = () => reactive({
backendRoute: 'http://127.0.0.1:8000/',
currentUser: {},
logIn: userData => {
let user = createState.currentUser
user = userData
console.log(user)
}
})
export const useState = () => inject(stateSymbol)
Then in main.js
import { createApp } from 'vue'
import App from './App.vue'
// store modules
import { stateSymbol, createState } from './store'
// create app
const app = createApp(App)
app.provide(stateSymbol, createState())
Then in login/register component named Login.vue
import { useState } from "../store"
import axios from 'axios'
export default {
name: 'RegisterLogin',
setup() {
let state = useState()
axios.get(getUser, jwtConfig).then(response => {
userId = response.data.id
const currentUser = getUserProfile+userId // URL
console.log(currentUser)
const getCurrentUser = axios.get(currentUser, jwtConfig)
getCurrentUser.then(response => {
state.logIn(response.data)
console.log(state.currentUser)
})
})
}
}
console.log(user) in store/index.js logs the userData as expected. I believe with that, createState.currentUser should be mutated. The problem however is console.log(state.currentUser) logs an empty proxy object. Also, if I try accessing the state.currentUser from another component like Home.vue:
<template>
{{ state.currentUser }}
</template>
<script>
import { useState } from '../store/'
export default {
name: 'Index',
setup() {
return {
state: useState(),
}
},
}
</script>
...an empty object is always displayed even after the user logs in successfully. Is there anything I'm doing wrong? Thank you for your assistance (in advance).
I was looking through the code but couldn't find anywhere where the variable was getting mutated, but I believe I know what's going on.
You are reassigning the user object, not assigning the createState.currentUser to another object.
import { reactive, inject } from 'vue'
// global states
export const stateSymbol = Symbol('state')
export const createState = reactive({
backendRoute: 'http://127.0.0.1:8000/',
currentUser: {},
logIn: userData => {
// over here you assign user to createState.currentUser
let user = createState.currentUser;
// then you reassign user to `user data`
user = userData
console.log(user)
}
})
export const useState = () => inject(stateSymbol)
you could instead try
import { reactive, inject } from 'vue'
// global states
export const stateSymbol = Symbol('state')
export const createState = () => reactive({
backendRoute: 'http://127.0.0.1:8000/',
currentUser: {},
logIn: userData => {
createState.currentUser = userData
console.log(createState.currentUser)
}
})
export const useState = () => inject(stateSymbol)
As an aside, why are you using inject? I can't figure out what you're expecting it to do.
export const useState = () => stateSymbol would have the same result, but you don't even need to pass the function, you can just use something more concise like...
//////// store
import { reactive } from 'vue'
// global states
export const store = reactive({
backendRoute: 'http://127.0.0.1:8000/',
currentUser: {}
})
export const logIn = (userData) => {
store.currentUser = userData
}
////////////////////////////////////////////////
////// main
import { store, logIn } from "../store"
import axios from 'axios'
export default {
name: 'RegisterLogin',
setup() {
axios.get(getUser, jwtConfig).then(response => {
userId = response.data.id
const currentUser = getUserProfile+userId // URL
console.log(currentUser)
const getCurrentUser = axios.get(currentUser, jwtConfig)
getCurrentUser.then(response => {
logIn(response.data)
console.log(store.currentUser)
})
})
}
}
It appears #Daniel was right after all. My logIn method in store/index.js was not really mutating the currentUser property. In fact, I'm unable to access that property from the method. To solve this, I had to do the mutation immediately after the axios call like state.currentUser = response.data in then method and boom, the state is updated in all components! Thanks.

firebase.auth.GoogleAuthProvider is not a constructor

I'm trying to use google sign using firebase in the Vue framework. I don't know what the error is this can anyone help me with this.
vue.runtime.esm.js?2b0e:1888 TypeError: _firebase_js__WEBPACK_IMPORTED_MODULE_2__.fb.auth.GoogleAuthProvider is not a constructor
at VueComponent.socialLogin (Signin.vue?3d55:76)
at invokeWithErrorHandling (vue.runtime.esm.js?2b0e:1854)
at HTMLButtonElement.invoker (vue.runtime.esm.js?2b0e:2179)
at HTMLButtonElement.original._wrapper (vue.runtime.esm.js?2b0e:6917)
this is my code
firebase.js
import firebase from "firebase";
var firebaseConfig = {
config
};
const fb=firebase.initializeApp(firebaseConfig);
export { fb };
Sign in.vue
<script>
import { fb } from "../firebase.js";
export default {
name: "Signin",
components: {},
data() {
return {
};
},
methods: {
socialLogin() {
const provider = new fb.auth.GoogleAuthProvider();
fb.auth().signInWithPopup(provider).then((result) => {
this.$router.replace('home');
}).catch((err) => {
alert('Oops. ' + err.message)
});
}
}
};
</script>
The auth property (not the auth() function) is available on the static firebase object, not your firebase app.
You want something more like this
import firebase from "firebase/app"
import "firebase/auth" // 👈 this could also be in your `firebase.js` file
const provider = new firebase.auth.GoogleAuthProvider()

Framework7, Firebase, VueJS Routes RequireAuth

This is my main.js
import Vue from 'vue';
import Framework7 from 'framework7/dist/js/framework7.js';
import Framework7Vue from 'framework7-vue/dist/framework7-vue.js';
import Routes from './routes.js';
import App from './app.vue';
import * as firebase from 'firebase';
Vue.use(Framework7Vue, Framework7);
var config = {
};
firebase.initializeApp(config);
new Vue({
el: '#app',
template: '<app/>',
framework7: {
id: 'io.framework7.testapp',
name: 'Framework7',
theme: 'auto',
routes: Routes,
},
components: {
app: App
}
});
console.log(Routes)
Routes.beforeEach((to, from, next) => {
// let currentUser = firebase.auth().currentUser;
console.log()
})
I tried for a few hours now to implement that a user needs to be Authenticated before he can enter the Home View. Otherwise he will be redirected to the login View.
Maybe anyone can help me.
The following changes to your main.js file should normally do the trick. (I don't know however, if the framework7 framework interacts with the router and may cause problems).
You use router.beforeEach() to check if the "target" needs the user to be authenticated (based on requiresAuth meta). If the user is not authenticated you need to redirect her/him to the signin page. For that you can use firebase.auth().currentUser. See the corresponding Firebase doc here.
import Vue from 'vue';
import VueRouter from 'vue-router';
import Framework7 from 'framework7/dist/js/framework7.js';
import Framework7Vue from 'framework7-vue/dist/framework7-vue.js';
import Routes from './routes.js';
import App from './app.vue';
import * as firebase from 'firebase';
Vue.use(Framework7Vue, Framework7);
Vue.use(VueRouter);
var config = {
};
firebase.initializeApp(config);
const router = new VueRouter({
Routes,
mode: 'history'
});
router.beforeEach((to, from, next) => {
const requiresAuth = to.matched.some(record => record.meta.requiresAuth)
const currentUser = firebase.auth().currentUser
if (requiresAuth && !currentUser) {
next('/signin')
} else if (requiresAuth && currentUser) {
next()
} else {
next()
}
})
new Vue({
el: '#app',
template: '<app/>',
framework7: {
id: 'io.framework7.testapp',
name: 'Framework7',
theme: 'auto',
routes: VueRouter,
},
components: {
app: App
}
});
I don't know anything about Vue Router but the firebase way to do this is to wait for onAuthStateChanged method to trigger and track user state with this function.
To wait and get user state, i wrote a piece of code that you can call before changing route.
var user, authPromiseResolver, authPromiseResolved
var authPromise = new Promise(function(resolve, reject){
authPromiseResolver = resolve
})
var waitForUser = function(){
return new Promise(function(resolve, reject){
if(!authPromiseResolved){
authPromise.then(resolve)
} else {
resolve(user)
}
})
}
firebaseApp.auth().onAuthStateChanged(function(firebaseUser) {
user = firebaseUser
if(!authPromiseResolved){
authPromiseResolver(user)
authPromiseResolved = true
}
})
waitForUser returns a promise who wait for first trigger of onAuthStateChanged (which will determine if an user is already logged in or not at page refresh), then resolve with user data or null it no user is logged in. So you just have to call this function before accessing each page :
waitForUser().then(function(user){
if(user) {
// An user is logged in
} else {
// Page unauthorized
}
})
Each time the user state change (user logout/login), user variable will be set to null or with firebaseUser object and waitForUser() will always be resolved with the current user state

this.$firebase is undefined

I'm building my first Vue.js app, trying to use vuex with vuexfire.
//main.js
import firebase from 'firebase';
...
Vue.prototype.$firebase = firebase.initializeApp(config);
...
firebase.auth().onAuthStateChanged(() => {
/* eslint-disable no-new */
new Vue({
el: '#app',
store,
router,
render: h => h(App),
created() {
this.$store.dispatch('setDealsRef');
},
});
});
router.beforeEach((to, from, next) => {
const currentUser = firebase.auth().currentUser;
const requiresAuth = to.matched.some(record => record.meta.requiresAuth);
if (requiresAuth && !currentUser) {
next('/signin');
} else if (requiresAuth && currentUser) {
next();
} else {
next();
}
});
And:
//store/index.js
import Vue from 'vue';
import Vue from 'vue';
import Vuex from 'vuex';
import { firebaseMutations } from 'vuexfire';
...
Vue.use(Vuex);
const db = this.$firebase.firestore();
const dealsRef = db.collection('deals');
And:
//store/mutations.js
export default {
SET_USER(state) {
state.user = this.$firebase.auth().currentUser;
},
...
}
This complies OK, but throws TypeError: this.$firebase is undefined[Learn More] in the console.
Any idea what I'm doing wrong? I think I've read every relevant tutorial and StackOverflow questions, and tried everything.
When you do:
Vue.prototype.$firebase = firebase.initializeApp(config);
You add $firebase to the Vue instance. So for
this.$firebase
to work, the this should be a Vue insteance. In other words, that line must execute inside a Vue method/hook/computed/etc.
And the code you show, doesn't do that:
const db = this.$firebase.firestore();
in the code above, the this is the outer context. (Probably is window.)
So for it to work outside a Vue instance, you have to do:
const db = Vue.prototype.$firebase.firestore();
Provided the line above executes after (in time/order) the line where you initialize the $firebase.
I think I solved the problem:
Moving firebase initialization to store.js
Changing firebase.auth().onAuthStateChanged(() => { to Vue.prototype.firebase.auth().onAuthStateChanged(() => { in main.js
Importing firebase as: import firebase from '#firebase/app';
import '#firebase/firestore';

Resources