Last active
February 28, 2021 21:45
-
-
Save brunoalano/7bccbad91c12d8cc5033d4278649a989 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { createContext, useEffect, useState, useMemo, useContext } from 'react'; | |
import cookie from 'js-cookie'; | |
import firebase from 'lib/auth/firebase'; | |
import { useDocument } from '@nandorojo/swr-firestore'; | |
import type { UserModel } from 'types/UserModel'; | |
// Create a context that will hold the values that we are going to expose to our components. | |
// Don't worry about the `null` value. It's gonna be *instantly* overriden by the component below | |
type UserContextType = { | |
user: UserModel | null; | |
error: unknown; | |
mutate: unknown; | |
signinWithGoogle: () => Promise<null>; | |
signInWithEmailAndPassword: (email: string, password: string) => Promise<null>; | |
signout: () => Promise<null>; | |
}; | |
export const UserContext = createContext<UserContextType>(null); | |
// Create a "controller" component that will calculate all the data that we need to give to our | |
// components bellow via the `UserContext.Provider` component. This is where the Firebase will be | |
// mapped to a different interface, the one that we are going to expose to the rest of the app. | |
type UserProviderProps = { children: React.ReactChild | React.ReactChild[] }; | |
export const UserProvider = ({ children }: UserProviderProps): JSX.Element => { | |
// Handle the Firestore User | |
const [authFirebaseUser, setAuthFirebaseUser] = useState<firebase.User | null>(null); | |
const { data: user, mutate, error } = useDocument<UserModel>( | |
authFirebaseUser ? `users/${authFirebaseUser.uid}` : null, | |
); | |
/** | |
* User provided by Firestore. Create if don't exists. | |
* | |
* @param rawUser Firebase returned User | |
*/ | |
const handleUser = async (rawUser: firebase.User | boolean): Promise<null> => { | |
if (typeof rawUser !== 'boolean' && rawUser) { | |
// Parse the Data | |
const userData = { | |
id: rawUser.uid, | |
email: rawUser.email, | |
token: await rawUser.getIdToken(), | |
}; | |
const { token, ...userWithoutToken } = userData; | |
// Store the Firebase Token into the Cookie | |
cookie.set('chatlog-token', userData, { | |
expires: 1, | |
}); | |
// Get and Create/Update the User in Firestore | |
await firebase.firestore().collection('users').doc(rawUser.uid).set(userWithoutToken, { merge: true }); | |
setAuthFirebaseUser(rawUser); | |
mutate(); | |
return null; | |
} | |
// User not authenticated | |
setAuthFirebaseUser(null); | |
cookie.remove('chatlog-token'); | |
mutate(); | |
return null; | |
}; | |
/** | |
* Authentication Helpers | |
*/ | |
const signinWithGoogle = (): Promise<null> => { | |
return firebase | |
.auth() | |
.signInWithPopup(new firebase.auth.GoogleAuthProvider()) | |
.then((response) => handleUser(response.user)); | |
}; | |
const signInWithEmailAndPassword = (email: string, password: string): Promise<null> => { | |
return firebase | |
.auth() | |
.signInWithEmailAndPassword(email, password) | |
.then((response) => handleUser(response.user)); | |
}; | |
const signout = (): Promise<null> => { | |
return firebase | |
.auth() | |
.signOut() | |
.then(() => handleUser(false)); | |
}; | |
// Listen to Firebase Authentication changes | |
useEffect(() => { | |
const unsubscribe = firebase.auth().onAuthStateChanged(handleUser); | |
return () => unsubscribe(); | |
}, []); | |
// Make sure to not force a re-render on the components that are reading these values, | |
// unless the `user` value has changed. This is an optimisation that is mostly needed in cases | |
// where the parent of the current component re-renders and thus the current component is forced | |
// to re-render as well. If it does, we want to make sure to give the `UserContext.Provider` the | |
// same value as long as the user data is the same. If you have multiple other "controller" | |
// components or Providers above this component, then this will be a performance booster. | |
const values = useMemo(() => ({ user, mutate, error, signinWithGoogle, signInWithEmailAndPassword, signout }), [ | |
user, | |
]); | |
// Finally, return the interface that we want to expose to our other components | |
return <UserContext.Provider value={values}>{children}</UserContext.Provider>; | |
}; | |
// We also create a simple custom hook to read these values from. We want our React components | |
// to know as little as possible on how everything is handled, so we are not only abtracting them from | |
// the fact that we are using React's context, but we also skip some imports. | |
export const useUser = (): UserContextType => { | |
const context = useContext(UserContext); | |
if (context === undefined) { | |
throw new Error('`useUser` hook must be used within a `UserProvider` component'); | |
} | |
return context; | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment