diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 4104ab9230f8..8119db521731 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -1054,6 +1054,7 @@ const CONST = { POLICY_CONNECTIONS_URL: (policyID: string) => `policy?param={"policyID":"${policyID}"}#connections`, POLICY_CONNECTIONS_URL_ENCODED: (policyID: string) => `policy?param=%7B%22policyID%22%3A%22${policyID}%22%7D#connections`, SIGN_OUT: 'signout', + SUPPORTAL_RESTORE_STASHED_LOGIN: '_support/index?action=restoreStashedLogin', }, EXPENSIFY_POLICY_DOMAIN, diff --git a/src/libs/actions/Link.ts b/src/libs/actions/Link.ts index e70bc566d7b4..477cf71bab6d 100644 --- a/src/libs/actions/Link.ts +++ b/src/libs/actions/Link.ts @@ -240,4 +240,15 @@ function getTravelDotLink(policyID: OnyxEntry) { }); } -export {openOldDotLink, openExternalLink, openLink, getInternalNewExpensifyPath, getInternalExpensifyPath, openTravelDotLink, buildTravelDotURL, openExternalLinkWithToken, getTravelDotLink}; +export { + openOldDotLink, + openExternalLink, + openLink, + getInternalNewExpensifyPath, + getInternalExpensifyPath, + openTravelDotLink, + buildTravelDotURL, + openExternalLinkWithToken, + getTravelDotLink, + buildOldDotURL, +}; diff --git a/src/libs/actions/Session/index.ts b/src/libs/actions/Session/index.ts index ab4ee0d7a290..549aefa7f767 100644 --- a/src/libs/actions/Session/index.ts +++ b/src/libs/actions/Session/index.ts @@ -5,6 +5,7 @@ import type {ChannelAuthorizationCallback} from 'pusher-js/with-encryption'; import {InteractionManager} from 'react-native'; import type {OnyxEntry, OnyxKey, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; +import {buildOldDotURL, openExternalLink} from '@libs/actions/Link'; import * as PersistedRequests from '@libs/actions/PersistedRequests'; import * as API from '@libs/API'; import type { @@ -267,6 +268,25 @@ function isExpiredSession(sessionCreationDate: number): boolean { return new Date().getTime() - sessionCreationDate >= CONST.SESSION_EXPIRATION_TIME_MS; } +const KEYS_TO_PRESERVE_SUPPORTAL = [ + ONYXKEYS.NVP_TRY_FOCUS_MODE, + ONYXKEYS.PREFERRED_THEME, + ONYXKEYS.NVP_PREFERRED_LOCALE, + ONYXKEYS.ARE_TRANSLATIONS_LOADING, + ONYXKEYS.SESSION, + ONYXKEYS.STASHED_SESSION, + ONYXKEYS.HAS_LOADED_APP, + ONYXKEYS.STASHED_CREDENTIALS, + ONYXKEYS.HYBRID_APP, + + // We need to preserve the sidebar loaded state since we never unmount the sidebar when connecting as a delegate + // This allows the report screen to load correctly when the delegate token expires and the delegate is returned to their original account. + ONYXKEYS.IS_SIDEBAR_LOADED, + ONYXKEYS.NETWORK, + ONYXKEYS.SHOULD_USE_STAGING_SERVER, + ONYXKEYS.IS_DEBUG_MODE_ENABLED, +]; + function signOutAndRedirectToSignIn(shouldResetToHome?: boolean, shouldStashSession?: boolean, shouldSignOutFromOldDot = true, shouldForceUseStashedSession?: boolean) { Log.info('Redirecting to Sign In because signOut() was called'); hideContextMenu(false); @@ -358,6 +378,30 @@ function signOutAndRedirectToSignIn(shouldResetToHome?: boolean, shouldStashSess true, true, ); + } else if (shouldRestoreStashedSession && !shouldStashSession && hasStashedSession()) { + // We have confirmed here that the supportal agent was logged in, so we can restore the stashed session + // and then redirect to the oldDot supportal page to restore the stashed session + // Clear the Onyx DB of stale data that might be present from a previous session + // of the customer account + Onyx.clear(KEYS_TO_PRESERVE_SUPPORTAL).then(() => { + Onyx.multiSet(onyxSetParams).then(() => { + buildOldDotURL(CONST.OLDDOT_URLS.SUPPORTAL_RESTORE_STASHED_LOGIN).then((oldDotURL) => { + // Open the oldDot URL to restore the stashed session and go back to OD supportal page + openExternalLink(oldDotURL, undefined, true); + }); + }); + }); + } else if (shouldRestoreStashedSession && !shouldStashSession && !hasStashedSession()) { + // If the supportal agent was not logged in, we call `redirectToSignIn` to clear the Onyx DB + // and then redirect to supportal and restore the stashed session + redirectToSignIn().then(() => { + Onyx.multiSet(onyxSetParams).then(() => { + buildOldDotURL(CONST.OLDDOT_URLS.SUPPORTAL_RESTORE_STASHED_LOGIN).then((oldDotURL) => { + // Open the oldDot URL to restore the stashed session and go back to OD supportal page + openExternalLink(oldDotURL, undefined, true); + }); + }); + }); } else { redirectToSignIn().then(() => { Onyx.multiSet(onyxSetParams); @@ -1427,6 +1471,7 @@ function resetSMSDeliveryFailureStatus(login: string) { } export { + KEYS_TO_PRESERVE_SUPPORTAL, beginSignIn, beginAppleSignIn, beginGoogleSignIn, diff --git a/src/pages/settings/InitialSettingsPage.tsx b/src/pages/settings/InitialSettingsPage.tsx index 010cc845677a..3649020d40ee 100755 --- a/src/pages/settings/InitialSettingsPage.tsx +++ b/src/pages/settings/InitialSettingsPage.tsx @@ -42,7 +42,7 @@ import variables from '@styles/variables'; import {confirmReadyToOpenApp} from '@userActions/App'; import {openExternalLink} from '@userActions/Link'; import {hasPaymentMethodError} from '@userActions/PaymentMethods'; -import {isSupportAuthToken, signOutAndRedirectToSignIn} from '@userActions/Session'; +import {hasStashedSession, isSupportAuthToken, signOutAndRedirectToSignIn} from '@userActions/Session'; import {openInitialSettingsPage} from '@userActions/Wallet'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; @@ -230,7 +230,7 @@ function InitialSettingsPage({currentUserPersonalDetails}: InitialSettingsPagePr * @returns object with translationKey, style and items for the general section */ const generalMenuItemsData: Menu = useMemo(() => { - const signOutTranslationKey = isSupportAuthToken() ? 'initialSettingsPage.restoreStashed' : 'initialSettingsPage.signOut'; + const signOutTranslationKey = isSupportAuthToken() && hasStashedSession() ? 'initialSettingsPage.restoreStashed' : 'initialSettingsPage.signOut'; return { sectionStyle: { ...styles.pt4, diff --git a/tests/actions/SessionTest.ts b/tests/actions/SessionTest.ts index 872a3fd42294..94343acf89bc 100644 --- a/tests/actions/SessionTest.ts +++ b/tests/actions/SessionTest.ts @@ -2,6 +2,7 @@ import {beforeEach, jest, test} from '@jest/globals'; import Onyx from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import {confirmReadyToOpenApp, openApp, reconnectApp} from '@libs/actions/App'; +import {buildOldDotURL, openExternalLink} from '@libs/actions/Link'; import OnyxUpdateManager from '@libs/actions/OnyxUpdateManager'; import {getAll as getAllPersistedRequests} from '@libs/actions/PersistedRequests'; // eslint-disable-next-line no-restricted-syntax @@ -15,7 +16,7 @@ import '@libs/Notification/PushNotification/subscribeToPushNotifications'; import CONFIG from '@src/CONFIG'; import CONST from '@src/CONST'; import * as SessionUtil from '@src/libs/actions/Session'; -import {signOutAndRedirectToSignIn} from '@src/libs/actions/Session'; +import {KEYS_TO_PRESERVE_SUPPORTAL, signOutAndRedirectToSignIn} from '@src/libs/actions/Session'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Credentials, Session} from '@src/types/onyx'; import * as TestHelper from '../utils/TestHelper'; @@ -31,6 +32,13 @@ jest.mock('@libs/Notification/PushNotification'); // Mocked to check SignOutAndRedirectToSignIn behavior jest.mock('@libs/asyncOpenURL'); +jest.mock('@libs/actions/Link', () => { + return { + buildOldDotURL: jest.fn(() => Promise.resolve('mockOldDotURL')), + openExternalLink: jest.fn(), + }; +}); + Onyx.init({ keys: ONYXKEYS, }); @@ -108,13 +116,13 @@ describe('Session', () => { test('Push notifications are subscribed after signing in', async () => { await TestHelper.signInWithTestUser(); await waitForBatchedUpdates(); - expect(PushNotification.register).toBeCalled(); + expect(PushNotification.register).toHaveBeenCalled(); }); test('Push notifications are unsubscribed after signing out', async () => { await TestHelper.signInWithTestUser(); await TestHelper.signOutTestUser(); - expect(PushNotification.deregister).toBeCalled(); + expect(PushNotification.deregister).toHaveBeenCalled(); }); test('ReconnectApp should push request to the queue', async () => { @@ -242,50 +250,87 @@ describe('Session', () => { expect(getAllPersistedRequests().length).toBe(0); }); - test('SignOutAndRedirectToSignIn should redirect to OldDot when LogOut returns truthy hasOldDotAuthCookies', async () => { - await TestHelper.signInWithTestUser(); - await Onyx.set(ONYXKEYS.NETWORK, {isOffline: true}); + describe('SignOutAndRedirectToSignIn', () => { + test('SignOutAndRedirectToSignIn should redirect to OldDot when LogOut returns truthy hasOldDotAuthCookies', async () => { + await TestHelper.signInWithTestUser(); + await Onyx.set(ONYXKEYS.NETWORK, {isOffline: true}); - (HttpUtils.xhr as jest.MockedFunction) - // This will make the call to OpenApp below return with an expired session code - .mockImplementationOnce(() => - Promise.resolve({ - jsonCode: CONST.JSON_CODE.SUCCESS, - hasOldDotAuthCookies: true, - }), - ); + (HttpUtils.xhr as jest.MockedFunction) + // This will make the call to OpenApp below return with an expired session code + .mockImplementationOnce(() => + Promise.resolve({ + jsonCode: CONST.JSON_CODE.SUCCESS, + hasOldDotAuthCookies: true, + }), + ); - const redirectToSignInSpy = jest.spyOn(SignInRedirect, 'default').mockImplementation(() => Promise.resolve()); + const redirectToSignInSpy = jest.spyOn(SignInRedirect, 'default').mockImplementation(() => Promise.resolve()); - signOutAndRedirectToSignIn(); + signOutAndRedirectToSignIn(); - await waitForBatchedUpdates(); + await waitForBatchedUpdates(); - expect(asyncOpenURL).toHaveBeenCalledWith(Promise.resolve(), `${CONFIG.EXPENSIFY.EXPENSIFY_URL}${CONST.OLDDOT_URLS.SIGN_OUT}`, true, true); - expect(redirectToSignInSpy).toHaveBeenCalled(); - jest.clearAllMocks(); - }); + expect(asyncOpenURL).toHaveBeenCalledWith(expect.any(Promise), `${CONFIG.EXPENSIFY.EXPENSIFY_URL}${CONST.OLDDOT_URLS.SIGN_OUT}`, true, true); + expect(redirectToSignInSpy).toHaveBeenCalled(); + jest.clearAllMocks(); + }); - test('SignOutAndRedirectToSignIn should not redirect to OldDot when LogOut return falsy hasOldDotAuthCookies', async () => { - await TestHelper.signInWithTestUser(); - await Onyx.set(ONYXKEYS.NETWORK, {isOffline: true}); + test('SignOutAndRedirectToSignIn should not redirect to OldDot when LogOut return falsy hasOldDotAuthCookies', async () => { + await TestHelper.signInWithTestUser(); + await Onyx.set(ONYXKEYS.NETWORK, {isOffline: true}); - (HttpUtils.xhr as jest.MockedFunction) - // This will make the call to OpenApp below return with an expired session code - .mockImplementationOnce(() => - Promise.resolve({ - jsonCode: CONST.JSON_CODE.SUCCESS, - }), - ); + (HttpUtils.xhr as jest.MockedFunction) + // This will make the call to OpenApp below return with an expired session code + .mockImplementationOnce(() => + Promise.resolve({ + jsonCode: CONST.JSON_CODE.SUCCESS, + }), + ); - const redirectToSignInSpy = jest.spyOn(SignInRedirect, 'default').mockImplementation(() => Promise.resolve()); + const redirectToSignInSpy = jest.spyOn(SignInRedirect, 'default').mockImplementation(() => Promise.resolve()); - signOutAndRedirectToSignIn(); + signOutAndRedirectToSignIn(); - await waitForBatchedUpdates(); + await waitForBatchedUpdates(); + + expect(asyncOpenURL).not.toHaveBeenCalled(); + expect(redirectToSignInSpy).toHaveBeenCalled(); + jest.clearAllMocks(); + }); + + test('SignOutAndRedirectToSignIn should restore stashed session and redirect to OldDot supportal of supportal agent', async () => { + jest.spyOn(SessionUtil, 'isSupportAuthToken').mockReturnValue(true); + jest.spyOn(SessionUtil, 'hasStashedSession').mockReturnValue(true); + jest.spyOn(SessionUtil, 'signOut').mockResolvedValue(undefined); + jest.spyOn(Onyx, 'clear').mockResolvedValue(undefined); + jest.spyOn(Onyx, 'multiSet').mockResolvedValue(undefined); + + const testStashedCredentials = {login: 'stashed@expensify.com', autoGeneratedLogin: 'stashedAutoLogin', autoGeneratedPassword: 'stashedAutoPassword'}; + const testStashedSession = {authToken: 'stashedAuthToken', email: 'stashed@expensify.com', accountID: 123, creationDate: new Date().getTime()}; - expect(asyncOpenURL).not.toHaveBeenCalled(); - expect(redirectToSignInSpy).toHaveBeenCalled(); - jest.clearAllMocks(); + await Onyx.set(ONYXKEYS.STASHED_CREDENTIALS, testStashedCredentials); + await Onyx.set(ONYXKEYS.STASHED_SESSION, testStashedSession); + + await waitForBatchedUpdates(); + + const onyxClearSpy = Onyx.clear as jest.Mock; + const onyxMultiSetSpy = Onyx.multiSet as jest.Mock; + + signOutAndRedirectToSignIn(false, false, true, true); + + await waitForBatchedUpdates(); + + expect(SessionUtil.signOut).not.toHaveBeenCalled(); + + expect(onyxClearSpy).toHaveBeenCalledWith(KEYS_TO_PRESERVE_SUPPORTAL); + + expect(onyxMultiSetSpy).toHaveBeenCalledWith({ + [ONYXKEYS.CREDENTIALS]: testStashedCredentials, + [ONYXKEYS.SESSION]: testStashedSession, + }); + + expect(buildOldDotURL).toHaveBeenCalledWith(CONST.OLDDOT_URLS.SUPPORTAL_RESTORE_STASHED_LOGIN); + expect(openExternalLink).toHaveBeenCalledWith('mockOldDotURL', undefined, true); + }); }); });