Skip to content

Commit c7934c5

Browse files
authored
Merge pull request #48030 from Expensify/tgolen-require-2fa-to-disable
Require a 2FA code to disable 2FA
2 parents 4d9f542 + 6066f8d commit c7934c5

File tree

14 files changed

+122
-31
lines changed

14 files changed

+122
-31
lines changed

src/CONST.ts

+1
Original file line numberDiff line numberDiff line change
@@ -3887,6 +3887,7 @@ const CONST = {
38873887
SUCCESS: 'SUCCESS',
38883888
ENABLED: 'ENABLED',
38893889
DISABLED: 'DISABLED',
3890+
GETCODE: 'GETCODE',
38903891
},
38913892
DELEGATE_ROLE: {
38923893
SUBMITTER: 'submitter',

src/languages/en.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1117,7 +1117,7 @@ export default {
11171117
twoFactorAuthEnabled: 'Two-factor authentication enabled',
11181118
whatIsTwoFactorAuth: 'Two-factor authentication (2FA) helps keep your account safe. When logging in, you’ll need to enter a code generated by your preferred authenticator app.',
11191119
disableTwoFactorAuth: 'Disable two-factor authentication',
1120-
disableTwoFactorAuthConfirmation: 'Two-factor authentication keeps your account more secure. Are you sure you want to disable it?',
1120+
explainProcessToRemove: 'In order to disable two-factor authentication (2FA), please enter a valid code from your authentication app.',
11211121
disabled: 'Two-factor authentication is now disabled',
11221122
noAuthenticatorApp: 'You’ll no longer require an authenticator app to log into Expensify.',
11231123
stepCodes: 'Recovery codes',

src/languages/es.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1122,7 +1122,7 @@ export default {
11221122
whatIsTwoFactorAuth:
11231123
'La autenticación de dos factores (2FA) ayuda a mantener tu cuenta segura. Al iniciar sesión, deberás ingresar un código generado por tu aplicación de autenticación preferida.',
11241124
disableTwoFactorAuth: 'Deshabilitar la autenticación de dos factores',
1125-
disableTwoFactorAuthConfirmation: 'La autenticación de dos factores mantiene tu cuenta más segura. ¿Estás seguro de que quieres desactivarla?',
1125+
explainProcessToRemove: 'Para deshabilitar la autenticación de dos factores (2FA), por favor introduce un código válido de tu aplicación de autenticación.',
11261126
disabled: 'La autenticación de dos factores está ahora deshabilitada',
11271127
noAuthenticatorApp: 'Ya no necesitarás una aplicación de autenticación para iniciar sesión en Expensify.',
11281128
stepCodes: 'Códigos de recuperación',
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
type DisableTwoFactorAuthParams = {
2+
twoFactorAuthCode: string;
3+
};
4+
5+
export default DisableTwoFactorAuthParams;

src/libs/API/parameters/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ export type {default as ValidateBankAccountWithTransactionsParams} from './Valid
8484
export type {default as ValidateLoginParams} from './ValidateLoginParams';
8585
export type {default as ValidateSecondaryLoginParams} from './ValidateSecondaryLoginParams';
8686
export type {default as ValidateTwoFactorAuthParams} from './ValidateTwoFactorAuthParams';
87+
export type {default as DisableTwoFactorAuthParams} from './DisableTwoFactorAuthParams';
8788
export type {default as VerifyIdentityForBankAccountParams} from './VerifyIdentityForBankAccountParams';
8889
export type {default as AnswerQuestionsForWalletParams} from './AnswerQuestionsForWalletParams';
8990
export type {default as AddCommentOrAttachementParams} from './AddCommentOrAttachementParams';

src/libs/API/types.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -425,7 +425,7 @@ type WriteCommandParameters = {
425425
[WRITE_COMMANDS.REQUEST_UNLINK_VALIDATION_LINK]: Parameters.RequestUnlinkValidationLinkParams;
426426
[WRITE_COMMANDS.UNLINK_LOGIN]: Parameters.UnlinkLoginParams;
427427
[WRITE_COMMANDS.ENABLE_TWO_FACTOR_AUTH]: null;
428-
[WRITE_COMMANDS.DISABLE_TWO_FACTOR_AUTH]: null;
428+
[WRITE_COMMANDS.DISABLE_TWO_FACTOR_AUTH]: Parameters.DisableTwoFactorAuthParams;
429429
[WRITE_COMMANDS.ADD_COMMENT]: Parameters.AddCommentOrAttachementParams;
430430
[WRITE_COMMANDS.ADD_ATTACHMENT]: Parameters.AddCommentOrAttachementParams;
431431
[WRITE_COMMANDS.ADD_TEXT_AND_ATTACHMENT]: Parameters.AddCommentOrAttachementParams;

src/libs/actions/Session/index.ts

+15-2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type {
1212
BeginAppleSignInParams,
1313
BeginGoogleSignInParams,
1414
BeginSignInParams,
15+
DisableTwoFactorAuthParams,
1516
RequestAccountValidationLinkParams,
1617
RequestNewValidateCodeParams,
1718
RequestUnlinkValidationLinkParams,
@@ -877,7 +878,7 @@ function unlinkLogin(accountID: number, validateCode: string) {
877878
/**
878879
* Toggles two-factor authentication based on the `enable` parameter
879880
*/
880-
function toggleTwoFactorAuth(enable: boolean) {
881+
function toggleTwoFactorAuth(enable: boolean, twoFactorAuthCode = '') {
881882
const optimisticData: OnyxUpdate[] = [
882883
{
883884
onyxMethod: Onyx.METHOD.MERGE,
@@ -894,6 +895,9 @@ function toggleTwoFactorAuth(enable: boolean) {
894895
key: ONYXKEYS.ACCOUNT,
895896
value: {
896897
isLoading: false,
898+
899+
// When disabling 2FA, the user needs to end up on the step that confirms the setting was disabled
900+
twoFactorAuthStep: enable ? undefined : CONST.TWO_FACTOR_AUTH_STEPS.DISABLED,
897901
},
898902
},
899903
];
@@ -908,7 +912,16 @@ function toggleTwoFactorAuth(enable: boolean) {
908912
},
909913
];
910914

911-
API.write(enable ? WRITE_COMMANDS.ENABLE_TWO_FACTOR_AUTH : WRITE_COMMANDS.DISABLE_TWO_FACTOR_AUTH, null, {optimisticData, successData, failureData});
915+
if (enable) {
916+
API.write(WRITE_COMMANDS.ENABLE_TWO_FACTOR_AUTH, null, {optimisticData, successData, failureData});
917+
return;
918+
}
919+
920+
// A 2FA code is required to disable 2FA
921+
const params: DisableTwoFactorAuthParams = {twoFactorAuthCode};
922+
923+
// eslint-disable-next-line rulesdir/no-multiple-api-calls
924+
API.write(WRITE_COMMANDS.DISABLE_TWO_FACTOR_AUTH, params, {optimisticData, successData, failureData});
912925
}
913926

914927
function updateAuthTokenAndOpenApp(authToken?: string, encryptedAuthToken?: string) {

src/pages/settings/Security/TwoFactorAuth/Steps/EnabledStep.tsx

+2-21
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import React, {useState} from 'react';
1+
import React from 'react';
22
import {View} from 'react-native';
3-
import ConfirmModal from '@components/ConfirmModal';
43
import * as Expensicons from '@components/Icon/Expensicons';
54
import * as Illustrations from '@components/Icon/Illustrations';
65
import ScrollView from '@components/ScrollView';
@@ -11,13 +10,11 @@ import useTheme from '@hooks/useTheme';
1110
import useThemeStyles from '@hooks/useThemeStyles';
1211
import StepWrapper from '@pages/settings/Security/TwoFactorAuth/StepWrapper/StepWrapper';
1312
import useTwoFactorAuthContext from '@pages/settings/Security/TwoFactorAuth/TwoFactorAuthContext/useTwoFactorAuth';
14-
import * as Session from '@userActions/Session';
1513
import CONST from '@src/CONST';
1614

1715
function EnabledStep() {
1816
const theme = useTheme();
1917
const styles = useThemeStyles();
20-
const [isConfirmModalVisible, setIsConfirmModalVisible] = useState(false);
2118

2219
const {setStep} = useTwoFactorAuthContext();
2320

@@ -33,7 +30,7 @@ function EnabledStep() {
3330
{
3431
title: translate('twoFactorAuth.disableTwoFactorAuth'),
3532
onPress: () => {
36-
setIsConfirmModalVisible(true);
33+
setStep(CONST.TWO_FACTOR_AUTH_STEPS.GETCODE);
3734
},
3835
icon: Expensicons.Close,
3936
iconFill: theme.danger,
@@ -46,22 +43,6 @@ function EnabledStep() {
4643
<Text style={styles.textLabel}>{translate('twoFactorAuth.whatIsTwoFactorAuth')}</Text>
4744
</View>
4845
</Section>
49-
<ConfirmModal
50-
title={translate('twoFactorAuth.disableTwoFactorAuth')}
51-
onConfirm={() => {
52-
setIsConfirmModalVisible(false);
53-
setStep(CONST.TWO_FACTOR_AUTH_STEPS.DISABLED);
54-
Session.toggleTwoFactorAuth(false);
55-
}}
56-
onCancel={() => setIsConfirmModalVisible(false)}
57-
onModalHide={() => setIsConfirmModalVisible(false)}
58-
isVisible={isConfirmModalVisible}
59-
prompt={translate('twoFactorAuth.disableTwoFactorAuthConfirmation')}
60-
confirmText={translate('common.disable')}
61-
cancelText={translate('common.cancel')}
62-
shouldShowCancelButton
63-
danger
64-
/>
6546
</ScrollView>
6647
</StepWrapper>
6748
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import React, {useRef} from 'react';
2+
import {View} from 'react-native';
3+
import {withOnyx} from 'react-native-onyx';
4+
import Button from '@components/Button';
5+
import FixedFooter from '@components/FixedFooter';
6+
import ScrollView from '@components/ScrollView';
7+
import Text from '@components/Text';
8+
import useLocalize from '@hooks/useLocalize';
9+
import useThemeStyles from '@hooks/useThemeStyles';
10+
import type {BackToParams} from '@libs/Navigation/types';
11+
import StepWrapper from '@pages/settings/Security/TwoFactorAuth/StepWrapper/StepWrapper';
12+
import useTwoFactorAuthContext from '@pages/settings/Security/TwoFactorAuth/TwoFactorAuthContext/useTwoFactorAuth';
13+
import TwoFactorAuthForm from '@pages/settings/Security/TwoFactorAuth/TwoFactorAuthForm';
14+
import type {BaseTwoFactorAuthFormOnyxProps, BaseTwoFactorAuthFormRef} from '@pages/settings/Security/TwoFactorAuth/TwoFactorAuthForm/types';
15+
import CONST from '@src/CONST';
16+
import ONYXKEYS from '@src/ONYXKEYS';
17+
18+
type GetCodeProps = BaseTwoFactorAuthFormOnyxProps & BackToParams;
19+
20+
function GetCode({account}: GetCodeProps) {
21+
const styles = useThemeStyles();
22+
const {translate} = useLocalize();
23+
24+
const formRef = useRef<BaseTwoFactorAuthFormRef>(null);
25+
26+
const {setStep} = useTwoFactorAuthContext();
27+
28+
return (
29+
<StepWrapper
30+
title={translate('twoFactorAuth.disableTwoFactorAuth')}
31+
shouldEnableKeyboardAvoidingView={false}
32+
onBackButtonPress={() => setStep(CONST.TWO_FACTOR_AUTH_STEPS.ENABLED, CONST.ANIMATION_DIRECTION.OUT)}
33+
onEntryTransitionEnd={() => formRef.current && formRef.current.focus()}
34+
>
35+
<ScrollView contentContainerStyle={styles.flexGrow1}>
36+
<View style={[styles.ph5, styles.mt3]}>
37+
<Text>{translate('twoFactorAuth.explainProcessToRemove')}</Text>
38+
</View>
39+
</ScrollView>
40+
<FixedFooter style={[styles.mt2, styles.pt2]}>
41+
<View style={[styles.mh5, styles.mb4]}>
42+
<TwoFactorAuthForm
43+
innerRef={formRef}
44+
validateInsteadOfDisable={false}
45+
/>
46+
</View>
47+
<Button
48+
success
49+
large
50+
text={translate('twoFactorAuth.disable')}
51+
isLoading={account?.isLoading}
52+
onPress={() => {
53+
if (!formRef.current) {
54+
return;
55+
}
56+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
57+
formRef.current.validateAndSubmitForm();
58+
}}
59+
/>
60+
</FixedFooter>
61+
</StepWrapper>
62+
);
63+
}
64+
65+
GetCode.displayName = 'GetCode';
66+
67+
export default withOnyx<GetCodeProps, BaseTwoFactorAuthFormOnyxProps>({
68+
account: {key: ONYXKEYS.ACCOUNT},
69+
user: {
70+
key: ONYXKEYS.USER,
71+
},
72+
})(GetCode);

src/pages/settings/Security/TwoFactorAuth/TwoFactorAuthForm/BaseTwoFactorAuthForm.tsx

+12-3
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,13 @@ import type {BaseTwoFactorAuthFormOnyxProps, BaseTwoFactorAuthFormRef} from './t
1212

1313
type BaseTwoFactorAuthFormProps = BaseTwoFactorAuthFormOnyxProps & {
1414
autoComplete: AutoCompleteVariant;
15+
16+
// Set this to true in order to call the validateTwoFactorAuth action which is used when setting up 2FA for the first time.
17+
// Set this to false in order to disable 2FA when a valid code is entered.
18+
validateInsteadOfDisable?: boolean;
1519
};
1620

17-
function BaseTwoFactorAuthForm({account, autoComplete}: BaseTwoFactorAuthFormProps, ref: ForwardedRef<BaseTwoFactorAuthFormRef>) {
21+
function BaseTwoFactorAuthForm({account, autoComplete, validateInsteadOfDisable}: BaseTwoFactorAuthFormProps, ref: ForwardedRef<BaseTwoFactorAuthFormRef>) {
1822
const {translate} = useLocalize();
1923
const [formError, setFormError] = useState<{twoFactorAuthCode?: string}>({});
2024
const [twoFactorAuthCode, setTwoFactorAuthCode] = useState('');
@@ -54,8 +58,13 @@ function BaseTwoFactorAuthForm({account, autoComplete}: BaseTwoFactorAuthFormPro
5458
}
5559

5660
setFormError({});
57-
Session.validateTwoFactorAuth(twoFactorAuthCode, shouldClearData);
58-
}, [twoFactorAuthCode, shouldClearData, translate]);
61+
62+
if (validateInsteadOfDisable !== false) {
63+
Session.validateTwoFactorAuth(twoFactorAuthCode, shouldClearData);
64+
return;
65+
}
66+
Session.toggleTwoFactorAuth(false, twoFactorAuthCode);
67+
}, [twoFactorAuthCode, validateInsteadOfDisable, translate, shouldClearData]);
5968

6069
useImperativeHandle(ref, () => ({
6170
validateAndSubmitForm() {

src/pages/settings/Security/TwoFactorAuth/TwoFactorAuthForm/index.android.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@ import React from 'react';
22
import BaseTwoFactorAuthForm from './BaseTwoFactorAuthForm';
33
import type {TwoFactorAuthFormProps} from './types';
44

5-
function TwoFactorAuthForm({innerRef}: TwoFactorAuthFormProps) {
5+
function TwoFactorAuthForm({innerRef, validateInsteadOfDisable}: TwoFactorAuthFormProps) {
66
return (
77
<BaseTwoFactorAuthForm
88
ref={innerRef}
99
autoComplete="sms-otp"
10+
validateInsteadOfDisable={validateInsteadOfDisable}
1011
/>
1112
);
1213
}

src/pages/settings/Security/TwoFactorAuth/TwoFactorAuthForm/index.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@ import React from 'react';
22
import BaseTwoFactorAuthForm from './BaseTwoFactorAuthForm';
33
import type {TwoFactorAuthFormProps} from './types';
44

5-
function TwoFactorAuthForm({innerRef}: TwoFactorAuthFormProps) {
5+
function TwoFactorAuthForm({innerRef, validateInsteadOfDisable}: TwoFactorAuthFormProps) {
66
return (
77
<BaseTwoFactorAuthForm
88
ref={innerRef}
99
autoComplete="one-time-code"
10+
validateInsteadOfDisable={validateInsteadOfDisable}
1011
/>
1112
);
1213
}

src/pages/settings/Security/TwoFactorAuth/TwoFactorAuthForm/types.ts

+4
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ type BaseTwoFactorAuthFormRef = {
1414

1515
type TwoFactorAuthFormProps = {
1616
innerRef: ForwardedRef<BaseTwoFactorAuthFormRef>;
17+
18+
// Set this to true in order to call the validateTwoFactorAuth action which is used when setting up 2FA for the first time.
19+
// Set this to false in order to disable 2FA when a valid code is entered.
20+
validateInsteadOfDisable?: boolean;
1721
};
1822

1923
export type {BaseTwoFactorAuthFormOnyxProps, TwoFactorAuthFormProps, BaseTwoFactorAuthFormRef};

src/pages/settings/Security/TwoFactorAuth/TwoFactorAuthSteps.tsx

+3
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type {TwoFactorAuthStep} from '@src/types/onyx/Account';
1313
import CodesStep from './Steps/CodesStep';
1414
import DisabledStep from './Steps/DisabledStep';
1515
import EnabledStep from './Steps/EnabledStep';
16+
import GetCodeStep from './Steps/GetCode';
1617
import SuccessStep from './Steps/SuccessStep';
1718
import VerifyStep from './Steps/VerifyStep';
1819
import TwoFactorAuthContext from './TwoFactorAuthContext';
@@ -62,6 +63,8 @@ function TwoFactorAuthSteps({account}: TwoFactorAuthStepProps) {
6263
return <EnabledStep />;
6364
case CONST.TWO_FACTOR_AUTH_STEPS.DISABLED:
6465
return <DisabledStep />;
66+
case CONST.TWO_FACTOR_AUTH_STEPS.GETCODE:
67+
return <GetCodeStep />;
6568
default:
6669
return <CodesStep backTo={backTo} />;
6770
}

0 commit comments

Comments
 (0)