diff --git a/src/setupTests.ts b/src/setupTests.ts index e67c029671..0661e42212 100644 --- a/src/setupTests.ts +++ b/src/setupTests.ts @@ -92,7 +92,6 @@ vi.mock('shared/hooks/useFeatureFlags', () => ({ enableMfa: false, enableAdminAnnouncementBanner: false, enableAuditLogs: true, - enableSessionKeepAlive: false, }, resetLDContext: vi.fn(), })), diff --git a/src/shared/hooks/useFeatureFlags.const.ts b/src/shared/hooks/useFeatureFlags.const.ts index 9128f4e812..f5b9d3752f 100644 --- a/src/shared/hooks/useFeatureFlags.const.ts +++ b/src/shared/hooks/useFeatureFlags.const.ts @@ -23,7 +23,6 @@ export const FeatureFlagDefaults: FeatureFlags = { enableMfa: true, enableAdminAnnouncementBanner: false, enableAuditLogs: false, - enableSessionKeepAlive: false, }; export const PROHIBITED_PII_KEYS = ['firstName', 'lastName', 'email']; diff --git a/src/shared/hooks/useSessionKeepAlive/useSessionAdoption.test.ts b/src/shared/hooks/useSessionKeepAlive/useSessionAdoption.test.ts index 149365f33a..90aeceb227 100644 --- a/src/shared/hooks/useSessionKeepAlive/useSessionAdoption.test.ts +++ b/src/shared/hooks/useSessionKeepAlive/useSessionAdoption.test.ts @@ -1,7 +1,6 @@ import { act } from '@testing-library/react'; import { authStorage } from 'shared/utils'; -import { useFeatureFlags } from 'shared/hooks/useFeatureFlags'; import { renderHookWithProviders } from 'shared/utils/renderHookWithProviders'; import { getPreloadedState } from 'shared/tests/getPreloadedState'; import { @@ -30,12 +29,6 @@ const ANNOUNCED = { const mockedReload = vi.fn(); -const setFlag = (enableSessionKeepAlive: boolean) => - vi.mocked(useFeatureFlags).mockReturnValue({ - featureFlags: { enableSessionKeepAlive }, - resetLDContext: vi.fn(), - } as never); - // Mirrors how the routes consume this: the hook re-renders, and the token is read afterwards. const renderAdoption = () => renderHookWithProviders( @@ -63,7 +56,6 @@ describe('useSessionAdoption', () => { sessionStorage.clear(); vi.stubGlobal('BroadcastChannel', InMemoryBroadcastChannel); vi.stubGlobal('location', { ...window.location, reload: mockedReload }); - setFlag(true); sibling = new InMemoryBroadcastChannel(SESSION_CHANNEL_NAME); }); @@ -118,15 +110,6 @@ describe('useSessionAdoption', () => { expect(authStorage.getRefreshToken()).toBe(ANNOUNCED.refreshToken); }); - test('ignores announcements when the flag is off', () => { - setFlag(false); - renderAdoption(); - - announceSession(sibling); - - expect(authStorage.getRefreshToken()).toBeNull(); - }); - test('asks for a session when the tab comes back into focus', () => { const onSiblingMessage = vi.fn(); sibling.onmessage = onSiblingMessage; diff --git a/src/shared/hooks/useSessionKeepAlive/useSessionAdoption.ts b/src/shared/hooks/useSessionKeepAlive/useSessionAdoption.ts index b411a869b1..e5a0b8a153 100644 --- a/src/shared/hooks/useSessionKeepAlive/useSessionAdoption.ts +++ b/src/shared/hooks/useSessionKeepAlive/useSessionAdoption.ts @@ -2,7 +2,6 @@ import { useEffect, useReducer } from 'react'; import { authStorage } from 'shared/utils/authStorage'; import { SessionStorageKeys } from 'shared/utils/storage'; -import { useFeatureFlags } from 'shared/hooks/useFeatureFlags'; import { getLastActivityAt } from './sessionStore'; import { publishSessionMessage, subscribeSessionSync } from './sessionSync'; @@ -14,12 +13,11 @@ import { resolveSessionConfig } from './useSessionKeepAlive.utils'; // So it listens for a session being announced, and asks again whenever it returns to focus, for // the case where the browser had frozen it when the announcement went out. export const useSessionAdoption = () => { - const { featureFlags } = useFeatureFlags(); // Adopting writes to storage, which no component is watching. This re-renders so the routes // read the token that just arrived. const [, onAdopted] = useReducer((count: number) => count + 1, 0); - const isListening = !!featureFlags.enableSessionKeepAlive && !authStorage.getRefreshToken(); + const isListening = !authStorage.getRefreshToken(); useEffect(() => { if (!isListening) { diff --git a/src/shared/hooks/useSessionKeepAlive/useSessionKeepAlive.test.ts b/src/shared/hooks/useSessionKeepAlive/useSessionKeepAlive.test.ts index 5e02326908..4d4bd71e0d 100644 --- a/src/shared/hooks/useSessionKeepAlive/useSessionKeepAlive.test.ts +++ b/src/shared/hooks/useSessionKeepAlive/useSessionKeepAlive.test.ts @@ -3,7 +3,6 @@ import { act } from '@testing-library/react'; import { refreshTokens } from 'shared/api'; import { authStorage } from 'shared/utils'; import { PlainStorageKeys } from 'shared/utils/storage'; -import { useFeatureFlags } from 'shared/hooks/useFeatureFlags'; import { useLogout } from 'shared/hooks/useLogout'; import { renderHookWithProviders } from 'shared/utils/renderHookWithProviders'; import { getPreloadedState } from 'shared/tests/getPreloadedState'; @@ -15,7 +14,7 @@ import { state as authState } from 'modules/Auth/state/Auth.state'; import { clearSessionState, setLastActivityAt } from './sessionStore'; import { useSessionKeepAlive } from './useSessionKeepAlive'; -import { closeSessionSync, publishSessionMessage } from './sessionSync'; +import { closeSessionSync } from './sessionSync'; import { SESSION_CHANNEL_NAME, SESSION_REQUEST_WINDOW_MS } from './sessionSync.const'; import { SessionMessage, SessionState } from './sessionSync.types'; import { MS_IN_MIN, MS_IN_SEC } from './useSessionKeepAlive.const'; @@ -42,12 +41,6 @@ const tokenExpiringIn = (ms: number) => const refreshTokenFor = (sessionId: string) => `header.${btoa(JSON.stringify({ family: sessionId }))}.signature`; -const setFlag = (enableSessionKeepAlive: boolean) => - vi.mocked(useFeatureFlags).mockReturnValue({ - featureFlags: { enableSessionKeepAlive }, - resetLDContext: vi.fn(), - } as never); - // A sibling tab that replies to every session request with the state it is given. const answerSessionRequests = (state: Partial) => { const sibling = new InMemoryBroadcastChannel(SESSION_CHANNEL_NAME); @@ -66,9 +59,9 @@ const answerSessionRequests = (state: Partial) => { }; }; -const renderEngine = () => +const renderEngine = (isAuthorized = true) => renderHookWithProviders(useSessionKeepAlive, { - preloadedState: { ...getPreloadedState(), auth: { ...authState, isAuthorized: true } }, + preloadedState: { ...getPreloadedState(), auth: { ...authState, isAuthorized } }, }); describe('useSessionKeepAlive', () => { @@ -82,7 +75,6 @@ describe('useSessionKeepAlive', () => { vi.mocked(useLogout).mockReturnValue(mockedLogout); mockedRefreshTokens.mockResolvedValue({ accessToken: 'a', refreshToken: 'r' }); - setFlag(true); authStorage.setAccessToken(tokenExpiringIn(TOKEN_LIFETIME_MS)); authStorage.setRefreshToken(refreshTokenFor(SESSION_ID)); }); @@ -463,9 +455,8 @@ describe('useSessionKeepAlive', () => { }); }); - test('neither refreshes nor logs out nor syncs when the flag is off', () => { - setFlag(false); - renderEngine(); + test('neither refreshes nor logs out nor syncs until the session is authorized', () => { + renderEngine(false); const sibling = new InMemoryBroadcastChannel(SESSION_CHANNEL_NAME); const onSiblingMessage = vi.fn(); sibling.onmessage = onSiblingMessage; @@ -474,7 +465,6 @@ describe('useSessionKeepAlive', () => { act(() => { vi.advanceTimersByTime(IDLE_TIMEOUT_MS * 2); window.dispatchEvent(new Event('keydown')); - publishSessionMessage({ type: 'SESSION_REQUEST' }); sibling.postMessage({ type: 'TOKENS_UPDATED', payload: { @@ -492,8 +482,7 @@ describe('useSessionKeepAlive', () => { expect(authStorage.getAccessToken()).toBe(mine); }); - test('still records activity when the flag is off, so the boot check has a clock to read', () => { - setFlag(false); + test('records activity as soon as the session is live, so the boot check has a clock to read', () => { renderEngine(); expect(localStorage.getItem(PlainStorageKeys.LastActivityAt)).toBe(String(Date.now())); diff --git a/src/shared/hooks/useSessionKeepAlive/useSessionKeepAlive.ts b/src/shared/hooks/useSessionKeepAlive/useSessionKeepAlive.ts index 59d763229a..2e36c76db1 100644 --- a/src/shared/hooks/useSessionKeepAlive/useSessionKeepAlive.ts +++ b/src/shared/hooks/useSessionKeepAlive/useSessionKeepAlive.ts @@ -3,7 +3,6 @@ import { useEffect, useRef } from 'react'; import { auth } from 'redux/modules'; import { refreshTokens } from 'shared/api'; import { authStorage, getTokenExpiration } from 'shared/utils'; -import { useFeatureFlags } from 'shared/hooks/useFeatureFlags'; import { useLogout } from 'shared/hooks/useLogout'; import { startActivityTracking, stopActivityTracking } from './activityTracker'; @@ -15,7 +14,6 @@ import { getSessionId } from './sessionSync.utils'; import { resolveSessionConfig } from './useSessionKeepAlive.utils'; export const useSessionKeepAlive = () => { - const { featureFlags } = useFeatureFlags(); const isAuthorized = auth.useAuthorized(); const logout = useLogout(); @@ -23,12 +21,10 @@ export const useSessionKeepAlive = () => { const logoutRef = useRef(logout); logoutRef.current = logout; - const isEnabled = !!featureFlags.enableSessionKeepAlive && isAuthorized; // Set by the engine below, so tracking can re-arm the timers without owning them. const scheduleRef = useRef<(() => void) | null>(null); - // Runs with the flag off too: a session outlives its tab now, and the boot check reads this - // clock to decide whether one left behind may carry on. + // Kept apart from the engine: the boot check reads this clock, so it outlives any teardown. useEffect(() => { if (!isAuthorized) return; @@ -38,7 +34,7 @@ export const useSessionKeepAlive = () => { }, [isAuthorized]); useEffect(() => { - if (!isEnabled) return; + if (!isAuthorized) return; const { idleTimeoutMs, refreshLeadMs } = resolveSessionConfig(); let refreshTimer: ReturnType; @@ -169,5 +165,5 @@ export const useSessionKeepAlive = () => { document.removeEventListener('visibilitychange', handleVisibilityChange); unsubscribe(); }; - }, [isEnabled]); + }, [isAuthorized]); }; diff --git a/src/shared/types/featureFlags.ts b/src/shared/types/featureFlags.ts index f24e675605..b1c1159f34 100644 --- a/src/shared/types/featureFlags.ts +++ b/src/shared/types/featureFlags.ts @@ -26,5 +26,4 @@ export type FeatureFlags = Partial<{ enableMfa: boolean; enableAdminAnnouncementBanner: boolean; enableAuditLogs: boolean; - enableSessionKeepAlive: boolean; }>;