78 lines
1.8 KiB
TypeScript
78 lines
1.8 KiB
TypeScript
import { createContext, useCallback, useContext, useMemo, useRef } from "react";
|
|
import { defaultTokenData, fetchToken, parseToken, type TokenData } from "./TokenUtils";
|
|
|
|
export interface TokenState {
|
|
getToken: () => Promise<string | null | undefined>;
|
|
}
|
|
|
|
const initialState: TokenState = {
|
|
getToken: () => new Promise(() => {})
|
|
}
|
|
|
|
const TokenContext = createContext<TokenState>(initialState);
|
|
|
|
|
|
export default function TokenProvider({
|
|
apiUrl,
|
|
children
|
|
}: Readonly<{
|
|
apiUrl: string;
|
|
children: React.ReactNode;
|
|
}>){
|
|
const tokenRef = useRef<TokenData>(defaultTokenData);
|
|
const refreshPromise = useRef<Promise<string | null | undefined>>(null);
|
|
|
|
const getToken = useCallback(async () => {
|
|
if(refreshPromise.current){
|
|
return refreshPromise.current;
|
|
}
|
|
|
|
const { accessToken, expires } = tokenRef.current;
|
|
|
|
const isExpired = Date.now() > (expires - 5000); //Give a 5 second buffer
|
|
|
|
if(!accessToken || isExpired){
|
|
refreshPromise.current = (async () => {
|
|
try {
|
|
const rawToken = (await fetchToken(apiUrl)).token;
|
|
const parsedToken = parseToken(rawToken);
|
|
tokenRef.current = parsedToken;
|
|
return rawToken;
|
|
}
|
|
catch(error){
|
|
tokenRef.current = defaultTokenData;
|
|
throw error;
|
|
}
|
|
finally {
|
|
refreshPromise.current = null;
|
|
}
|
|
})();
|
|
return refreshPromise.current;
|
|
}
|
|
|
|
return accessToken;
|
|
}, [apiUrl]);
|
|
|
|
const value: TokenState = useMemo(() => ({
|
|
getToken
|
|
}), [getToken]);
|
|
|
|
return (
|
|
<TokenContext.Provider value={value}>
|
|
{children}
|
|
</TokenContext.Provider>
|
|
);
|
|
}
|
|
|
|
|
|
// eslint-disable-next-line react-refresh/only-export-components
|
|
export function useToken(){
|
|
const context = useContext(TokenContext);
|
|
|
|
if(!context){
|
|
throw new Error("useToken must be called inside a TokenProvider");
|
|
}
|
|
|
|
return context;
|
|
}
|