diff --git a/src/constants/Common.constants.ts b/src/constants/Common.constants.ts index f5f717a6..73916ab2 100644 --- a/src/constants/Common.constants.ts +++ b/src/constants/Common.constants.ts @@ -12,3 +12,5 @@ export const fullMonthNames = [ 'November', 'December' ]; + +export const REDIRECT_TIMESTAMP_RECORD = 'redirectRecordTimestamp'; diff --git a/src/utils/ApiHelper.ts b/src/utils/ApiHelper.ts index 24665f69..4decb41f 100644 --- a/src/utils/ApiHelper.ts +++ b/src/utils/ApiHelper.ts @@ -13,10 +13,16 @@ import { GLOBAL } from '../constants/Global'; import APP_ROUTES, { LOGIN_PATH } from '../layout/Routes'; import { setAuthData, setShowStickyNote } from '../reducers/commonSlice'; import { checkBuildTime, handleBuildNudgeInNewPortal } from './checkBuildTime'; -import { isJson, isRedirectionPermitted } from './commonUtils'; +import { + isJson, + isRedirectionPermitted, + recordRedirectTime, + zeroImmediateThenDebounce +} from './commonUtils'; import { setLocalServerTimeDiff } from './DateHelper'; import { checkHooman } from './detectBots'; import { getDeviceId } from './FingerPrintJS'; +import { REDIRECT_TIMESTAMP_RECORD } from '../constants/Common.constants'; export const logError = (error?: Error | unknown, message?: string) => Sentry.captureException({ error, message }); @@ -26,7 +32,10 @@ const MOCK_DIR = '../../__mocks__'; // set this to `true` to use mock data for local development // to connect with real apis this should be set to `false` const USE_MOCK = !import.meta.env.PROD && false; - +const REDIRECT_URL_VERSIONED = '_V_'; +const NEW_LH_ROUTE_CONSTANT = '/new'; +const REDIRECT_SAFE_TIMEOUT = 3000; +const NEW_LH_VERSIONED_ROUTE_CONSTANT = '/versions'; export enum ApiKeys { PEOPLE, SEND_OTP, @@ -284,6 +293,25 @@ const errorsToRetry = [500, 503]; const axiosInstance = axios.create(); const getURLForOldPortal = () => window?.location?.href?.split('/new')[0]; +const getURLForNewPortal = () => { + const currentUrl = window.location.href; + let urlToAppend = ''; + if (currentUrl.includes(NEW_LH_VERSIONED_ROUTE_CONSTANT)) { + urlToAppend = currentUrl.split('/versions')[1].split('/').splice(2).join('/'); + } + return `${window?.location?.origin}/new/${urlToAppend}`; +}; +const getUrlForForcedVersion = (versionString: string) => { + const currentUrl = window.location.href; + let urlToAppend = ''; + if (currentUrl.includes(NEW_LH_VERSIONED_ROUTE_CONSTANT)) { + urlToAppend = currentUrl.split('/versions')[1].split('/').splice(2).join('/'); + } else { + urlToAppend = window.location.href.split('new/')[1] || ''; + } + const [major, minor, patch] = versionString.split('_').slice(-3); + return `${window?.location?.origin}/new/versions/${major}.${minor}.${patch}/${urlToAppend}`; +}; axiosInstance.interceptors.request.use( request => { @@ -320,12 +348,71 @@ const excludedRedirectionEndPointsList = [ ]; const checkIsValidRedirection = () => { - const isValidForRedirection = + const isWhitelistedEndpoint = excludedRedirectionEndPointsList.filter(endPoint => window.location?.href?.includes(endPoint)) .length === 0; - return isValidForRedirection; + return isWhitelistedEndpoint; }; +/** + * checks the given endpoint against the redirect version (something_V_X_Y_Z), and returns that is the "endpoint" + * is actually of the x.y.z version + */ +/** + * Note : returns true if the redirectVersion does not belong to new lh portal some version + */ +function isURLValidForGivenLHVersion(endpoint: string, redirectVersion: string): boolean { + // first see that does the redirectVersion string, really have the version info, if not return true + if (!redirectVersion.includes(REDIRECT_URL_VERSIONED)) { + return true; + } + const [major, minor, patch] = redirectVersion.split('_').slice(-3); + return endpoint.includes(`versions/${major}.${minor}.${patch}`); +} + +function isSafeFromInfiniteRedirection() { + const currentRecord: number[] = JSON.parse( + localStorage.getItem(REDIRECT_TIMESTAMP_RECORD) || '[]' + ); + return currentRecord.length < 2 || Date.now() - (currentRecord?.[1] || 0) > REDIRECT_SAFE_TIMEOUT; +} + +/** + * Note : returns true if the redirectVersion does not belong to old lh portal + */ +function isURLValidForOldLhVersion(endpoint: string, redirectVersion: string): boolean { + return ( + redirectVersion !== XREDIRECTTO.OLD_LONGHORN_PORTAL || + !window?.location?.pathname.startsWith(NEW_LH_ROUTE_CONSTANT) + ); +} + +/** + * Note : returns true if the redirectVersion does not belong to new lh portal + */ +function isURLValidForLatestLHNew(endpoint: string, redirectVersion: string): boolean { + const path = window?.location?.pathname; + return ( + redirectVersion !== XREDIRECTTO.NEW_LONGHORN_PORTAL || + (path?.startsWith(NEW_LH_ROUTE_CONSTANT) && !path?.includes(NEW_LH_VERSIONED_ROUTE_CONSTANT)) + ); +} +const debouncedRouteChecker = zeroImmediateThenDebounce(redirectHeader => { + if (!isSafeFromInfiniteRedirection()) { + return; + } + if (!isURLValidForGivenLHVersion(window?.location?.href, redirectHeader)) { + recordRedirectTime(); + window.location.replace(getUrlForForcedVersion(redirectHeader)); + } else if (!isURLValidForOldLhVersion(window?.location?.href, redirectHeader)) { + recordRedirectTime(); + window.location.replace(getURLForOldPortal()); + } else if (!isURLValidForLatestLHNew(window?.location?.href, redirectHeader)) { + recordRedirectTime(); + window.location.replace(getURLForNewPortal()); + } +}, 800); + axiosInstance.interceptors.response.use( response => { if (!checkHooman()) return { ...response, data: null, error: commonErrorObject }; @@ -345,15 +432,8 @@ axiosInstance.interceptors.response.use( const payload = { buildTime: Number(response.headers['build_time']) }; checkBuildTime(payload, handleBuildNudgeInNewPortal(dispatch, setShowStickyNote)); const redirectHeader = response.headers?.[REDIRECT_HEADER_NAME]; - if ( - redirectHeader && - checkIsValidRedirection() && - getURLForOldPortal() && - import.meta.env.PROD - ) { - if (redirectHeader === XREDIRECTTO.OLD_LONGHORN_PORTAL) { - window.location.replace(getURLForOldPortal()); - } + if (redirectHeader && checkIsValidRedirection() && import.meta.env.PROD) { + debouncedRouteChecker(redirectHeader); } return response; }, diff --git a/src/utils/commonUtils.ts b/src/utils/commonUtils.ts index 6b9f33d8..f8e86c20 100644 --- a/src/utils/commonUtils.ts +++ b/src/utils/commonUtils.ts @@ -8,6 +8,7 @@ import { } from 'src/pages/Cases/constants/CasesConstants'; import { interpolatePathParams } from './interpolate'; import { logError } from './ApiHelper'; +import { REDIRECT_TIMESTAMP_RECORD } from '../constants/Common.constants'; export const isObjectEmpty = (object: any) => { for (const prop in object) { @@ -224,3 +225,27 @@ export const formatNumber = (value: number) => { maximumSignificantDigits: 10 }).format(value); }; + +export const zeroImmediateThenDebounce = (callback: (...params: any[]) => any, ms = 300) => { + let timeoutId: ReturnType; + let executedTimes = 0; + return function (this: any, ...args: any[]) { + if (executedTimes === 0) { + callback.apply(this, args); + } else { + clearTimeout(timeoutId); + timeoutId = setTimeout(() => callback.apply(this, args), ms); + } + ++executedTimes; + }; +}; +export function recordRedirectTime() { + const currentRecord: number[] = JSON.parse( + localStorage.getItem(REDIRECT_TIMESTAMP_RECORD) || '[]' + ); + if (currentRecord.length === 2) { + currentRecord.shift(); + } + currentRecord.push(Date.now()); + localStorage.setItem(REDIRECT_TIMESTAMP_RECORD, JSON.stringify(currentRecord)); +}