frontend/app/LoginRefreshComponent.tsx (87 lines of code) (raw):
import React, { useState, useEffect, useRef } from "react";
import { JwtDataShape } from "./DecodedProfile";
import { refreshLogin } from "./OAuth2Helper";
interface LoginComponentProps {
refreshToken?: string;
checkInterval?: number;
loginData: JwtDataShape;
onLoginRefreshed?: () => void;
onLoginCantRefresh?: (reason: string) => void;
onLoginExpired: () => void;
onLoggedOut?: () => void;
overrideRefreshLogin?: (tokenUri: string) => Promise<void>; //only used for testing
tokenUri: string;
}
const LoginRefreshComponent: React.FC<LoginComponentProps> = (props) => {
const [refreshFailed, setRefreshFailed] = useState<boolean>(false);
let loginDataRef = useRef(props.loginData);
const tokenUriRef = useRef(props.tokenUri);
const overrideRefreshLoginRef = useRef(props.overrideRefreshLogin);
useEffect(() => {
const intervalTimerId = window.setInterval(
checkExpiryHandler,
props.checkInterval ?? 60000
);
return () => {
console.log("Removing checkExpiryHandler");
window.clearInterval(intervalTimerId);
};
}, []);
useEffect(() => {
console.log("refreshFailed was toggled to ", refreshFailed);
if (refreshFailed) {
console.log("Setting countdown handler");
const intervalTimerId = window.setInterval(updateCountdownHandler, 1000);
return () => {
console.log("Cleared countdown handler");
window.clearInterval(intervalTimerId);
};
}
}, [refreshFailed]);
useEffect(() => {
loginDataRef.current = props.loginData;
}, [props.loginData]);
/**
* Called periodically every second once a refresh has failed
*/
const updateCountdownHandler = () => {
const nowTime = new Date().getTime() / 1000; //assume time is in seconds
const expiry = loginDataRef.current.exp;
const timeToGo = expiry - nowTime;
if (timeToGo <= 1 && props.onLoginExpired) props.onLoginExpired();
};
/**
* Lightweight function that is called every minute to verify the state of the token
* it returns a promise that resolves when the component state has been updated. In normal usage this
* is ignored but it is used in testing to ensure that the component state is only checked after it has been set.
*/
const checkExpiryHandler = () => {
if (loginDataRef.current) {
const nowTime = new Date().getTime() / 1000; //assume time is in seconds
//we know that it is not null due to above check
const expiry = loginDataRef.current.exp;
const timeToGo = expiry - nowTime;
if (timeToGo <= 120) {
console.log("Less than 2mins to expiry, attempting refresh...");
let refreshedPromise;
if (overrideRefreshLoginRef.current) {
refreshedPromise = overrideRefreshLoginRef.current(
tokenUriRef.current
);
} else {
refreshedPromise = refreshLogin(tokenUriRef.current);
}
refreshedPromise
.then(() => {
console.log("Login refreshed");
setRefreshFailed(false);
if (props.onLoginRefreshed) props.onLoginRefreshed();
})
.catch((errString) => {
if (props.onLoginCantRefresh) props.onLoginCantRefresh(errString);
setRefreshFailed(true);
updateCountdownHandler();
return;
});
}
} else {
console.log("No login data present for expiry check");
}
};
return <></>;
};
export default LoginRefreshComponent;