Skip to content

Commit 237759d

Browse files
authored
Merge pull request #56604 from Expensify/michal-signout-redirect
[InternalQA] Redirect to OldDot to clear cookies
2 parents e96b274 + 9b6fda0 commit 237759d

File tree

5 files changed

+153
-82
lines changed

5 files changed

+153
-82
lines changed

src/CONST.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1062,6 +1062,7 @@ const CONST = {
10621062
ADMIN_DOMAINS_URL: 'admin_domains',
10631063
INBOX: 'inbox',
10641064
POLICY_CONNECTIONS_URL: (policyID: string) => `policy?param={"policyID":"${policyID}"}#connections`,
1065+
SIGN_OUT: 'signout',
10651066
},
10661067

10671068
EXPENSIFY_POLICY_DOMAIN,

src/libs/API/types.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,6 @@ const WRITE_COMMANDS = {
8787
VERIFY_IDENTITY: 'VerifyIdentity',
8888
ACCEPT_WALLET_TERMS: 'AcceptWalletTerms',
8989
ANSWER_QUESTIONS_FOR_WALLET: 'AnswerQuestionsForWallet',
90-
LOG_OUT: 'LogOut',
9190
REQUEST_ACCOUNT_VALIDATION_LINK: 'RequestAccountValidationLink',
9291
REQUEST_NEW_VALIDATE_CODE: 'RequestNewValidateCode',
9392
SIGN_IN_WITH_APPLE: 'SignInWithApple',
@@ -533,7 +532,6 @@ type WriteCommandParameters = {
533532
[WRITE_COMMANDS.VERIFY_IDENTITY]: Parameters.VerifyIdentityParams;
534533
[WRITE_COMMANDS.ACCEPT_WALLET_TERMS]: Parameters.AcceptWalletTermsParams;
535534
[WRITE_COMMANDS.ANSWER_QUESTIONS_FOR_WALLET]: Parameters.AnswerQuestionsForWalletParams;
536-
[WRITE_COMMANDS.LOG_OUT]: Parameters.LogOutParams;
537535
[WRITE_COMMANDS.REQUEST_ACCOUNT_VALIDATION_LINK]: Parameters.RequestAccountValidationLinkParams;
538536
[WRITE_COMMANDS.REQUEST_NEW_VALIDATE_CODE]: Parameters.RequestNewValidateCodeParams;
539537
[WRITE_COMMANDS.SIGN_IN_WITH_APPLE]: Parameters.BeginAppleSignInParams;
@@ -1091,6 +1089,7 @@ const SIDE_EFFECT_REQUEST_COMMANDS = {
10911089

10921090
// PayMoneyRequestOnSearch only works online (pattern C) and we need to play the success sound only when the request is successful
10931091
PAY_MONEY_REQUEST_ON_SEARCH: 'PayMoneyRequestOnSearch',
1092+
LOG_OUT: 'LogOut',
10941093
} as const;
10951094

10961095
type SideEffectRequestCommand = ValueOf<typeof SIDE_EFFECT_REQUEST_COMMANDS>;
@@ -1111,6 +1110,7 @@ type SideEffectRequestCommandParameters = {
11111110
[SIDE_EFFECT_REQUEST_COMMANDS.CONNECT_POLICY_TO_QUICKBOOKS_DESKTOP]: Parameters.ConnectPolicyToQuickBooksDesktopParams;
11121111
[SIDE_EFFECT_REQUEST_COMMANDS.BANK_ACCOUNT_CREATE_CORPAY]: Parameters.BankAccountCreateCorpayParams;
11131112
[SIDE_EFFECT_REQUEST_COMMANDS.PAY_MONEY_REQUEST_ON_SEARCH]: Parameters.PayMoneyRequestOnSearchParams;
1113+
[SIDE_EFFECT_REQUEST_COMMANDS.LOG_OUT]: Parameters.LogOutParams;
11141114
};
11151115

11161116
type ApiRequestCommandParameters = WriteCommandParameters & ReadCommandParameters & SideEffectRequestCommandParameters;

src/libs/actions/Session/index.ts

+76-71
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import type {OnyxEntry, OnyxUpdate} from 'react-native-onyx';
66
import Onyx from 'react-native-onyx';
77
import type {ValueOf} from 'type-fest';
88
import * as PersistedRequests from '@libs/actions/PersistedRequests';
9-
import {resolveDuplicationConflictAction} from '@libs/actions/RequestConflictUtils';
109
import * as API from '@libs/API';
1110
import type {
1211
AuthenticatePusherParams,
@@ -25,6 +24,7 @@ import type {
2524
} from '@libs/API/parameters';
2625
import type SignInUserParams from '@libs/API/parameters/SignInUserParams';
2726
import {READ_COMMANDS, SIDE_EFFECT_REQUEST_COMMANDS, WRITE_COMMANDS} from '@libs/API/types';
27+
import asyncOpenURL from '@libs/asyncOpenURL';
2828
import * as Authentication from '@libs/Authentication';
2929
import * as ErrorUtils from '@libs/ErrorUtils';
3030
import Fullstory from '@libs/Fullstory';
@@ -56,6 +56,7 @@ import type {HybridAppRoute, Route} from '@src/ROUTES';
5656
import ROUTES from '@src/ROUTES';
5757
import SCREENS from '@src/SCREENS';
5858
import type Credentials from '@src/types/onyx/Credentials';
59+
import type Response from '@src/types/onyx/Response';
5960
import type Session from '@src/types/onyx/Session';
6061
import type {AutoAuthState} from '@src/types/onyx/Session';
6162
import clearCache from './clearCache';
@@ -178,7 +179,7 @@ function signInWithSupportAuthToken(authToken: string) {
178179
/**
179180
* Clears the Onyx store and redirects user to the sign in page
180181
*/
181-
function signOut() {
182+
function signOut(): Promise<void | Response> {
182183
Log.info('Flushing logs before signing out', true, {}, true);
183184
const params = {
184185
// Send current authToken because we will immediately clear it once triggering this command
@@ -189,14 +190,8 @@ function signOut() {
189190
shouldRetry: false,
190191
};
191192

192-
API.write(
193-
WRITE_COMMANDS.LOG_OUT,
194-
params,
195-
{},
196-
{
197-
checkAndFixConflictingRequest: (persistedRequests) => resolveDuplicationConflictAction(persistedRequests, (request) => request.command === WRITE_COMMANDS.LOG_OUT),
198-
},
199-
);
193+
// eslint-disable-next-line rulesdir/no-api-side-effects-method
194+
return API.makeRequestWithSideEffects(SIDE_EFFECT_REQUEST_COMMANDS.LOG_OUT, params, {});
200195
}
201196

202197
/**
@@ -228,71 +223,81 @@ function isExpiredSession(sessionCreationDate: number): boolean {
228223
function signOutAndRedirectToSignIn(shouldResetToHome?: boolean, shouldStashSession?: boolean, killHybridApp = true) {
229224
Log.info('Redirecting to Sign In because signOut() was called');
230225
hideContextMenu(false);
231-
if (!isAnonymousUser()) {
232-
// In the HybridApp, we want the Old Dot to handle the sign out process
233-
if (NativeModules.HybridAppModule && killHybridApp) {
234-
NativeModules.HybridAppModule.closeReactNativeApp(true, false);
235-
return;
236-
}
237-
// We'll only call signOut if we're not stashing the session and this is not a supportal session,
238-
// otherwise we'll call the API to invalidate the autogenerated credentials used for infinite
239-
// session.
240-
const isSupportal = isSupportAuthToken();
241-
if (!isSupportal && !shouldStashSession) {
242-
signOut();
243-
}
244226

245-
// The function redirectToSignIn will clear the whole storage, so let's create our onyx params
246-
// updates for the credentials before we call it
247-
let onyxSetParams = {};
248-
249-
// If we are not currently using a support token, and we received stashSession as true, we need to
250-
// store the credentials so the user doesn't need to login again after they finish their supportal
251-
// action. This needs to computed before we call `redirectToSignIn`
252-
if (!isSupportal && shouldStashSession) {
253-
onyxSetParams = {
254-
[ONYXKEYS.STASHED_CREDENTIALS]: credentials,
255-
[ONYXKEYS.STASHED_SESSION]: session,
256-
};
257-
}
258-
// If this is a supportal token, and we've received the parameters to stashSession as true, and
259-
// we already have a stashedSession, that means we are supportaled, currently supportaling
260-
// into another account and we want to keep the stashed data from the original account.
261-
if (isSupportal && shouldStashSession && hasStashedSession()) {
262-
onyxSetParams = {
263-
[ONYXKEYS.STASHED_CREDENTIALS]: stashedCredentials,
264-
[ONYXKEYS.STASHED_SESSION]: stashedSession,
265-
};
266-
}
267-
// Now if this is a supportal access, we do not want to stash the current session and we have a
268-
// stashed session, then we need to restore the stashed session instead of completely logging out
269-
if (isSupportal && !shouldStashSession && hasStashedSession()) {
270-
onyxSetParams = {
271-
[ONYXKEYS.CREDENTIALS]: stashedCredentials,
272-
[ONYXKEYS.SESSION]: stashedSession,
273-
};
274-
}
275-
if (isSupportal && !shouldStashSession && !hasStashedSession()) {
276-
Log.info('No stashed session found for supportal access, clearing the session');
227+
if (isAnonymousUser()) {
228+
if (!Navigation.isActiveRoute(ROUTES.SIGN_IN_MODAL)) {
229+
if (shouldResetToHome) {
230+
Navigation.resetToHome();
231+
}
232+
Navigation.navigate(ROUTES.SIGN_IN_MODAL);
233+
Linking.getInitialURL().then((url) => {
234+
const reportID = getReportIDFromLink(url);
235+
if (reportID) {
236+
Onyx.merge(ONYXKEYS.LAST_OPENED_PUBLIC_ROOM_ID, reportID);
237+
}
238+
});
277239
}
278-
redirectToSignIn().then(() => {
240+
return;
241+
}
242+
243+
// In the HybridApp, we want the Old Dot to handle the sign out process
244+
if (NativeModules.HybridAppModule && killHybridApp) {
245+
NativeModules.HybridAppModule.closeReactNativeApp(true, false);
246+
return;
247+
}
248+
// We'll only call signOut if we're not stashing the session and this is not a supportal session,
249+
// otherwise we'll call the API to invalidate the autogenerated credentials used for infinite
250+
// session.
251+
const isSupportal = isSupportAuthToken();
252+
const signOutPromise: Promise<void | Response> = !isSupportal && !shouldStashSession ? signOut() : Promise.resolve();
253+
254+
// The function redirectToSignIn will clear the whole storage, so let's create our onyx params
255+
// updates for the credentials before we call it
256+
let onyxSetParams = {};
257+
258+
// If we are not currently using a support token, and we received stashSession as true, we need to
259+
// store the credentials so the user doesn't need to login again after they finish their supportal
260+
// action. This needs to computed before we call `redirectToSignIn`
261+
if (!isSupportal && shouldStashSession) {
262+
onyxSetParams = {
263+
[ONYXKEYS.STASHED_CREDENTIALS]: credentials,
264+
[ONYXKEYS.STASHED_SESSION]: session,
265+
};
266+
}
267+
// If this is a supportal token, and we've received the parameters to stashSession as true, and
268+
// we already have a stashedSession, that means we are supportaled, currently supportaling
269+
// into another account and we want to keep the stashed data from the original account.
270+
if (isSupportal && shouldStashSession && hasStashedSession()) {
271+
onyxSetParams = {
272+
[ONYXKEYS.STASHED_CREDENTIALS]: stashedCredentials,
273+
[ONYXKEYS.STASHED_SESSION]: stashedSession,
274+
};
275+
}
276+
// Now if this is a supportal access, we do not want to stash the current session and we have a
277+
// stashed session, then we need to restore the stashed session instead of completely logging out
278+
if (isSupportal && !shouldStashSession && hasStashedSession()) {
279+
onyxSetParams = {
280+
[ONYXKEYS.CREDENTIALS]: stashedCredentials,
281+
[ONYXKEYS.SESSION]: stashedSession,
282+
};
283+
}
284+
if (isSupportal && !shouldStashSession && !hasStashedSession()) {
285+
Log.info('No stashed session found for supportal access, clearing the session');
286+
}
287+
288+
// Wait for signOut (if called), then redirect and update Onyx.
289+
signOutPromise
290+
.then((response) => {
279291
Onyx.multiSet(onyxSetParams);
280-
});
281-
} else {
282-
if (Navigation.isActiveRoute(ROUTES.SIGN_IN_MODAL)) {
283-
return;
284-
}
285-
if (shouldResetToHome) {
286-
Navigation.resetToHome();
287-
}
288-
Navigation.navigate(ROUTES.SIGN_IN_MODAL);
289-
Linking.getInitialURL().then((url) => {
290-
const reportID = getReportIDFromLink(url);
291-
if (reportID) {
292-
Onyx.merge(ONYXKEYS.LAST_OPENED_PUBLIC_ROOM_ID, reportID);
292+
293+
if (response?.hasOldDotAuthCookies) {
294+
Log.info('Redirecting to OldDot sign out');
295+
asyncOpenURL(redirectToSignIn(), `${CONFIG.EXPENSIFY.EXPENSIFY_URL}${CONST.OLDDOT_URLS.SIGN_OUT}`, true, true);
296+
} else {
297+
redirectToSignIn();
293298
}
294-
});
295-
}
299+
})
300+
.catch((error: string) => Log.warn('Error during sign out process:', error));
296301
}
297302

298303
/**

src/types/onyx/Response.ts

+3
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,9 @@ type Response = {
9292

9393
/** The ID of the original user (returned when in delegate mode) */
9494
requesterID?: number;
95+
96+
/** If there are httponly OldDot authentication cookies stored */
97+
hasOldDotAuthCookies?: boolean;
9598
};
9699

97100
export default Response;

tests/actions/SessionTest.ts

+71-9
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,18 @@ import type {OnyxEntry} from 'react-native-onyx';
44
import {confirmReadyToOpenApp, openApp, reconnectApp} from '@libs/actions/App';
55
import OnyxUpdateManager from '@libs/actions/OnyxUpdateManager';
66
import {getAll as getAllPersistedRequests} from '@libs/actions/PersistedRequests';
7+
// eslint-disable-next-line no-restricted-syntax
8+
import * as SignInRedirect from '@libs/actions/SignInRedirect';
79
import {WRITE_COMMANDS} from '@libs/API/types';
10+
import asyncOpenURL from '@libs/asyncOpenURL';
811
import HttpUtils from '@libs/HttpUtils';
912
import PushNotification from '@libs/Notification/PushNotification';
1013
// This lib needs to be imported, but it has nothing to export since all it contains is an Onyx connection
1114
import '@libs/Notification/PushNotification/subscribePushNotification';
15+
import CONFIG from '@src/CONFIG';
1216
import CONST from '@src/CONST';
1317
import * as SessionUtil from '@src/libs/actions/Session';
18+
import {signOutAndRedirectToSignIn} from '@src/libs/actions/Session';
1419
import ONYXKEYS from '@src/ONYXKEYS';
1520
import type {Credentials, Session} from '@src/types/onyx';
1621
import * as TestHelper from '../utils/TestHelper';
@@ -23,6 +28,9 @@ HttpUtils.xhr = jest.fn<typeof HttpUtils.xhr>();
2328
// Mocked to ensure push notifications are subscribed/unsubscribed as the session changes
2429
jest.mock('@libs/Notification/PushNotification');
2530

31+
// Mocked to check SignOutAndRedirectToSignIn behavior
32+
jest.mock('@libs/asyncOpenURL');
33+
2634
Onyx.init({
2735
keys: ONYXKEYS,
2836
});
@@ -207,23 +215,77 @@ describe('Session', () => {
207215
expect(getAllPersistedRequests().length).toBe(0);
208216
});
209217

210-
test('LogOut should replace same requests from the queue instead of adding new one', async () => {
218+
test('SignOut should return a promise with response containing hasOldDotAuthCookies', async () => {
211219
await TestHelper.signInWithTestUser();
212220
await Onyx.set(ONYXKEYS.NETWORK, {isOffline: true});
213221

214-
SessionUtil.signOut();
215-
SessionUtil.signOut();
216-
SessionUtil.signOut();
217-
SessionUtil.signOut();
218-
SessionUtil.signOut();
222+
(HttpUtils.xhr as jest.MockedFunction<typeof HttpUtils.xhr>)
223+
// This will make the call to OpenApp below return with an expired session code
224+
.mockImplementationOnce(() =>
225+
Promise.resolve({
226+
jsonCode: CONST.JSON_CODE.SUCCESS,
227+
hasOldDotAuthCookies: true,
228+
}),
229+
);
230+
231+
const signOutPromise = SessionUtil.signOut();
219232

220-
await waitForBatchedUpdates();
233+
expect(signOutPromise).toBeInstanceOf(Promise);
221234

222-
expect(getAllPersistedRequests().length).toBe(1);
223-
expect(getAllPersistedRequests().at(0)?.command).toBe(WRITE_COMMANDS.LOG_OUT);
235+
expect(await signOutPromise).toStrictEqual({
236+
jsonCode: CONST.JSON_CODE.SUCCESS,
237+
hasOldDotAuthCookies: true,
238+
});
224239

225240
await Onyx.set(ONYXKEYS.NETWORK, {isOffline: false});
226241

227242
expect(getAllPersistedRequests().length).toBe(0);
228243
});
244+
245+
test('SignOutAndRedirectToSignIn should redirect to OldDot when LogOut returns truthy hasOldDotAuthCookies', async () => {
246+
await TestHelper.signInWithTestUser();
247+
await Onyx.set(ONYXKEYS.NETWORK, {isOffline: true});
248+
249+
(HttpUtils.xhr as jest.MockedFunction<typeof HttpUtils.xhr>)
250+
// This will make the call to OpenApp below return with an expired session code
251+
.mockImplementationOnce(() =>
252+
Promise.resolve({
253+
jsonCode: CONST.JSON_CODE.SUCCESS,
254+
hasOldDotAuthCookies: true,
255+
}),
256+
);
257+
258+
const redirectToSignInSpy = jest.spyOn(SignInRedirect, 'default').mockImplementation(() => Promise.resolve());
259+
260+
signOutAndRedirectToSignIn();
261+
262+
await waitForBatchedUpdates();
263+
264+
expect(asyncOpenURL).toHaveBeenCalledWith(Promise.resolve(), `${CONFIG.EXPENSIFY.EXPENSIFY_URL}${CONST.OLDDOT_URLS.SIGN_OUT}`, true, true);
265+
expect(redirectToSignInSpy).toHaveBeenCalled();
266+
jest.clearAllMocks();
267+
});
268+
269+
test('SignOutAndRedirectToSignIn should not redirect to OldDot when LogOut return falsy hasOldDotAuthCookies', async () => {
270+
await TestHelper.signInWithTestUser();
271+
await Onyx.set(ONYXKEYS.NETWORK, {isOffline: true});
272+
273+
(HttpUtils.xhr as jest.MockedFunction<typeof HttpUtils.xhr>)
274+
// This will make the call to OpenApp below return with an expired session code
275+
.mockImplementationOnce(() =>
276+
Promise.resolve({
277+
jsonCode: CONST.JSON_CODE.SUCCESS,
278+
}),
279+
);
280+
281+
const redirectToSignInSpy = jest.spyOn(SignInRedirect, 'default').mockImplementation(() => Promise.resolve());
282+
283+
signOutAndRedirectToSignIn();
284+
285+
await waitForBatchedUpdates();
286+
287+
expect(asyncOpenURL).not.toHaveBeenCalled();
288+
expect(redirectToSignInSpy).toHaveBeenCalled();
289+
jest.clearAllMocks();
290+
});
229291
});

0 commit comments

Comments
 (0)