Skip to content

Commit fc41e57

Browse files
authored
Merge pull request Expensify#72218 from software-mansion-labs/fix/onboarding-required-2fa
Implement logic to prevent onboarding flow when 2FA setup is required.
2 parents 98cefa4 + 85014e5 commit fc41e57

6 files changed

Lines changed: 65 additions & 49 deletions

File tree

src/hooks/useOnboardingFlow.ts

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {isSingleNewDotEntrySelector} from '@selectors/HybridApp';
22
import {emailSelector} from '@selectors/Session';
3-
import {useEffect, useRef} from 'react';
3+
import {useEffect, useMemo, useRef} from 'react';
44
import {InteractionManager} from 'react-native';
55
import {startOnboardingFlow} from '@libs/actions/Welcome/OnboardingFlow';
66
import getCurrentUrl from '@libs/Navigation/currentUrl';
@@ -30,13 +30,14 @@ function useOnboardingFlowRouter() {
3030
});
3131
const [currentOnboardingPurposeSelected] = useOnyx(ONYXKEYS.ONBOARDING_PURPOSE_SELECTED, {canBeMissing: true});
3232
const [currentOnboardingCompanySize] = useOnyx(ONYXKEYS.ONBOARDING_COMPANY_SIZE, {canBeMissing: true});
33-
const [onboardingInitialPath, onboardingInitialPathResult] = useOnyx(ONYXKEYS.ONBOARDING_LAST_VISITED_PATH, {canBeMissing: true});
34-
const isOnboardingInitialPathLoading = isLoadingOnyxValue(onboardingInitialPathResult);
33+
const [onboardingInitialPath, onboardingInitialPathMetadata] = useOnyx(ONYXKEYS.ONBOARDING_LAST_VISITED_PATH, {canBeMissing: true});
34+
const [account, accountMetadata] = useOnyx(ONYXKEYS.ACCOUNT, {canBeMissing: true});
35+
const isOnboardingLoading = isLoadingOnyxValue(onboardingInitialPathMetadata, accountMetadata);
3536

36-
const [account] = useOnyx(ONYXKEYS.ACCOUNT, {canBeMissing: true});
3737
const [sessionEmail] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: true, selector: emailSelector});
3838
const isLoggingInAsNewSessionUser = isLoggingInAsNewUser(currentUrl, sessionEmail);
3939
const startedOnboardingFlowRef = useRef(false);
40+
const started2FAFlowRef = useRef(false);
4041
const [tryNewDot, tryNewDotMetadata] = useOnyx(ONYXKEYS.NVP_TRY_NEW_DOT, {
4142
selector: tryNewDotOnyxSelector,
4243
canBeMissing: true,
@@ -47,6 +48,10 @@ function useOnboardingFlowRouter() {
4748

4849
const [isSingleNewDotEntry, isSingleNewDotEntryMetadata] = useOnyx(ONYXKEYS.HYBRID_APP, {selector: isSingleNewDotEntrySelector, canBeMissing: true});
4950
const {typeMenuSections} = useSearchTypeMenuSections();
51+
const shouldShowRequire2FAPage = useMemo(
52+
() => (!!account?.needsTwoFactorAuthSetup && !account?.requiresTwoFactorAuth) || (!!account?.twoFactorAuthSetupInProgress && !hasCompletedGuidedSetupFlowSelector(onboardingValues)),
53+
[account?.needsTwoFactorAuthSetup, account?.requiresTwoFactorAuth, account?.twoFactorAuthSetupInProgress, onboardingValues],
54+
);
5055

5156
useEffect(() => {
5257
// This should delay opening the onboarding modal so it does not interfere with the ongoing ReportScreen params changes
@@ -56,7 +61,8 @@ function useOnboardingFlowRouter() {
5661
if (currentUrl?.includes(ROUTES.TRANSITION_BETWEEN_APPS) && isLoggingInAsNewSessionUser) {
5762
return;
5863
}
59-
if (isLoadingApp !== false || isOnboardingInitialPathLoading) {
64+
65+
if (isLoadingApp !== false || isOnboardingLoading) {
6066
return;
6167
}
6268

@@ -73,6 +79,16 @@ function useOnboardingFlowRouter() {
7379
return;
7480
}
7581

82+
if (shouldShowRequire2FAPage) {
83+
if (started2FAFlowRef.current) {
84+
startedOnboardingFlowRef.current = false;
85+
return;
86+
}
87+
started2FAFlowRef.current = true;
88+
Navigation.navigate(ROUTES.REQUIRE_TWO_FACTOR_AUTH);
89+
return;
90+
}
91+
7692
if (hasBeenAddedToNudgeMigration && !isProductTrainingElementDismissed('migratedUserWelcomeModal', dismissedProductTraining)) {
7793
const navigationState = navigationRef.getRootState();
7894
const lastRoute = navigationState.routes.at(-1);
@@ -149,11 +165,17 @@ function useOnboardingFlowRouter() {
149165
currentOnboardingCompanySize,
150166
currentOnboardingPurposeSelected,
151167
onboardingInitialPath,
152-
isOnboardingInitialPathLoading,
168+
isOnboardingLoading,
153169
typeMenuSections,
170+
shouldShowRequire2FAPage,
154171
]);
155172

156-
return {isOnboardingCompleted: hasCompletedGuidedSetupFlowSelector(onboardingValues), isHybridAppOnboardingCompleted};
173+
return {
174+
isOnboardingCompleted: hasCompletedGuidedSetupFlowSelector(onboardingValues),
175+
isHybridAppOnboardingCompleted,
176+
shouldShowRequire2FAPage,
177+
isOnboardingLoading: !!onboardingValues?.isLoading,
178+
};
157179
}
158180

159181
export default useOnboardingFlowRouter;

src/libs/Navigation/AppNavigator/AuthScreens.tsx

Lines changed: 2 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import type {RouteProp} from '@react-navigation/native';
2-
import {useNavigation} from '@react-navigation/native';
32
import type {StackCardInterpolationProps} from '@react-navigation/stack';
43
import React, {memo, useContext, useEffect, useMemo, useRef, useState} from 'react';
54
import ComposeProviders from '@components/ComposeProviders';
@@ -156,11 +155,8 @@ function AuthScreens() {
156155
});
157156
const [onboardingCompanySize] = useOnyx(ONYXKEYS.ONBOARDING_COMPANY_SIZE, {canBeMissing: true});
158157
const [userReportedIntegration] = useOnyx(ONYXKEYS.ONBOARDING_USER_REPORTED_INTEGRATION, {canBeMissing: true});
159-
const {isOnboardingCompleted} = useOnboardingFlowRouter();
160-
const [isOnboardingLoading] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {canBeMissing: true, selector: (value) => !!value?.isLoading});
158+
const {isOnboardingCompleted, shouldShowRequire2FAPage, isOnboardingLoading} = useOnboardingFlowRouter();
161159
const prevIsOnboardingLoading = usePrevious(isOnboardingLoading);
162-
const [shouldShowRequire2FAPage, setShouldShowRequire2FAPage] = useState(!!account?.needsTwoFactorAuthSetup && !account.requiresTwoFactorAuth);
163-
const navigation = useNavigation();
164160
const {initialURL, isAuthenticatedAtStartup, setIsAuthenticatedAtStartup} = useContext(InitialURLContext);
165161
const modalCardStyleInterpolator = useModalCardStyleInterpolator();
166162

@@ -223,20 +219,6 @@ function AuthScreens() {
223219
};
224220
}, [theme]);
225221

226-
useEffect(() => {
227-
if (!account?.needsTwoFactorAuthSetup || !!account.requiresTwoFactorAuth || shouldShowRequire2FAPage) {
228-
return;
229-
}
230-
setShouldShowRequire2FAPage(true);
231-
}, [account?.needsTwoFactorAuthSetup, account?.requiresTwoFactorAuth, shouldShowRequire2FAPage]);
232-
233-
useEffect(() => {
234-
if (!shouldShowRequire2FAPage) {
235-
return;
236-
}
237-
Navigation.navigate(ROUTES.REQUIRE_TWO_FACTOR_AUTH);
238-
}, [shouldShowRequire2FAPage]);
239-
240222
useEffect(() => {
241223
const shortcutsOverviewShortcutConfig = CONST.KEYBOARD_SHORTCUTS.SHORTCUTS;
242224
const searchShortcutConfig = CONST.KEYBOARD_SHORTCUTS.SEARCH;
@@ -614,20 +596,7 @@ function AuthScreens() {
614596
name={NAVIGATORS.RIGHT_MODAL_NAVIGATOR}
615597
options={rootNavigatorScreenOptions.rightModalNavigator}
616598
component={RightModalNavigator}
617-
listeners={{
618-
...modalScreenListenersWithCancelSearch,
619-
beforeRemove: () => {
620-
modalScreenListenersWithCancelSearch.beforeRemove();
621-
622-
// When a 2FA RHP page is closed, if the 2FA require page is visible and the user has now enabled the 2FA, then remove the 2FA require page from the navigator.
623-
const routeParams = navigation.getState()?.routes?.at(-1)?.params;
624-
const screen = routeParams && 'screen' in routeParams ? routeParams.screen : '';
625-
if (!shouldShowRequire2FAPage || !account?.requiresTwoFactorAuth || screen !== SCREENS.RIGHT_MODAL.TWO_FACTOR_AUTH) {
626-
return;
627-
}
628-
setShouldShowRequire2FAPage(false);
629-
},
630-
}}
599+
listeners={modalScreenListenersWithCancelSearch}
631600
/>
632601
<RootStack.Screen
633602
name={SCREENS.DESKTOP_SIGN_IN_REDIRECT}

src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ function RightModalNavigator({navigation, route}: RightModalNavigatorProps) {
8787
listeners={{
8888
beforeRemove: () => {
8989
// eslint-disable-next-line deprecation/deprecation
90-
InteractionManager.runAfterInteractions(clearTwoFactorAuthData);
90+
InteractionManager.runAfterInteractions(() => clearTwoFactorAuthData(true));
9191
},
9292
}}
9393
/>

src/libs/actions/TwoFactorAuthActions.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {InteractionManager} from 'react-native';
2+
import type {OnyxMergeInput} from 'react-native-onyx';
23
import Onyx from 'react-native-onyx';
34
import Navigation from '@libs/Navigation/Navigation';
45
import ONYXKEYS from '@src/ONYXKEYS';
@@ -7,12 +8,18 @@ import type {Route} from '@src/ROUTES';
78
/**
89
* Clear 2FA data if the flow is interrupted without finishing
910
*/
10-
function clearTwoFactorAuthData() {
11-
Onyx.merge(ONYXKEYS.ACCOUNT, {recoveryCodes: null, twoFactorAuthSecretKey: null, codesAreCopied: false});
11+
function clearTwoFactorAuthData(clearProgress = false) {
12+
const data: OnyxMergeInput<typeof ONYXKEYS.ACCOUNT> = {recoveryCodes: null, twoFactorAuthSecretKey: null, codesAreCopied: false};
13+
14+
if (clearProgress) {
15+
data.twoFactorAuthSetupInProgress = null;
16+
}
17+
18+
Onyx.merge(ONYXKEYS.ACCOUNT, data);
1219
}
1320

1421
function setCodesAreCopied() {
15-
Onyx.merge(ONYXKEYS.ACCOUNT, {codesAreCopied: true});
22+
Onyx.merge(ONYXKEYS.ACCOUNT, {codesAreCopied: true, twoFactorAuthSetupInProgress: true});
1623
}
1724

1825
function quitAndNavigateBack(backTo?: Route) {

src/libs/actions/Welcome/index.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,15 @@ import CONFIG from '@src/CONFIG';
1010
import type {OnboardingAccounting} from '@src/CONST';
1111
import ONYXKEYS from '@src/ONYXKEYS';
1212
import ROUTES from '@src/ROUTES';
13-
import type {OnboardingPurpose} from '@src/types/onyx';
13+
import type {Account, OnboardingPurpose} from '@src/types/onyx';
1414
import type Onboarding from '@src/types/onyx/Onboarding';
1515
import {isEmptyObject} from '@src/types/utils/EmptyObject';
1616
import type {GetOnboardingInitialPathParamsType, OnboardingCompanySize} from './OnboardingFlow';
1717
import {startOnboardingFlow} from './OnboardingFlow';
1818

19-
type OnboardingData = Onboarding | undefined;
20-
2119
let isLoadingReportData = true;
22-
let onboarding: OnboardingData;
20+
let onboarding: Onboarding | undefined;
21+
let account: Account | undefined;
2322

2423
type HasCompletedOnboardingFlowProps = {
2524
onCompleted?: () => void;
@@ -44,6 +43,12 @@ function onServerDataReady(): Promise<void> {
4443
let isOnboardingInProgress = false;
4544
function isOnboardingFlowCompleted({onCompleted, onNotCompleted, onCanceled}: HasCompletedOnboardingFlowProps) {
4645
isOnboardingFlowStatusKnownPromise.then(() => {
46+
// Don't trigger onboarding if we are showing the require 2FA page
47+
const shouldShowRequire2FAPage = account && !!account.needsTwoFactorAuthSetup && (!account.requiresTwoFactorAuth || !!account.twoFactorAuthSetupInProgress);
48+
if (shouldShowRequire2FAPage) {
49+
return;
50+
}
51+
4752
if (isEmptyObject(onboarding) || onboarding?.hasCompletedGuidedSetupFlow === undefined) {
4853
onCanceled?.();
4954
return;
@@ -81,7 +86,7 @@ function checkServerDataReady() {
8186
* Check if the onboarding data is loaded
8287
*/
8388
function checkOnboardingDataReady() {
84-
if (onboarding === undefined) {
89+
if (onboarding === undefined || account === undefined) {
8590
return;
8691
}
8792

@@ -163,6 +168,16 @@ function completeHybridAppOnboarding() {
163168
});
164169
}
165170

171+
// We use `connectWithoutView` here since this connection only updates a module-level variable
172+
// and doesn't need to trigger component re-renders.
173+
Onyx.connectWithoutView({
174+
key: ONYXKEYS.ACCOUNT,
175+
callback: (value) => {
176+
account = value;
177+
checkOnboardingDataReady();
178+
},
179+
});
180+
166181
// We use `connectWithoutView` here since this connection only updates a module-level variable
167182
// and doesn't need to trigger component re-renders.
168183
Onyx.connectWithoutView({

src/types/onyx/Account.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,9 @@ type Account = {
9292
/** Whether this account needs 2FA setup before it can be used. eg: 2FA is required when Xero integration is enabled */
9393
needsTwoFactorAuthSetup?: boolean;
9494

95+
/** Whether the account 2FA setup is in progress, driven by the frontend */
96+
twoFactorAuthSetupInProgress?: boolean;
97+
9598
/** Whether the account is validated */
9699
validated?: boolean;
97100

0 commit comments

Comments
 (0)