Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion src/setupTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,6 @@ vi.mock('shared/hooks/useFeatureFlags', () => ({
enableMfa: false,
enableAdminAnnouncementBanner: false,
enableAuditLogs: true,
enableSessionKeepAlive: false,
},
resetLDContext: vi.fn(),
})),
Expand Down
1 change: 0 additions & 1 deletion src/shared/hooks/useFeatureFlags.const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ export const FeatureFlagDefaults: FeatureFlags = {
enableMfa: true,
enableAdminAnnouncementBanner: false,
enableAuditLogs: false,
enableSessionKeepAlive: false,
};

export const PROHIBITED_PII_KEYS = ['firstName', 'lastName', 'email'];
17 changes: 0 additions & 17 deletions src/shared/hooks/useSessionKeepAlive/useSessionAdoption.test.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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);
});
Expand Down Expand Up @@ -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;
Expand Down
4 changes: 1 addition & 3 deletions src/shared/hooks/useSessionKeepAlive/useSessionAdoption.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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) {
Expand Down
23 changes: 6 additions & 17 deletions src/shared/hooks/useSessionKeepAlive/useSessionKeepAlive.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand All @@ -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<SessionState>) => {
const sibling = new InMemoryBroadcastChannel(SESSION_CHANNEL_NAME);
Expand All @@ -66,9 +59,9 @@ const answerSessionRequests = (state: Partial<SessionState>) => {
};
};

const renderEngine = () =>
const renderEngine = (isAuthorized = true) =>
renderHookWithProviders(useSessionKeepAlive, {
preloadedState: { ...getPreloadedState(), auth: { ...authState, isAuthorized: true } },
preloadedState: { ...getPreloadedState(), auth: { ...authState, isAuthorized } },
});

describe('useSessionKeepAlive', () => {
Expand All @@ -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));
});
Expand Down Expand Up @@ -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;
Expand All @@ -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: {
Expand All @@ -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()));
Expand Down
10 changes: 3 additions & 7 deletions src/shared/hooks/useSessionKeepAlive/useSessionKeepAlive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -15,20 +14,17 @@ import { getSessionId } from './sessionSync.utils';
import { resolveSessionConfig } from './useSessionKeepAlive.utils';

export const useSessionKeepAlive = () => {
const { featureFlags } = useFeatureFlags();
const isAuthorized = auth.useAuthorized();
const logout = useLogout();

// Refreshed every render so the logout never closes over a stale email or workspace.
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;

Expand All @@ -38,7 +34,7 @@ export const useSessionKeepAlive = () => {
}, [isAuthorized]);

useEffect(() => {
if (!isEnabled) return;
if (!isAuthorized) return;

const { idleTimeoutMs, refreshLeadMs } = resolveSessionConfig();
let refreshTimer: ReturnType<typeof setTimeout>;
Expand Down Expand Up @@ -169,5 +165,5 @@ export const useSessionKeepAlive = () => {
document.removeEventListener('visibilitychange', handleVisibilityChange);
unsubscribe();
};
}, [isEnabled]);
}, [isAuthorized]);
};
1 change: 0 additions & 1 deletion src/shared/types/featureFlags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,4 @@ export type FeatureFlags = Partial<{
enableMfa: boolean;
enableAdminAnnouncementBanner: boolean;
enableAuditLogs: boolean;
enableSessionKeepAlive: boolean;
}>;
Loading