React Native - Authentication - trigger value to change Auth & UnAuth Stack Navigators - reactjs

Here my App.js
import React, { useEffect } from "react";
import { NavigationContainer } from "#react-navigation/native";
import AuthStore from "./src/stores/AuthStore";
import AuthStackNavigator from "./src/navigation/AuthStackNavigator";
import UnAuthStackNavigator from "./src/navigation/UnAuthStackNavigator";
const App = () => {
useEffect(() => {
console.log("APP JS", AuthStore.userAuthenticated);
}, [AuthStore.userAuthenticated]);
return <NavigationContainer>
{AuthStore.userAuthenticated ? <AuthStackNavigator /> : <UnAuthStackNavigator />}
</NavigationContainer>;
};
export default App;
The AuthStore value of userAuthenticated is computed and updated on auto login or login.
Here AuthStore.js
import { userLogin, userRegister } from "../api/AuthAgent";
import { clearStorage, retrieveUserSession, storeUserSession } from "../utils/EncryptedStorage";
import { Alert } from "react-native";
import { computed, makeObservable, observable } from "mobx";
import { setBearerToken } from "../config/HttpClient";
class AuthStore {
user = {};
token = undefined;
refreshToken = undefined;
decodedToken = undefined;
constructor() {
makeObservable(this, {
token: observable,
refreshToken: observable,
user: observable,
decodedToken: observable,
userAuthenticated: computed,
});
this.autoLogin();
}
async doLogin(body) {
const resp = await userLogin(body);
console.log("AuthStore > userLogin > resp => ", resp);
if (resp.success) {
this.decodedToken = await this.getDecodedToken(resp.token);
this.setUserData(resp);
storeUserSession(resp);
} else {
Alert.alert(
"Wrong credentials!",
"Please, make sure that your email & password are correct",
);
}
}
async autoLogin() {
const user = await retrieveUserSession();
if (user) {
this.setUserData(user);
}
}
setUserData(data) {
this.user = data;
this.token = data.token;
setBearerToken(data.token);
}
get userAuthenticated() {
console.log('AuthStore > MOBX - COMPUTED userAuthenticated', this.user);
if (this.token) {
return true;
} else return false;
}
async logout() {
await clearStorage();
this.user = undefined;
this.token = undefined;
this.refreshToken = undefined;
this.decodedToken = undefined;
}
}
export default new AuthStore();
The main problem is that the AuthStore.userAuthenticated value even when it changes on AuthStore it does not triggered by useEffect of the App.js.
So, when I log in or log out I have to reload the App to trigger the useEffect hook and then the navigators are only updated.

You can use useMemo hook to achive this.
const App = () => {
const [userToken, setUserToken] = useState("")
const authContext: any = useMemo(() => {
return {
signIn: (data: any) => {
AsyncStorage.setValue("token", data.accessToken);
setUserToken(data.accessToken);
},
signOut: () => {
setUserToken("");
AsyncStorage.setValue("token", "");
},
};
}, []);
return (
<AuthContext.Provider value={authContext}>
{userToken.length ? (
<UnAuthStackNavigator />
) : (
<AuthStackNavigator />
)}
)
</AuthContext.Provider>
)
}
AuthContext.ts
import React from "react";
export const AuthContext: any = React.createContext({
signIn: (res: any) => {},
signOut: () => {},
});
Now you can use this functions in any files like this:
export const SignIn = () => {
const { signIn } = useContext(AuthContext);
return (
<Button onPress={() => {signIn()}} />
)
}

If your primary purpose is to navigate in and out of stack if authentication is available or not then asynstorage is the best option you have to first
store token.
const storeToken = async (value) => {
try {
await AsynStorage.setItem("userAuthenticated", JSON.stringify(value));
} catch (error) {
console.log(error);
}
};
("userAuthenticated" is the key where we get the value )
Now go to the screen where you want this token to run turnery condition
const [token, setToken] = useState();
const getToken = async () => {
try {
const userData = JSON.parse(await AsynStorage.getItem("userAuthenticated"))
setToken(userData)
} catch (error) {
console.log(error);
}
};
Now use the token in the state then run the condition:
{token? <AuthStackNavigator /> : <UnAuthStackNavigator />}
or
{token != null? <AuthStackNavigator /> : <UnAuthStackNavigator />}

Related

Stay logged in on refresh, JWT

On my website switching between pages is completely fine and works (it doesnt refresh or load due to redux) but the moment the page is refreshed or i manually enter a link to access, it logs me out. Also it just started happening now, yesterday when i was working on some other things in code, it never logged me out when I manually with links/urls navigated thru website or refreshing but now for some reason it doesnt work and I'm 99% sure I havent touched any auth part of the code...
This is my code:
authApiSlice:
import { apiSlice } from "../../app/api/apiSlice";
import { logOut, setCredentials } from "./authSlice";
export const authApiSlice = apiSlice.injectEndpoints({
endpoints: (builder) => ({
login: builder.mutation({
query: (credentials) => ({
url: "/auth",
method: "POST",
body: { ...credentials },
}),
}),
sendLogout: builder.mutation({
query: () => ({
url: "/auth/logout",
method: "POST",
}),
async onQueryStarted(arg, { dispatch, queryFulfilled }) {
try {
const { data } = await queryFulfilled;
console.log(data);
dispatch(logOut());
setTimeout(() => {
dispatch(apiSlice.util.resetApiState());
}, 1000);
} catch (err) {
console.log(err);
}
},
}),
refresh: builder.mutation({
query: () => ({
url: "/auth/refresh",
method: "GET",
}),
async onQueryStarted(arg, { dispatch, queryFulfilled }) {
try {
const { data } = await queryFulfilled;
console.log(data);
const { accessToken } = data;
dispatch(setCredentials({ accessToken }));
} catch (err) {
console.log(err);
}
},
}),
}),
});
export const { useLoginMutation, useSendLogoutMutation, useRefreshMutation } =
authApiSlice;
authSlice:
import { createSlice } from "#reduxjs/toolkit";
const authSlice = createSlice({
name: "auth",
initialState: { token: null },
reducers: {
setCredentials: (state, action) => {
const { accessToken } = action.payload;
state.token = accessToken;
},
logOut: (state, action) => {
state.token = null;
},
},
});
export const { setCredentials, logOut } = authSlice.actions;
export default authSlice.reducer;
export const selectCurrentToken = (state) => state.auth.token;
import { Outlet, Link } from "react-router-dom";
import { useEffect, useRef, useState } from "react";
import { useRefreshMutation } from "./authApiSlice";
import usePersist from "../../hooks/usePersist";
import { useSelector } from "react-redux";
import { selectCurrentToken } from "./authSlice";
const PersistLogin = () => {
const [persist] = usePersist();
const token = useSelector(selectCurrentToken);
const effectRan = useRef(false);
const [trueSuccess, setTrueSuccess] = useState(false);
const [refresh, { isUninitialized, isLoading, isSuccess, isError, error }] =
useRefreshMutation();
useEffect(() => {
if (effectRan.current === true || process.env.NODE_ENV !== "development") {
// React 18 Strict Mode
const verifyRefreshToken = async () => {
console.log("verifying refresh token");
try {
//const response =
await refresh();
//const { accessToken } = response.data
setTrueSuccess(true);
} catch (err) {
console.error(err);
}
};
if (!token && persist) verifyRefreshToken();
}
return () => (effectRan.current = true);
// eslint-disable-next-line
}, []);
let content;
if (!persist) {
// persist: no
console.log("no persist");
content = <Outlet />;
} else if (isLoading) {
//persist: yes, token: no
console.log("loading");
} else if (isError) {
//persist: yes, token: no
console.log("error");
content = (
<p className="errmsg">
{`${error?.data?.message} - `}
<Link to="/login">Please login again</Link>.
</p>
);
} else if (isSuccess && trueSuccess) {
//persist: yes, token: yes
console.log("success");
content = <Outlet />;
} else if (token && isUninitialized) {
//persist: yes, token: yes
console.log("token and uninit");
console.log(isUninitialized);
content = <Outlet />;
}
return content;
};
export default PersistLogin;
RequireAuth
import { useLocation, Navigate, Outlet } from "react-router-dom";
import useAuth from "../../hooks/useAuth";
const RequireAuth = ({ allowedRoles }) => {
const location = useLocation();
const { roles } = useAuth();
const content = roles.some((role) => allowedRoles.includes(role)) ? (
<Outlet />
) : (
<Navigate to="/prijava" state={{ from: location }} replace />
);
return content;
};
export default RequireAuth;
It just stopped worked for some reason, it shouldnt log me out when I refresh.

Why does the background color only change when the page is reloaded and not after a successful login?

When the user hits the login button, it redirects to the Unsplash login page. After a successful login, the page redirects back to "localhost" with the "code=" parameter in the URL (http://localhost:3000/?code=VbnuDo5fKJE16cjR#=). After that, I need to get the username of the current user and change the background color of his liked images.
Why does the background color only change when the page is reloaded and not after a successful login?
There are too many requests happening at the same time and I don't know how to handle them properly.
Home.js
import React, { useState, useEffect } from "react";
import axios from "axios";
import ImageList from "../components/ImageList";
import SearchBar from "../components/SearchBar";
import Loader from "../helpers/Loader";
import Login from "../components/Login";
function Home() {
const [page, setPage] = useState(1);
const [query, setQuery] = useState("landscape");
const [images, setImages] = useState([]);
const [loading, setLoading] = useState(false);
const clientId = process.env.REACT_APP_UNSPLASH_KEY;
const url = `https://api.unsplash.com/search/photos?page=${page}&query=${query}&client_id=${clientId}&per_page=30`;
const fetchImages = () => {
setLoading(true);
axios
.get(url)
.then((response) => {
setImages([...images, ...response.data.results]);
})
.catch((error) => console.log(error))
.finally(() => {
setLoading(false);
});
setPage(page + 1);
};
useEffect(() => {
fetchImages();
setQuery("");
}, []);
return (
<div>
<Login />
{loading && <Loader />}
<ImageList images={images} />
</div>
);
}
export default Home;
Login.js
import React, { useEffect } from "react"
import { useAppContext } from "../context/appContext";
function Login() {
const { handleClick, getToken, token, getUserProfile } = useAppContext();
useEffect(() => {
if (window.location.search.includes("code=")) {
getToken();
}
if (token) {
getUserProfile();
}
}, [token]);
return (
<div>
<button onClick={() => handleClick()}>Log in</button>
</div>
);
}
export default Login;
appContext.js
import React, { useReducer, useContext } from "react";
import reducer from "./reducer";
import axios from "axios";
import {SET_TOKEN,SET_LIKED_PHOTOS_ID } from "./actions";
const token = localStorage.getItem("token");
const username = localStorage.getItem("username");
const initialState = {
token: token,
username: username,
likedPhotosId: [],
};
const AppContext = React.createContext();
const AppProvider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState);
const handleClick = () => {
window.location.href = `${api_auth_uri}?client_id=${client_id}&redirect_uri=${redirect_uri}&response_type=${response_type}&scope=${scope.join(
"+"
)}`;
};
const getToken = async () => {
const urlCode = window.location.search.split("code=")[1];
try {
const { data } = await axios.post(
`${api_token_uri}?client_id=${client_id}&client_secret=${client_secret}&redirect_uri=${redirect_uri}&code=${urlCode}&grant_type=${grant_type}`
);
const { access_token } = data;
localStorage.setItem("token", access_token);
dispatch({
type: SET_TOKEN,
payload: { access_token },
});
} catch (error) {
console.log(error);
}
};
const getUserProfile = async () => {
try {
const { data } = await axios.get(`https://api.unsplash.com/me`, {
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + state.token,
},
});
const { username } = data;
localStorage.setItem("username", username);
} catch (error) {
console.log(error);
}
};
const getLikedPhotos = async () => {
try {
const { data } = await axios.get(
`https://api.unsplash.com/users/${state.username}/likes`,
{
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + state.token,
},
}
);
const likedPhotosId = data.map((photo) => photo.id);
dispatch({
type: SET_LIKED_PHOTOS_ID,
payload: { likedPhotosId },
});
} catch (error) {
console.log(error);
}
};
return (
<AppContext.Provider
value={{
...state,
handleClick,
getToken,
getUserProfile,
getLikedPhotos,
}}
>
{children}
</AppContext.Provider>
);
};
const useAppContext = () => useContext(AppContext);
export { AppProvider, initialState, useAppContext };
ImageList.js
import React, {useEffect } from "react";
import "../styles/ImageList.scss";
import { useAppContext } from "../context/appContext";
function ImageList({ images }) {
const { username, likedPhotosId, getLikedPhotos } = useAppContext();
useEffect(() => {
if (username) {
getLikedPhotos();
}
}, [username]);
return (
<div className="result">
{images?.map((image) => (
<div
style={{
backgroundColor: likedPhotosId?.includes(image.id) ? "red" : "",
}}
>
<div key={image.id}>
<img src={image.urls.small} alt={image.alt_description} />
</div>
</div>
))}
</div>
);
}
export default ImageList;
reducer.js
import { SET_TOKEN, SET_LIKED_PHOTOS_ID } from "./actions";
const reducer = (state, action) => {
if (action.type === SET_TOKEN) {
return {
...state,
token: action.payload.access_token,
};
}
if (action.type === SET_LIKED_PHOTOS_ID) {
return {
...state,
likedPhotosId: action.payload.likedPhotosId,
};
}
throw new Error(`no such action : ${action.type}`);
};
export default reducer;
The problem is in your function. You save the username in localStorage but not in your reducer state:
const getUserProfile = async () => {
try {
const { data } = await axios.get(`https://api.unsplash.com/me`, {
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + state.token,
},
});
const { username } = data;
localStorage.setItem("username", username);
} catch (error) {
console.log(error);
}
};
The issue here is that react doesn't trigger a rerender of components when you set something in localStorage and in your ImageList component use have a useEffect expecting username to change before calling the getLikedPhotos:
const { username, likedPhotosId, getLikedPhotos } = useAppContext();
useEffect(() => {
if (username) {
getLikedPhotos();
}
}, [username]);
So to fix you need to add an action for setting the username state in your reducer:
import { SET_TOKEN, SET_LIKED_PHOTOS_ID, SET_USERNAME } from "./actions";
const reducer = (state, action) => {
if (action.type === SET_TOKEN) {
return {
...state,
token: action.payload.access_token,
};
}
if (action.type === SET_USERNAME) {
return {
...state,
username: action.payload.username,
};
}
if (action.type === SET_LIKED_PHOTOS_ID) {
return {
...state,
likedPhotosId: action.payload.likedPhotosId,
};
}
throw new Error(`no such action : ${action.type}`);
};
export default reducer;
And then dispatch that action from the getUserProfile:
const getUserProfile = async () => {
try {
const { data } = await axios.get(`https://api.unsplash.com/me`, {
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + state.token,
},
});
const { username } = data;
localStorage.setItem("username", username);
dispatch({
type: SET_USERNAME,
payload: { username },
});
} catch (error) {
console.log(error);
}
};

Handling user context on logout for user.isAdmin in ReactJs / Uncaught TypeError: user is null

I am trying to build a functionality in ReacJs (MERN) app, for my navbar link to '/admin',
to be only visible when user.isAdmin === true.
So my navbar (just an excerpt) looks the follwing:
Navbar
import { useAuthContext } from '../hooks/useAuthContext'
import { useIsAdmin } from '../hooks/useAdmin'
const Navbar = () => {
const [nav, setNav] = useState(false)
const {logout} = useLogout()
const {user} = useAuthContext()
const isAdmin = useIsAdmin()
//and on return
{ isAdmin ? <Link to='/admin'><li className='p-4 hover:text-[#00df9a] transition-all duration-500'>Admin {user.name}</li></Link> : <div></div> }
I as well made a function to get the user.isAdmin from AuthContext
useAdmin
import { useEffect, useState } from 'react';
import { useAuthContext} from '../hooks/useAuthContext';
export function useIsAdmin() {
const { user } = useAuthContext();
const [isAdmin, setIsAdmin] = useState(null);
useEffect(() => {
if (user) {
setIsAdmin(user.isAdmin && user);
}
}, [user]);
useEffect(() => {
if (!user || user === null) {
setIsAdmin(null);
}
}, [user]);
return isAdmin;
}
And this works okay, normal user does not see the /admin link, and user.isAdmin does.
However, the problem starts when I try to logout the user.isAdmin, then I receive "Uncaught TypeError: user is null"
as user changes back to Object { user: null }. On contrary,
I do not have that error, if I log out regular user, although it comes back to Object { user: null } as well.
I have tried working on my hook/function with no result, but I am guessing there is some problem with my authContext and global context for user.
So for reference my authContext file and logout.
Any hints and tips would be much appreciated.
AuthContext
import { createContext, useReducer, useEffect } from 'react'
export const AuthContext = createContext()
export const authReducer = (state, action) => {
switch (action.type) {
case 'LOGIN':
return { user: action.payload }
case 'LOGOUT':
window.localStorage.clear()
return {user: null}
case 'DELETE_USER':
return {...state, user: state.user.filter(u =>
u.id !== action.payload
)}
default:
return state
}
}
export const AuthContextProvider = ({ children }) => {
const [state, dispatch] = useReducer(authReducer, {
user: null
})
useEffect(() => {
const user = JSON.parse(localStorage.getItem('user'))
if (user) {
dispatch({ type: 'LOGIN', payload: user })
}
}, [])
console.log('AuthContext state:', state)
return (
<AuthContext.Provider value={{ ...state, dispatch }}>
{ children }
</AuthContext.Provider>
)
}
useLogout
import { useAuthContext } from './useAuthContext'
export const useLogout = () => {
const { dispatch } = useAuthContext()
const logout = () => {
// remove user from local storage
localStorage.removeItem('user')
// dispatch logout action
dispatch({ type: 'LOGOUT' })
}
return { logout }
}
Well, i forgot to add user to my if check in navbar. After this change, all works like a charm.
Navbar
{ isAdmin && user ? <Link to='/admin'><li className='p-4 hover:text-[#00df9a] transition-all duration-500'>Admin {user.name}</li></Link> : <div></div> }

React context not updating from after promise completed

I have a React context which I am using to manage the authentication within my application. I have done this previously and all seemed OK, but in this application the value of the isAuthenticated property is not being updated. I've tried to replicate using CodeSanbox but I get the expected result.
Essentially, I want the context to hold a value of isAuthenticating: true until the authentication flow has finished, once this has finished I will determine if the user is authenticated by checking isAuthenticated === true && authenticatedUser !== undefined however, the state does not seem to be getting updated.
As a bit of additional context to this, I am using turborepo and next.js.
AuthenticationContext:
import { SilentRequest } from '#azure/msal-browser';
import { useMsal } from '#azure/msal-react';
import { User } from 'models';
import { createContext, useCallback, useContext, useEffect, useState } from "react";
import { msal, sendRequest } from 'utils';
interface AuthenticationContextType {
authenticatedUser?: User;
isAuthenticating: boolean;
}
const AuthenticationContext = createContext<AuthenticationContextType>({
authenticatedUser: undefined,
isAuthenticating: true
});
export const AuthenticationProvider = (props: { children: React.ReactNode }) => {
const { accounts, instance } = useMsal();
const [user, setUser] = useState<User>();
const [isAuthenticating, setIsAuthenticating] = useState<boolean>(true);
const [currentAccessToken, setCurrentAccessToken] = useState<string>();
const getUserFromToken = useCallback(async () => {
if (user) {
setIsAuthenticating(false);
return;
}
const userRequest = await sendRequest('me');
if (! userRequest.error && userRequest.data) {
setUser(userRequest.data as User);
}
}, [user]);
const getAccessToken = useCallback(async () => {
if (! currentAccessToken) {
const request: SilentRequest = {
...msal.getRedirectRequest(),
account: accounts[0]
}
const response = await instance.acquireTokenSilent(request);
setCurrentAccessToken(response.accessToken);
}
return getUserFromToken();
}, [accounts, currentAccessToken, getUserFromToken, instance]);
useEffect(() => {
async function initialiseAuthentication() {
await getAccessToken();
setIsAuthenticating(false);
}
initialiseAuthentication();
}, [getAccessToken]);
return (
<AuthenticationContext.Provider value={{ authenticatedUser: user, isAuthenticating }}>
{ props.children }
</AuthenticationContext.Provider>
)
}
export const useAuth = () => {
const context = useContext(AuthenticationContext);
if (context === undefined) {
throw new Error("useAuth was used outside of it's provider.")
}
return context;
}
AuthenticationLayout:
import { useEffect, useState } from 'react';
import { AuthenticationProvider, useAuth } from '../hooks/authentication';
import MsalLayout from './msal-layout';
const AuthenticationLayout = (props: { children: React.ReactNode }) => {
const { isAuthenticating, authenticatedUser } = useAuth();
const wasAuthenticationSuccessful = () => {
return ! isAuthenticating && authenticatedUser !== undefined;
}
const renderContent = () => {
if (! wasAuthenticationSuccessful()) {
return (
<p>You are not authorized to view this application.</p>
)
}
return props.children;
}
if (isAuthenticating) {
return (
<p>Authenticating...</p>
)
}
return (
<MsalLayout>
{ renderContent() }
</MsalLayout>
)
}
export default AuthenticationLayout;
MsalLayout:
import { InteractionType } from '#azure/msal-browser';
import {
AuthenticatedTemplate,
MsalAuthenticationTemplate,
MsalProvider,
} from "#azure/msal-react";
import { msalInstance, msal } from 'utils';
import { AuthenticationProvider } from '../hooks/authentication';
msal.initialize();
const MsalLayout = (props: { children: React.ReactNode }) => {
return (
<MsalProvider instance={msalInstance}>
<MsalAuthenticationTemplate interactionType={InteractionType.Redirect} authenticationRequest={msal.getRedirectRequest()}>
<AuthenticatedTemplate>
<AuthenticationProvider>
{props.children}
</AuthenticationProvider>
</AuthenticatedTemplate>
</MsalAuthenticationTemplate>
</MsalProvider>
)
}
export default MsalLayout;
Theoretically, once the authentication is finished I would expect the props.children to display.
I think that the problem is AuthenticationLayout is above the provider. You have consumed the provider in MsalLayout. Then AuthenticationLayout uses MsalLayout so the AuthenticationLayout component is above the provider in the component tree. Any component that consumes the context, needs to be a child of the provider for that context.
Therefore the context is stuck on the static default values.
Your capture of this scenario in useAuth where you throw an error is not warning you of this as when its outside the context -- context is not undefined, it is instead the default values which you pass to createContext. So your if guard isn't right.
There are some workarounds to checking if its available -- for example you could use undefined in the default context for isAuthenticating and authenticatedUser and then check that. Or you can change them to getters and set the default context version of this function such that it throws an error.

How to set the default react context value as data from firestore?

I'm building a workout program planner app, the workout program is handled in the app with a SetProgram context and is updated with a custom hook called useProgram. I need that when the user logins that the app will fetch data from firestore and display the user's workout program, how can I do this? Keeping in mind that the useProgram hook is also used throughout the app to edit and update one's workout program.
App.tsx
import React, { useContext, useEffect, useState } from "react";
import { BrowserRouter as Router } from "react-router-dom";
import AppRouter from "./Router";
import FirebaseApp from "./firebase";
import SetProgram from "./context/program";
import { useProgram } from "./components/hooks/useProgram";
import firebaseApp from "./firebase/firebase";
import { useAuthState } from "react-firebase-hooks/auth";
function App() {
const program = useProgram();
const day = useDay();
const [user, loading, error] = useAuthState(firebaseApp.auth);
return (
<div className="App">
<SetProgram.Provider value={program}>
<Router>
<AppRouter />
</Router>
</SetProgram.Provider>
</div>
);
}
export default App;
firebase.ts
import firebase from "firebase/app";
import "firebase/auth";
import "firebase/firestore";
import firebaseConfig from "./config";
class Firebase {
auth: firebase.auth.Auth;
user: firebase.User | null | undefined;
db: firebase.firestore.Firestore;
userProgram: {} | undefined;
constructor() {
firebase.initializeApp(firebaseConfig);
this.auth = firebase.auth();
this.db = firebase.firestore();
}
async register() {
if (this.user) {
this.db.collection("users").doc(this.user.uid).set({
name: this.user.displayName,
email: this.user.email,
userId: this.user.uid,
program: {},
});
}
}
async getResults() {
return await this.auth.getRedirectResult().then((results) => {
console.log("results.user", results.user);
if (!results.additionalUserInfo?.isNewUser) {
this.getProgram();
} else {
this.register();
}
});
}
async login(
user: firebase.User | null | undefined,
loading: boolean,
error: firebase.auth.Error | undefined
) {
const provider = new firebase.auth.GoogleAuthProvider();
return await this.auth
.signInWithRedirect(provider)
.then(() => this.getResults());
}
async logout() {
return await this.auth.signOut().then(() => console.log("logged out"));
}
async updateProgram(user: firebase.User, program: {}) {
if (this.userProgram !== program) {
firebaseApp.db
.collection("users")
.doc(user.uid)
.update({
program: program,
})
.then(() => console.log("Program updated successfully!"))
.catch((error: any) => console.error("Error updating program:", error));
} else {
console.log("No changes to the program!");
}
}
async getProgram() {
firebaseApp.db
.collection("users")
.doc(this.user?.uid)
.get()
.then((doc) => {
console.log("hello");
if (doc.exists) {
this.userProgram = doc.data()?.program;
console.log("this.userProgram", this.userProgram);
} else {
console.log("doc.data()", doc.data());
}
});
}
}
const firebaseApp = new Firebase();
export default firebaseApp;
programContext.tsx
import React from "react";
import Program, { muscleGroup, DefaultProgram } from "../interfaces/program";
export interface ProgramContextInt {
program: Program | undefined;
days: Array<[string, muscleGroup]> | undefined;
setProgram: (p: Program) => void;
}
export const DefaultProgramContext: ProgramContextInt = {
program: undefined,
days: undefined,
setProgram: (p: Program): void => {},
};
const ProgramContext = React.createContext<ProgramContextInt>(
DefaultProgramContext
);
export default ProgramContext;
useProgram.tsx
import React from "react";
import {
ProgramContextInt,
DefaultProgramContext,
} from "../../context/program";
import Program, { muscleGroup } from "../../interfaces/program";
import { useAuthState } from "react-firebase-hooks/auth";
import firebaseApp from "../../firebase";
export const useProgram = (): ProgramContextInt => {
const [user] = useAuthState(firebaseApp.auth);
const [program, setEditedProgram] = React.useState<Program | undefined>();
const [days, setProgramDays] = React.useState<
[string, muscleGroup][] | undefined
>(program && Object.entries(program));
const setProgram = React.useCallback(
(program: Program): void => {
firebaseApp.updateProgram(user, program);
setEditedProgram(program);
setProgramDays(Object.entries(program));
},
[user]
);
return {
program,
days,
setProgram,
};
};
There are two ways to handle this in my opinion:
Update the ProgramContext to make sure that the user is logged in
Wrap the App or any other entry point from whence you need to make sure that the user is logged in, in a separate UserContextProvider
Let's talk about the latter method, where we can wrap in a separate context called UserContext. Firebase provides us a listener called onAuthStateChanged, which we can make use of in our context, like so:
import { createContext, useEffect, useState } from "react";
import fb from "services/firebase"; // you need to define this yourself. It's just getting the firebase instance. that's all
import fbHelper from "services/firebase/helpers"; // update path based on your project organization
type FirestoreDocSnapshot = firebase.default.firestore.DocumentSnapshot<firebase.default.firestore.DocumentData>;
const UserContext = createContext({ user: null, loading: true });
const UserContextProvider = ({ children }) => {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const userContext = { user, loading };
const updateUser = (snapShot: FirestoreDocSnapshot) => {
setUser({
id: snapShot.id,
...snapShot.data,
});
};
const authStateListener = async (authUser: firebase.default.User) => {
try {
if (!authUser) {
setUser(authUser);
return;
}
const fbUserRef = await fbHelper.findOrCreateFirestoreUser(authUser)
if ("error" in fbUserRef) throw new Error(fbUserRef?.error);
(fbUserRef as FirestoreUserRef).onSnapshot(updateUser)
} catch (error) {
throw error;
} finally {
setLoading(false);
}
};
useEffect(() => {
const unSubscribeAuthStateListener = fb.auth.onAuthStateChanged(authStateListener);
return () => unSubscribeAuthStateListener();
}, [])
return (
<UserContext.Provider value={userContext}>
{children}
</UserContext.Provider>
)
};
export default UserContextProvider;
Where the helper can be something like this:
export type FirestoreUserRef = firebase.default.firestore.DocumentReference<firebase.default.firestore.DocumentData>
const findOrCreateFirestoreUser = async (authUser: firebase.default.User, additionalData = {}): Promise<FirestoreUserRef | { error?: string }> => {
try {
if (!authUser) return { error: 'authUser is missing!' };
const user = fb.firestore.doc(`users/${authUser.uid}`); // update this logic according to your schema
const snapShot = await user.get();
if (snapShot.exists) return user;
const { email } = authUser;
await user.set({
email,
...additionalData
});
return user;
} catch (error) {
throw error;
}
};
Then wrap your other context which provides firestore data, within this UserContextProvider. Thus whenever you login or logout, this particular listener be invoked.

Resources