How to make "onAuthStateChanged" wait to "currentUser.updateProfile" to finish? - javascript

I have a problem registering users in my Vue app... When a user register, I want to fire updateProfile to add more user data, and after that, in the onAuthStateChanged in my main.js save all that user data in vuex to use across my app, but the onAuthStateChanged excutes before the updateProfile, so in vuex displayName and photoURL are undefined.
How can I make the onAuthStateChanged wait to updateProfile to finish?
I have this function in my Register.vue component:
register() {
firebase
.auth()
.createUserWithEmailAndPassword(this.email, this.password)
.then(user => {
firebase
.auth()
.currentUser.updateProfile({
displayName: this.displayName,
photoURL: this.photoURL
})
.then(() => {
db.collection("users")
.doc(user.user.uid)
.set({
email: this.email,
displayName: this.displayName,
realName: this.realName,
photoURL: this.photoURL
})
.then(() => {
console.log(user);
this.$router.replace("/");
})
.catch(err => {
this.errorMessage = err.message;
});
})
.catch(err => {
this.errorMessage = err.message;
});
})
.catch(function(error) {
// Handle Errors here.
var errorCode = error.code;
var errorMessage = error.message;
if (errorCode == "auth/weak-password") {
alert("The password is too weak.");
} else {
alert(errorMessage);
}
console.log(error);
});
}
}
And have this in my main.js:
let app = "";
firebase.auth().onAuthStateChanged((user) => {
store.dispatch("fetchUser", user);
if (!app) {
new Vue({
router,
store,
render: (h) => h(App),
}).$mount("#app");
}
});

The reason you are getting undefined is probably because you don't return the asynchronous functions from the register method.
You currently have this
register() {
firebase // undefined as you don't return anything
.auth()
.createUserWithEmailAndPassword(this.email, this.password)
.then(user => {
firebase
.auth()
.currentUser.updateProfile({
....
})
.then(() => { // undefined as you don't return in the previous then block
db.collection("users")
.doc(user.user.uid)
.set({
......
})
.then(() => { // undefined because you don't return in the previous then block
....
})
.catch(err => {
...
});
})
.catch(err => {
....
});
})
.catch(function(error) {
....
});
}
}
Do this
import { EventBus } from './eventBus.js';
register() {
EventBus.$emit('user-registration-causes-auth-change', true) // set this to true when the registration starts so that when eventbus listens to it in main.js, it won't update user profile (alternatively, replace with let self = this)
// (if following alternative route, this.isRegistrationTriggeringAuthStateChange = true;)
return firebase
.auth()
....
.then(user => {
return firebase
.auth()
.currentUser....
.then(() => {
return db.collection("users")
....
.set({
......
})
.then(() => {
EventBus.$emit('user-registration-causes-auth-change', false) // set it to false after you have finised updating the user profile.
// if following alternative route, self.isRegistrationTriggeringAuthStateChange = false;
})
})
})
}
eventBus.js
import Vue from 'vue';
export const EventBus = new Vue(); // initialise an event bus here and export it so that it could be used in other files
Main.js
import Vue from 'vue';
import { EventBus } from './eventBus.js';
let app
auth.onAuthStateChanged(async() => {
if (!app) {
app = new Vue({
router,
store,
data: {
isRegistrationTriggeringAuthStateChange: false; // initialise it to false so that when you load the page, it won't be hindered from dispatching "fetchUser" if there's a user.
},
render: h => h(App)
}).$mount('#app')
}
EventBus.$on('user-registration-causes-auth-change', (payload) => {
app.isRegistrationTriggeringAuthStateChange = payload;
})
await Vue.$nextTick() // wait for the DOM to update
if (user && !app.isRegistrationTriggeringAuthStateChange) { // put the conditional here
store.dispatch("fetchUser", user);
}
})
EventBus.$off('user-registration-causes-auth-change', (payload) => { // this could be extracted into a handler
app.isRegistrationTriggeringAuthStateChange = payload;
})
This is rough code, but the goal is to convey the information. You can clean it up as you please.

Related

How to commit vuex muation form within the same file (in Nuxt)

I followed this tutorial to setup a firebase auth store in vuex (https://www.youtube.com/watch?v=n9cERWIRgMw&list=PL4cUxeGkcC9jveNu1TI0P62Dn9Me1j9tG&index=10)
However, I'm using Nuxt which causes the last step to break.
My file:
import { auth } from "~/plugins/firebase.js";
import {
createUserWithEmailAndPassword,
signInWithEmailAndPassword,
signOut,
onAuthStateChanged,
} from "firebase/auth";
export const state = () => ({
user: null,
authIsReady: false,
});
export const mutations = {
setUser(state, payload) {
state.user = payload;
console.log("user state changed:", state.user);
},
setAuthIsReady(state, payload) {
state.authIsReady = payload;
},
};
export const actions = {
async signup(context, { email, password }) {
console.log("signup action");
const res = await createUserWithEmailAndPassword(auth, email, password);
if (res) {
context.commit("setUser", res.user);
} else {
throw new Error("could not complete signup");
}
},
async login(context, { email, password }) {
console.log("login action");
const res = await signInWithEmailAndPassword(auth, email, password);
if (res) {
context.commit("setUser", res.user);
} else {
throw new Error("could not complete login");
}
},
async logout(context) {
console.log("logout action");
await signOut(auth);
context.commit("setUser", null);
},
};
const unsub = onAuthStateChanged(auth, (user) => {
store.commit("setAuthIsReady", true);
store.commit("setUser", user);
unsub();
});
Error:
Uncaught (in promise) ReferenceError: store is not defined
at eval (index.js?9101:56:1)
at eval (index-6de4cbb9.js?3d11:2453:1)
How do I commit a mutation from here? I tried loads of things, like this.$store, $store etc.

How can I use the current status of redux after the thunks and actions have finished?

How can I use the current status of redux after the thunks and actions have finished? The problem is in the handleSubmit function if I register a user with errors, it updates the status of redux with the message "Email already registered", but when accessing the state in the dispatch promise sends me a wrong state, without the message.
Function hanldeSubmit
const handleSubmit = (e) => {
e.preventDefault()
const form = {
name: e.target[0].value,
email: e.target[1].value,
password: e.target[2].value,
confirmPassword: e.target[3].value
}
const { name, email, password } = form
if (isFormValid(form)) {
//TODO: FIX IT synchronize redux with errors
dispatch( startRegisterUser(name, email, password) ).then(() => {
console.log(state)
})
}
}
register action and thunk
export const startRegisterUser = (name, email, password) => {
return (dispatch, state) => {
dispatch(startLoadingAction())
return firebase.auth().createUserWithEmailAndPassword(email, password)
.then(async ({ user }) => {
await user.updateProfile({
displayName: name,
photoURL: ''
})
dispatch(registerUserAction(user.uid, user.displayName))
})
.catch(e => {
if (e.code === "auth/email-already-in-use") {
dispatch(errorAction("Email already registered"))
} else {
dispatch(errorAction("Unexpected error"))
}
})
.then(() => {
dispatch(finishLoadingAction())
console.log("finished dispatch's", state())
return
})
}
}
export const registerUserAction = (uid, displayname) => {
return {
type: types.register,
payload: {
uid,
displayname
}
}
}
console logs
I want to get the status of the first console log but in the handlesubmit function
You should handle the errorAction in the reducer, update the ui store slice with the error message. And, you need to return the state() in the promise in the thunk function. Then, you will get the whole state inside the handleSubmit event handler.
E.g.
import { applyMiddleware, createStore } from 'redux';
import thunk from 'redux-thunk';
function errorAction(message) {
return {
type: 'ERROR',
payload: message,
error: true,
};
}
export const startRegisterUser = (name, email, password) => {
return (dispatch, state) => {
return Promise.reject({ code: 'auth/email-already-in-use' })
.catch((e) => {
if (e.code === 'auth/email-already-in-use') {
dispatch(errorAction('Email already registered'));
} else {
dispatch(errorAction('Unexpected error'));
}
})
.then(() => state());
};
};
export const registerUserAction = (uid, displayname) => {
return {
type: 'REGISTER',
payload: {
uid,
displayname,
},
};
};
function rootReducer(state = { ui: { error: '' } }, action) {
switch (action.type) {
case 'ERROR':
return { ui: { error: action.payload } };
default:
return state;
}
}
const store = createStore(rootReducer, applyMiddleware(thunk));
function handleSubmit() {
store
.dispatch(startRegisterUser('name', 'example#gmail.com', '123') as any)
.then((state) => {
console.log('handleSubmit state: ', state);
});
}
// triggered by user submit event
handleSubmit();
Output:
handleSubmit state: { ui: { error: 'Email already registered' } }

testing login api with jest

how to test this Api and get 100% score of testing coverage?
const login = async (email, password) => {
axios
.post('https://conduit.productionready.io/api/users/login', {
user: {
email,
password,
},
})
.then((response) => response);
};
Your function is relatively simple : one path, no branching logic, one external call.
All your function do is calling an endpoint through axios.post.
login.js
export const login = async (email, password) => {
/*
* Notice that I added the 'await', else 'async' is useless.
* Else you can directly return the axios.post method.
*/
await axios
.post('https://conduit.productionready.io/api/users/login', {
user: {
email,
password,
},
})
.then((response) => response); // This line is also useless for the moment
};
login.spec.js
import { login } from './login';
// Mock axios, else you will really request the endpoint
jest.mock('axios');
import axios from 'axios';
describe('Login tests', () => {
describe('login function', () => {
const email = 'test#test.com';
const password = 'password';
beforeEach(() => {
/*
* Not necessary for the moment, but will be useful
* to test successful & error response
*/
axios.post.mockResolvedValue({});
});
it('should call endpoint with given email & password', async () => {
await login(email, password);
expect(axios.post).toBeCalledWith(
'https://conduit.productionready.io/api/users/login',
{ user: { email, password } },
);
});
});
});
Notice that you could greatly improve your login function by returning something and handling error with an Authentication Error. Your tests would be more significant :
errors.js
export class DefaultError extends Error {
static STATUS_CODE = 500; // You can change it, it depends how you use it
name = 'DefaultError';
constructor() {
super('Default error, add what you want');
}
}
export class AuthenticationError extends Error {
static STATUS_CODE = 401;
name = 'AuthenticationError';
constructor() {
super('Wrong credentials');
}
}
login.js
import { AuthenticationError, DefaultError } from './errors';
export const login = async (email, password) =>
axios
.post('https://conduit.productionready.io/api/users/login', {
user: {
email,
password,
},
})
.then(response => response.data)
.catch(error => {
// Handles the error how you want it
if (error.status === AuthenticationError.STATUS_CODE) {
throw new AuthenticationError();
}
throw new DefaultError();
});
login.spec.js
import { login } from './login';
import { AuthenticationError, DefaultError } from './errors';
// Mock axios, else you will really request the endpoint
jest.mock('axios');
import axios from 'axios';
describe('Login tests', () => {
describe('login function', () => {
const email = 'test#test.com';
const password = 'password';
describe('with success', () => {
const data = { something: {} };
beforeEach(() => {
axios.post.mockResolvedValue({ data });
});
it('should call endpoint with given email & password', async () => {
await login(email, password);
expect(axios.post).toBeCalledWith(
'https://conduit.productionready.io/api/users/login',
{ user: { email, password } },
);
});
it('should return response data', async () => {
const response = await login(email, password);
expect(response).toStrictEqual(data);
});
});
describe('with error', () => {
describe('status 401', () => {
beforeEach(() => {
axios.post.mockRejectedValue({ status: 401 });
});
it('should throw AuthenticationError', async () => {
await expect(login(email, password)).rejects.toThrow(AuthenticationError);
});
});
describe('other status', () => {
beforeEach(() => {
axios.post.mockRejectedValue({});
});
it('should throw DefaultError', async () => {
await expect(login(email, password)).rejects.toThrow(DefaultError);
});
});
});
});
});
We could go further but I think you got the point. Btw, you don't need to split the tests as I did, I just enjoy being able to group the describe by the mocks needed and making little & readable tests.

Can't use Redux props in firebase redirect results

I'm trying to build Google Authentication in React + Redux + Firebase. And I want to create user after google authentication. But this.props.createUser() is not working after Redirects on Firebase.
TypeError: Cannot read property 'props' of undefined
app/src/components/home.js
import React, { Component } from "react";
import { connect } from "react-redux";
import firebase from "../config/firebase";
import { createUser } from "../actions";
class Home extends Component {
constructor(props) {
super(props);
this.state = {
user: null
};
}
async componentWillMount() {
console.log("this", this.props);
firebase.auth().onAuthStateChanged(user => {
this.setState({ user });
if (user) {
this.props.history.push("/user/settings");
}
});
firebase
.auth()
.getRedirectResult()
.then(function(result) {
if (result.credential) {
var token = result.credential.accessToken;
console.log("token", token);
}
var user = result.user;
// Successfully got a user but type error in below
this.props.createUser(user);
})
.catch(function(error) {
console.log("error", error);
});
}
onLogin = () => {
const provider = new firebase.auth.GoogleAuthProvider();
firebase.auth().signInWithRedirect(provider);
};
render() {
return (
<button className="btn btnPrimary" onClick={this.onLogin}>
<span>Google Signin</span>
</button>
);
}
}
function mapStateToProps({ user }) {
return { user: user };
}
export default connect(mapStateToProps, { createUser })(Home);
app/src/actions/index.js
import firebase from "../config/firebase";
export function createUser(user) {
console.log('user', user)
}
Since you declare the callback with the function keyword, this inside the function refers to the function itself and not to the class on which you declared the componentWillMount method.
The simplest solution is to use fat arrow notation, as you do elsewhere already:
firebase
.auth()
.getRedirectResult()
.then((result) => {
if (result.credential) {
var token = result.credential.accessToken;
console.log("token", token);
}
var user = result.user;
// Successfully got a user but type error in below
this.props.createUser(user);
})
Also see this answer on the cause of the problem, and other solutions: How to access the correct `this` inside a callback?
You need to use an arrow function here:
firebase
.auth()
.getRedirectResult()
.then(function(result) {
if (result.credential) {
var token = result.credential.accessToken;
console.log("token", token);
}
var user = result.user;
// Successfully got a user but type error in below
this.props.createUser(user);
})
.catch(function(error) {
console.log("error", error);
});
should be:
firebase
.auth()
.getRedirectResult()
.then((result) => {
if (result.credential) {
var token = result.credential.accessToken;
console.log("token", token);
}
var user = result.user;
// Successfully got a user but type error in below
this.props.createUser(user);
})
.catch(function(error) {
console.log("error", error);
});
The function() {} keyword is eating your this value.

Vue js calling action inside action

I have the following actions in my Vuex store:
import { HTTP } from '#/services/http'
import router from '#/router'
export const actions = {
loginUser ({ commit, state }, params) {
HTTP.post('v1/login.json', { email: params.email, password: params.password })
.then(response => {
localStorage.setItem('access_token', response.data.token)
router.push({name: 'Hello'})
}).catch(error => {
commit('SET_LOGIN_ERROR', error.response.data.error)
})
},
myAccount ({ commit }) {
HTTP.get('v1/my_account.json').headers({'Authorization': ('Token token=' + localStorage.getItem('access_token'))})
.then(response => {
commit('SET_USER', response.data)
})
}
}
I want to launch myAccount action when loginUser succeeds. How can I do that?
I've tried something like this:
import { HTTP } from '#/services/http'
import router from '#/router'
export const actions = {
loginUser ({ commit, state }, params) {
HTTP.post('v1/login.json', { email: params.email, password: params.password })
.then(response => {
localStorage.setItem('access_token', response.data.token)
router.push({name: 'Hello'})
}).catch(error => {
commit('SET_LOGIN_ERROR', error.response.data.error)
})
},
myAccount ({ dispatch, commit, state }, payload) {
dispatch('loginUser', payload)
.then((res) => {
console.log('dupa')
// Do this when loginUser finished
})
}
}
but this not works...
actions receive the context object, so you can simply either pass the entire object or add dispatch to your destructuring assignment :
const store = new Vuex.Store({
actions: {
foo(context) {
console.log('foo called');
},
bar({dispatch}) {
setTimeout(() => dispatch('foo'), 1000)
}
}
});
Here's the JSFiddle: https://jsfiddle.net/y1527vxh/
Since vue actions can be asynchronous you can add dispatch handler to an action to call another action when it is done;
export const actions = {
loginUser ({ commit, state }, params) {
... // some http request or what would you like to do
},
myAccount ({ dispatch, commit, state }, payload) {
dispatch('loginUser', payload)
.then((res) => {
...
// Do this when loginUser finished
})
},
}
I am doing autentication in my projects like this, i am using axios btw:
loginUser ({ dispatch, commit, state }, payload) {
let loginData = {
username: payload.username,
password: payload.password
}
return axios.post(state.url, loginData)
.then((res) => {
// You can console.log(res.data) to see if your token data is fine
window.localStorage.setItem('AuthTokens', JSON.stringify(res.data))
dispatch('myAccount', { tokens: res.data })
})
.catch((err) => {
// Error handling...
})
},
myAccount ({ commit, state }, { tokens }) {
let headerOptions = {
// Header options with tokens.access_token...
}
return axios.get(state.url, headerOptions)
.then((res) => {
// You have the user data
console.log(res.data)
})
.catch((err) => {
// Error handling...
})
}

Categories

Resources