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: 1 addition & 0 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
13 changes: 12 additions & 1 deletion src/libs/actions/Link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,4 +240,15 @@ function getTravelDotLink(policyID: OnyxEntry<string>) {
});
}

export {openOldDotLink, openExternalLink, openLink, getInternalNewExpensifyPath, getInternalExpensifyPath, openTravelDotLink, buildTravelDotURL, openExternalLinkWithToken, getTravelDotLink};
export {
openOldDotLink,
openExternalLink,
openLink,
getInternalNewExpensifyPath,
getInternalExpensifyPath,
openTravelDotLink,
buildTravelDotURL,
openExternalLinkWithToken,
getTravelDotLink,
buildOldDotURL,
};
45 changes: 45 additions & 0 deletions src/libs/actions/Session/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
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 {
Expand Down Expand Up @@ -77,7 +78,7 @@
let isHybridAppSetupFinished = false;
let hasSwitchedAccountInHybridMode = false;

Onyx.connect({

Check warning on line 81 in src/libs/actions/Session/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.SESSION,
callback: (value) => {
session = value ?? {};
Expand All @@ -102,25 +103,25 @@
});

let stashedSession: Session = {};
Onyx.connect({

Check warning on line 106 in src/libs/actions/Session/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.STASHED_SESSION,
callback: (value) => (stashedSession = value ?? {}),
});

let credentials: Credentials = {};
Onyx.connect({

Check warning on line 112 in src/libs/actions/Session/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.CREDENTIALS,
callback: (value) => (credentials = value ?? {}),
});

let stashedCredentials: Credentials = {};
Onyx.connect({

Check warning on line 118 in src/libs/actions/Session/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.STASHED_CREDENTIALS,
callback: (value) => (stashedCredentials = value ?? {}),
});

let activePolicyID: OnyxEntry<string>;
Onyx.connect({

Check warning on line 124 in src/libs/actions/Session/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.NVP_ACTIVE_POLICY_ID,
callback: (newActivePolicyID) => {
activePolicyID = newActivePolicyID;
Expand Down Expand Up @@ -267,6 +268,25 @@
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);
Expand Down Expand Up @@ -358,6 +378,30 @@
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);
Expand Down Expand Up @@ -1427,6 +1471,7 @@
}

export {
KEYS_TO_PRESERVE_SUPPORTAL,
beginSignIn,
beginAppleSignIn,
beginGoogleSignIn,
Expand Down
4 changes: 2 additions & 2 deletions src/pages/settings/InitialSettingsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
Expand Down
119 changes: 82 additions & 37 deletions tests/actions/SessionTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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';
Expand All @@ -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,
});
Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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<typeof HttpUtils.xhr>)
// 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<typeof HttpUtils.xhr>)
// 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<typeof HttpUtils.xhr>)
// 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<typeof HttpUtils.xhr>)
// 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);
});
});
});
Loading