From 9787c2c55da0d6221caa51304fcc11ad916a8a45 Mon Sep 17 00:00:00 2001 From: allgandaf Date: Wed, 1 Jul 2026 14:35:41 +0530 Subject: [PATCH 1/3] Record support token in NetworkStore so the transition pages can dedupe the sign-in --- src/libs/actions/Session/index.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/libs/actions/Session/index.ts b/src/libs/actions/Session/index.ts index acf9a58748be..7a728166f44a 100644 --- a/src/libs/actions/Session/index.ts +++ b/src/libs/actions/Session/index.ts @@ -236,6 +236,11 @@ function getShortLivedLoginParams(isSupportAuthTokenUsed = false, isSAML = false function signInWithSupportAuthToken(authToken: string) { const {optimisticData, finallyData} = getShortLivedLoginParams(true); API.read(READ_COMMANDS.SIGN_IN_WITH_SUPPORT_AUTH_TOKEN, {authToken}, {optimisticData, finallyData}); + + // Record the token so the transition pages skip a duplicate sign-in for it. The Public/Auth navigator + // swap re-mounts the transition screen mid-login, and without this the re-mounted page fires this call + // again and trips the support-token rate limit. Matches signInWithShortLivedAuthToken. + NetworkStore.setLastShortAuthToken(authToken); } /** From 4b90a5b3b8b015517118c2ec91e095e90839b298 Mon Sep 17 00:00:00 2001 From: allgandaf Date: Wed, 1 Jul 2026 14:35:41 +0530 Subject: [PATCH 2/3] Guard LogOutPreviousUserPage against a duplicate supportal sign-in --- src/pages/LogOutPreviousUserPage.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/pages/LogOutPreviousUserPage.tsx b/src/pages/LogOutPreviousUserPage.tsx index 251beda95043..3a29b41ce1b2 100644 --- a/src/pages/LogOutPreviousUserPage.tsx +++ b/src/pages/LogOutPreviousUserPage.tsx @@ -3,6 +3,7 @@ import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import {useInitialURLState} from '@components/InitialURLContextProvider'; import useOnyx from '@hooks/useOnyx'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; +import {getLastShortAuthToken} from '@libs/Network/NetworkStore'; import {isLoggingInAsDelegate as isLoggingInAsDelegateSessionUtils, isLoggingInAsNewUser as isLoggingInAsNewUserSessionUtils} from '@libs/SessionUtils'; import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan'; import Navigation from '@navigation/Navigation'; @@ -41,7 +42,12 @@ function LogOutPreviousUserPage({route}: LogOutPreviousUserPageProps) { } if (isSupportalLogin) { - signInWithSupportAuthToken(shortLivedAuthToken); + // The public transition page may already have started this exact sign-in before the Public/Auth + // navigator swap re-mounted us here. Firing it again trips the support-token rate limit, so skip + // the duplicate but still finish navigating home. + if (shortLivedAuthToken !== getLastShortAuthToken()) { + signInWithSupportAuthToken(shortLivedAuthToken); + } Navigation.isNavigationReady().then(() => { // We must call goBack() to remove the /transition route from history Navigation.goBack(); From e3f9616faf4fa7c3c224b9ff33de841f439920f4 Mon Sep 17 00:00:00 2001 From: allgandaf Date: Wed, 1 Jul 2026 14:35:41 +0530 Subject: [PATCH 3/3] Add test asserting the supportal sign-in fires once across the transition --- tests/ui/SessionTest.tsx | 83 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/tests/ui/SessionTest.tsx b/tests/ui/SessionTest.tsx index 1cabf3504eb2..86d86a6e5c91 100644 --- a/tests/ui/SessionTest.tsx +++ b/tests/ui/SessionTest.tsx @@ -50,6 +50,20 @@ function getInitialURL() { return deeplinkUrl; } +// cspell:disable-next-line +const TEST_SUPPORT_AUTH_TOKEN = 'supporttoken123'; + +function getSupportAuthURL() { + const params = new URLSearchParams(); + + params.set('email', TEST_USER_LOGIN_1); + params.set('delegatorEmail', TEST_USER_LOGIN_2); + params.set('shortLivedAuthToken', TEST_SUPPORT_AUTH_TOKEN); + params.set('authTokenType', CONST.AUTH_TOKEN_TYPES.SUPPORT); + + return `${CONST.DEEPLINK_BASE_URL}/transition?${params.toString()}`; +} + describe('Deep linking', () => { let lastVisitedPath: string | undefined; let lastVisitedPathConnectionID: ReturnType | undefined; @@ -223,3 +237,72 @@ describe('Deep linking', () => { 4 * 60 * 1000, ); }); + +describe('Support auth token login', () => { + beforeEach(() => { + jest.restoreAllMocks(); + wrapOnyxWithWaitForBatchedUpdates(Onyx); + + jest.spyOn(Session, 'signInWithSupportAuthToken').mockImplementation(() => {}); + + // Set the keys the app needs to finish loading rather than going through a full OpenApp round-trip. + jest.spyOn(AppActions, 'openApp').mockImplementation(() => + Onyx.multiSet({ + [ONYXKEYS.IS_LOADING_APP]: false, + [ONYXKEYS.IS_LOADING_REPORT_DATA]: false, + [ONYXKEYS.HAS_LOADED_APP]: true, + [ONYXKEYS.NVP_ONBOARDING]: {hasCompletedGuidedSetupFlow: true}, + }), + ); + }); + + afterEach(async () => { + cleanup(); + await act(async () => { + await Onyx.clear(); + }); + await waitForBatchedUpdatesWithAct(); + await waitForNetworkPromises(); + PusherHelper.teardown(); + jest.clearAllMocks(); + Linking.setInitialURL(''); + setLastShortAuthToken(null); + }); + + // Renders the full App and processes a supportal transition, so it runs longer than the suite default. + it( + 'does not fire the support sign-in again when LogOutPreviousUserPage re-processes an already-handled token', + async () => { + expect(hasAuthToken()).toBe(false); + + // Sign in so the app is on AuthScreens, where LogOutPreviousUserPage owns the /transition route. + const {unmount: unmount1} = render(); + await TestHelper.signInWithTestUser(TEST_USER_ACCOUNT_ID_2, TEST_USER_LOGIN_2, undefined, TEST_AUTH_TOKEN_2); + + await waitForBatchedUpdatesWithAct(); + + expect(hasAuthToken()).toBe(true); + unmount1(); + + await waitForBatchedUpdatesWithAct(); + await waitForNetworkPromises(); + + // The public transition page (LogInWithShortLivedAuthTokenPage) records the support token when it + // fires the sign-in. Simulate that, then re-process the SAME support deep link, which lands on + // LogOutPreviousUserPage. It must skip the duplicate sign-in; before the fix it fired + // unconditionally and tripped the support-token rate limit. + setLastShortAuthToken(TEST_SUPPORT_AUTH_TOKEN); + Linking.setInitialURL(getSupportAuthURL()); + const {unmount: unmount2} = render(); + + await waitForBatchedUpdatesWithAct(); + + expect(Session.signInWithSupportAuthToken).not.toHaveBeenCalled(); + + unmount2(); + await waitForBatchedUpdatesWithAct(); + await waitForNetworkPromises(); + }, + 4 * 60 * 1000, + ); +});