Skip to content

Commit

Permalink
Merge pull request #29 from nushydude/28-fetch-profile-after-logging-…
Browse files Browse the repository at this point in the history
…in-and-at-app-launch

Fetch user profile on login and at app launch. Also, fetch access tok…
  • Loading branch information
nushydude authored Nov 7, 2023
2 parents 9c7a44c + 6053a5a commit 5b41645
Show file tree
Hide file tree
Showing 13 changed files with 417 additions and 70 deletions.
39 changes: 33 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"chart.js": "^3.9.1",
"date-fns": "^2.29.3",
"fast-deep-equal": "^3.1.3",
"jwt-decode": "^4.0.0",
"memoize-one": "^6.0.0",
"query-string": "^8.1.0",
"react": "^18.2.0",
Expand All @@ -41,6 +42,7 @@
"cypress": "^10.8.0",
"husky": "^8.0.3",
"lint-staged": "^13.1.2",
"mockdate": "^3.0.5",
"postcss-scss": "^4.0.6",
"prettier": "2.8.4",
"stylelint": "^13.13.1",
Expand Down
11 changes: 11 additions & 0 deletions src/context/user.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import React from 'react';

export type Profile = {
firstname: string;
lastname: string;
email: string;
settings: Record<string, any> | null;
};

export type UserContextType = {
fetchAccessToken: () => any;
isLoggedIn: boolean;
Expand All @@ -9,6 +16,8 @@ export type UserContextType = {
setRefreshToken: (refreshToken: string) => void;
accessToken?: string | null;
refreshToken?: string | null;
profile: Profile | null;
fetchProfile: () => Promise<Profile | null>;
};

export const UserContext = React.createContext<UserContextType>({
Expand All @@ -20,4 +29,6 @@ export const UserContext = React.createContext<UserContextType>({
setRefreshToken: () => {},
accessToken: null,
refreshToken: null,
profile: null,
fetchProfile: () => Promise.resolve(null),
});
63 changes: 34 additions & 29 deletions src/hooks/usePersistedState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,41 +24,46 @@ const usePersistedState = (
}
}, [stateKey, state, hasMounted]);

useEffect(() => {
const loadPersistedState = () => {
try {
const persistedState = localStorage.getItem(stateKey);
useEffect(
() => {
const loadPersistedState = () => {
try {
const persistedState = localStorage.getItem(stateKey);

if (persistedState) {
const parsedState = JSON.parse(persistedState);
if (persistedState) {
const parsedState = JSON.parse(persistedState);

// Filter out any fields from parsedState that are not present in initialState,
// in case we want to clean up the staled states in the future.
const filteredState = Object.keys(initialState).reduce(
(result, key) => {
if (parsedState[key]) {
return {
...result,
[key]: parsedState[key],
};
}
// Filter out any fields from parsedState that are not present in initialState,
// in case we want to clean up the staled states in the future.
const filteredState = Object.keys(initialState).reduce(
(result, key) => {
if (parsedState[key]) {
return {
...result,
[key]: parsedState[key],
};
}

return result;
},
initialState,
);
return result;
},
initialState,
);

setState(filteredState);
setState(filteredState);
}
} catch (error) {
// Ignore
} finally {
setHasMounted(true);
}
} catch (error) {
// Ignore
} finally {
setHasMounted(true);
}
};
};

loadPersistedState();
}, []);
loadPersistedState();
},
// Only run this effect once, when the component mounts.
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
);

return hasMounted;
};
Expand Down
8 changes: 4 additions & 4 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ const root = ReactDOM.createRoot(

root.render(
<React.StrictMode>
<AppSettingsProvider>
<UserProvider>
<UserProvider>
<AppSettingsProvider>
<App />
</UserProvider>
</AppSettingsProvider>
</AppSettingsProvider>
</UserProvider>
</React.StrictMode>,
);

Expand Down
16 changes: 6 additions & 10 deletions src/providers/AppSettingsProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import React, { useCallback, useState } from 'react';
import {
AppSettingsContext,
AppSettingsContextType,
AppSettings,
} from '../context/appSettings';
import { AppSettingsContext, AppSettings } from '../context/appSettings';
import { FeatureEnum } from '../types/features';
import { getCookieValue } from '../utils/getCookieValue';
import { useLocalStorage } from 'react-use';
import { DEFAULT_SETTINGS } from '../consts/DefaultSettings';
import { deepMergeSerializableObjects } from '../utils/deepMergeSerializableObjects';

const DEFAULT_APP_SETTINGS = {
...DEFAULT_SETTINGS,
Expand Down Expand Up @@ -41,11 +38,10 @@ function getInitialSettings(storedSettings: any) {
? JSON.parse(storedSettings)
: DEFAULT_SETTINGS;

// TODO: perform deep merge
return {
...DEFAULT_APP_SETTINGS,
...storedSettingsObj,
};
return deepMergeSerializableObjects(
DEFAULT_APP_SETTINGS,
storedSettingsObj,
) as AppSettings;
}

// These settings are not user configurable, so we should not to persist them to localStorage.
Expand Down
86 changes: 65 additions & 21 deletions src/providers/UserProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { useCallback, useState } from 'react';
import { UserContext } from '../context/user';
import { useCallback, useEffect, useState } from 'react';
import { Profile, UserContext } from '../context/user';
import usePersistedState from '../hooks/usePersistedState';
import { config } from '../config';
import { fetchWithToken } from '../utils/fetchWithToken';
import { fetchAccessTokenUsingRefreshToken } from '../utils/fetchAccessTokenUsingRefreshToken';

const STATE_KEY = 'user-state';

const INITIAL_STATE = {
accessToken: null,
refreshToken: null,
profile: null,
};

type Props = {
Expand All @@ -17,6 +20,7 @@ type Props = {
const UserProvider = ({ children }: Props) => {
const [accessToken, setAccessToken] = useState<string | null>(null);
const [refreshToken, setRefreshToken] = useState<string | null>(null);
const [profile, setProfile] = useState<Profile | null>(null);

const hasMounted = usePersistedState(
STATE_KEY,
Expand All @@ -31,36 +35,74 @@ const UserProvider = ({ children }: Props) => {
const removeUser = useCallback(() => {
setAccessToken(null);
setRefreshToken(null);
setProfile(null);
}, []);

const login = useCallback((refreshToken: string, accessToken: string) => {
setAccessToken(accessToken);
setRefreshToken(refreshToken);
}, []);

const isLoggedIn = !!refreshToken;
const fetchAccessToken = useCallback(
() =>
fetchAccessTokenUsingRefreshToken(refreshToken)
.then((accessToken) => {
setAccessToken(accessToken);
return accessToken;
})
.catch((error) => {
console.log(error);
removeUser();
}),
[setAccessToken, refreshToken, removeUser],
);

const fetchAccessToken = useCallback(() => {
if (!refreshToken) {
return Promise.reject('No refresh token available');
const fetchProfile = useCallback(() => {
if (!accessToken) {
return Promise.resolve(null);
}

return fetch(`${config.API_URI}/api/auth/refresh`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken }),
return fetchWithToken({
url: `${config.API_URI}/api/auth/profile`,
accessToken,
options: {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
},
refreshAccessToken: fetchAccessToken,
})
.then((response) => response.json())
.then((data) => {
if (data.accessToken) {
setAccessToken(data.accessToken);
if (data.profile) {
setProfile(data.profile);

return data.accessToken;
return data.profile as Profile;
} else {
return Promise.reject('No access token available');
removeUser();
return null;
}
})
.catch((error) => {
console.log(error);
// detect 401 error and retry
return null;
});
}, [setAccessToken, refreshToken]);
}, [accessToken, fetchAccessToken, removeUser]);

const login = useCallback(
(refreshToken: string, accessToken: string) => {
setAccessToken(accessToken);
setRefreshToken(refreshToken);
fetchProfile();
},
[fetchProfile],
);

useEffect(() => {
if (refreshToken) {
fetchProfile();
} else {
removeUser();
}
}, [refreshToken, fetchProfile, removeUser]);

if (!hasMounted) {
return null;
Expand All @@ -69,12 +111,14 @@ const UserProvider = ({ children }: Props) => {
const value = {
accessToken,
fetchAccessToken,
isLoggedIn,
isLoggedIn: !!refreshToken,
login,
refreshToken,
removeUser,
setAccessToken,
setRefreshToken,
profile,
fetchProfile,
};

return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
Expand Down
Loading

0 comments on commit 5b41645

Please sign in to comment.