78 lines
1.7 KiB
TypeScript
78 lines
1.7 KiB
TypeScript
import { createContext, use, useCallback, 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 refreshPromiseRef = useRef<Promise<string | null | undefined>>(null);
|
|
|
|
const getToken = useCallback(async () => {
|
|
if(refreshPromiseRef.current){
|
|
return refreshPromiseRef.current;
|
|
}
|
|
|
|
const { accessToken, expires } = tokenRef.current;
|
|
|
|
const isExpired = Date.now() > (expires - 5000); //Give a 5 second buffer
|
|
|
|
if(!accessToken || isExpired){
|
|
refreshPromiseRef.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 {
|
|
refreshPromiseRef.current = null;
|
|
}
|
|
})();
|
|
return refreshPromiseRef.current;
|
|
}
|
|
|
|
return accessToken;
|
|
}, [apiUrl]);
|
|
|
|
const value: TokenState = useMemo(() => ({
|
|
getToken
|
|
}), [getToken]);
|
|
|
|
return (
|
|
<TokenContext value={value}>
|
|
{children}
|
|
</TokenContext>
|
|
);
|
|
}
|
|
|
|
|
|
// eslint-disable-next-line react-refresh/only-export-components
|
|
export function useToken(){
|
|
const context = use(TokenContext);
|
|
|
|
if(!context){
|
|
throw new Error("useToken must be called inside a TokenProvider");
|
|
}
|
|
|
|
return context;
|
|
}
|