diff --git a/src/CONST.js b/src/CONST.js
index 619a28cc9f0e..2dba46e87e7a 100755
--- a/src/CONST.js
+++ b/src/CONST.js
@@ -32,6 +32,10 @@ const CONST = {
ANIMATED_TRANSITION: 300,
ANIMATED_TRANSITION_FROM_VALUE: 100,
ANIMATION_IN_TIMING: 100,
+ ANIMATION_DIRECTION: {
+ IN: 'in',
+ OUT: 'out',
+ },
ARROW_HIDE_DELAY: 3000,
API_ATTACHMENT_VALIDATIONS: {
@@ -2524,6 +2528,13 @@ const CONST = {
AFTER_WEEK: 'afterWeek',
CUSTOM: 'custom',
},
+ TWO_FACTOR_AUTH_STEPS: {
+ CODES: 'CODES',
+ VERIFY: 'VERIFY',
+ SUCCESS: 'SUCCESS',
+ ENABLED: 'ENABLED',
+ DISABLED: 'DISABLED',
+ },
TAB: {
RECEIPT_TAB_ID: 'ReceiptTab',
MANUAL: 'manual',
diff --git a/src/ROUTES.js b/src/ROUTES.js
index 2ad3cd7a5cd4..4afafa2a2d1b 100644
--- a/src/ROUTES.js
+++ b/src/ROUTES.js
@@ -58,11 +58,7 @@ export default {
SETTINGS_CONTACT_METHOD_DETAILS: `${SETTINGS_CONTACT_METHODS}/:contactMethod/details`,
getEditContactMethodRoute: (contactMethod) => `${SETTINGS_CONTACT_METHODS}/${encodeURIComponent(contactMethod)}/details`,
SETTINGS_NEW_CONTACT_METHOD: `${SETTINGS_CONTACT_METHODS}/new`,
- SETTINGS_2FA_IS_ENABLED: 'settings/security/two-factor-auth/enabled',
- SETTINGS_2FA_DISABLE: 'settings/security/two-factor-auth/disable',
- SETTINGS_2FA_CODES: 'settings/security/two-factor-auth/codes',
- SETTINGS_2FA_VERIFY: 'settings/security/two-factor-auth/verify',
- SETTINGS_2FA_SUCCESS: 'settings/security/two-factor-auth/success',
+ SETTINGS_2FA: 'settings/security/two-factor-auth',
SETTINGS_STATUS,
SETTINGS_STATUS_SET,
NEW_GROUP: 'new/group',
diff --git a/src/components/AnimatedStep/AnimatedStepContext.js b/src/components/AnimatedStep/AnimatedStepContext.js
new file mode 100644
index 000000000000..30377147fdb8
--- /dev/null
+++ b/src/components/AnimatedStep/AnimatedStepContext.js
@@ -0,0 +1,5 @@
+import {createContext} from 'react';
+
+const AnimatedStepContext = createContext();
+
+export default AnimatedStepContext;
diff --git a/src/components/AnimatedStep/AnimatedStepProvider.js b/src/components/AnimatedStep/AnimatedStepProvider.js
new file mode 100644
index 000000000000..280fbd1a2776
--- /dev/null
+++ b/src/components/AnimatedStep/AnimatedStepProvider.js
@@ -0,0 +1,17 @@
+import React, {useState} from 'react';
+import PropTypes from 'prop-types';
+import AnimatedStepContext from './AnimatedStepContext';
+import CONST from '../../CONST';
+
+const propTypes = {
+ children: PropTypes.node.isRequired,
+};
+
+function AnimatedStepProvider({children}) {
+ const [animationDirection, setAnimationDirection] = useState(CONST.ANIMATION_DIRECTION.IN);
+
+ return {children};
+}
+
+AnimatedStepProvider.propTypes = propTypes;
+export default AnimatedStepProvider;
diff --git a/src/components/AnimatedStep.js b/src/components/AnimatedStep/index.js
similarity index 58%
rename from src/components/AnimatedStep.js
rename to src/components/AnimatedStep/index.js
index dce06cb33760..a8b9b80fcc0e 100644
--- a/src/components/AnimatedStep.js
+++ b/src/components/AnimatedStep/index.js
@@ -1,8 +1,8 @@
import React from 'react';
import PropTypes from 'prop-types';
import * as Animatable from 'react-native-animatable';
-import CONST from '../CONST';
-import styles from '../styles/styles';
+import CONST from '../../CONST';
+import styles from '../../styles/styles';
const propTypes = {
/** Children to wrap in AnimatedStep. */
@@ -14,27 +14,37 @@ const propTypes = {
/** Whether we're animating the step in or out */
direction: PropTypes.oneOf(['in', 'out']),
+
+ /** Callback to fire when the animation ends */
+ onAnimationEnd: PropTypes.func,
};
const defaultProps = {
direction: 'in',
style: [],
+ onAnimationEnd: () => {},
};
-function AnimatedStep(props) {
- function getAnimationStyle(direction) {
- let animationStyle;
-
- if (direction === 'in') {
- animationStyle = styles.makeSlideInTranslation('translateX', CONST.ANIMATED_TRANSITION_FROM_VALUE);
- } else if (direction === 'out') {
- animationStyle = styles.makeSlideInTranslation('translateX', -CONST.ANIMATED_TRANSITION_FROM_VALUE);
- }
- return animationStyle;
+function getAnimationStyle(direction) {
+ let transitionValue;
+
+ if (direction === 'in') {
+ transitionValue = CONST.ANIMATED_TRANSITION_FROM_VALUE;
+ } else if (direction === 'out') {
+ transitionValue = -CONST.ANIMATED_TRANSITION_FROM_VALUE;
}
+ return styles.makeSlideInTranslation('translateX', transitionValue);
+}
+function AnimatedStep(props) {
return (
{
+ if (!props.onAnimationEnd) {
+ return;
+ }
+ props.onAnimationEnd();
+ }}
duration={CONST.ANIMATED_TRANSITION}
animation={getAnimationStyle(props.direction)}
useNativeDriver
diff --git a/src/components/AnimatedStep/useAnimatedStepContext.js b/src/components/AnimatedStep/useAnimatedStepContext.js
new file mode 100644
index 000000000000..e2af9514e20e
--- /dev/null
+++ b/src/components/AnimatedStep/useAnimatedStepContext.js
@@ -0,0 +1,12 @@
+import {useContext} from 'react';
+import AnimatedStepContext from './AnimatedStepContext';
+
+function useAnimatedStepContext() {
+ const context = useContext(AnimatedStepContext);
+ if (!context) {
+ throw new Error('useAnimatedStepContext must be used within an AnimatedStepContextProvider');
+ }
+ return context;
+}
+
+export default useAnimatedStepContext;
diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js
index ab9e2dcd73b2..5cf88c4259db 100644
--- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js
+++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js
@@ -630,38 +630,10 @@ const SettingsModalStackNavigator = createModalStackNavigator([
},
{
getComponent: () => {
- const SettingsTwoFactorAuthIsEnabled = require('../../../pages/settings/Security/TwoFactorAuth/IsEnabledPage').default;
- return SettingsTwoFactorAuthIsEnabled;
+ const SettingsTwoFactorAuth = require('../../../pages/settings/Security/TwoFactorAuth/TwoFactorAuthPage').default;
+ return SettingsTwoFactorAuth;
},
- name: 'Settings_TwoFactorAuthIsEnabled',
- },
- {
- getComponent: () => {
- const SettingsTwoFactorAuthDisable = require('../../../pages/settings/Security/TwoFactorAuth/DisablePage').default;
- return SettingsTwoFactorAuthDisable;
- },
- name: 'Settings_TwoFactorAuthDisable',
- },
- {
- getComponent: () => {
- const SettingsTwoFactorAuthCodes = require('../../../pages/settings/Security/TwoFactorAuth/CodesPage').default;
- return SettingsTwoFactorAuthCodes;
- },
- name: 'Settings_TwoFactorAuthCodes',
- },
- {
- getComponent: () => {
- const SettingsTwoFactorAuthVerify = require('../../../pages/settings/Security/TwoFactorAuth/VerifyPage').default;
- return SettingsTwoFactorAuthVerify;
- },
- name: 'Settings_TwoFactorAuthVerify',
- },
- {
- getComponent: () => {
- const SettingsTwoFactorAuthSuccess = require('../../../pages/settings/Security/TwoFactorAuth/SuccessPage').default;
- return SettingsTwoFactorAuthSuccess;
- },
- name: 'Settings_TwoFactorAuthSuccess',
+ name: 'Settings_TwoFactorAuth',
},
]);
diff --git a/src/libs/Navigation/linkingConfig.js b/src/libs/Navigation/linkingConfig.js
index 5a3f0a9abe71..ad9da4bf4c3c 100644
--- a/src/libs/Navigation/linkingConfig.js
+++ b/src/libs/Navigation/linkingConfig.js
@@ -152,24 +152,8 @@ export default {
path: ROUTES.SETTINGS_PERSONAL_DETAILS_ADDRESS,
exact: true,
},
- Settings_TwoFactorAuthIsEnabled: {
- path: ROUTES.SETTINGS_2FA_IS_ENABLED,
- exact: true,
- },
- Settings_TwoFactorAuthDisable: {
- path: ROUTES.SETTINGS_2FA_DISABLE,
- exact: true,
- },
- Settings_TwoFactorAuthCodes: {
- path: ROUTES.SETTINGS_2FA_CODES,
- exact: true,
- },
- Settings_TwoFactorAuthVerify: {
- path: ROUTES.SETTINGS_2FA_VERIFY,
- exact: true,
- },
- Settings_TwoFactorAuthSuccess: {
- path: ROUTES.SETTINGS_2FA_SUCCESS,
+ Settings_TwoFactorAuth: {
+ path: ROUTES.SETTINGS_2FA,
exact: true,
},
Settings_Share_Code: {
diff --git a/src/libs/actions/TwoFactorAuthActions.js b/src/libs/actions/TwoFactorAuthActions.js
index f778b1f2ceae..3bc64b8ada4c 100644
--- a/src/libs/actions/TwoFactorAuthActions.js
+++ b/src/libs/actions/TwoFactorAuthActions.js
@@ -1,14 +1,26 @@
import Onyx from 'react-native-onyx';
import ONYXKEYS from '../../ONYXKEYS';
+import Navigation from '../Navigation/Navigation';
+import ROUTES from '../../ROUTES';
/**
* Clear 2FA data if the flow is interrupted without finishing
*/
function clearTwoFactorAuthData() {
- Onyx.merge(ONYXKEYS.ACCOUNT, {recoveryCodes: '', twoFactorAuthSecretKey: ''});
+ Onyx.merge(ONYXKEYS.ACCOUNT, {recoveryCodes: '', twoFactorAuthSecretKey: '', twoFactorAuthStep: '', codesAreCopied: false});
}
-export {
- // eslint-disable-next-line import/prefer-default-export
- clearTwoFactorAuthData,
-};
+function setTwoFactorAuthStep(twoFactorAuthStep) {
+ Onyx.merge(ONYXKEYS.ACCOUNT, {twoFactorAuthStep});
+}
+
+function setCodesAreCopied() {
+ Onyx.merge(ONYXKEYS.ACCOUNT, {codesAreCopied: true});
+}
+
+function quitAndNavigateBackToSettings() {
+ clearTwoFactorAuthData();
+ Navigation.goBack(ROUTES.SETTINGS_SECURITY);
+}
+
+export {clearTwoFactorAuthData, setTwoFactorAuthStep, quitAndNavigateBackToSettings, setCodesAreCopied};
diff --git a/src/pages/settings/Security/SecuritySettingsPage.js b/src/pages/settings/Security/SecuritySettingsPage.js
index 65fa265c425a..90b006669527 100644
--- a/src/pages/settings/Security/SecuritySettingsPage.js
+++ b/src/pages/settings/Security/SecuritySettingsPage.js
@@ -13,7 +13,6 @@ import withLocalize, {withLocalizePropTypes} from '../../../components/withLocal
import MenuItem from '../../../components/MenuItem';
import compose from '../../../libs/compose';
import ONYXKEYS from '../../../ONYXKEYS';
-import * as Session from '../../../libs/actions/Session';
const propTypes = {
...withLocalizePropTypes,
@@ -36,14 +35,7 @@ function SecuritySettingsPage(props) {
{
translationKey: 'twoFactorAuth.headerTitle',
icon: Expensicons.Shield,
- action: () => {
- if (props.account.requiresTwoFactorAuth) {
- Navigation.navigate(ROUTES.SETTINGS_2FA_IS_ENABLED);
- } else {
- Session.toggleTwoFactorAuth(true);
- Navigation.navigate(ROUTES.SETTINGS_2FA_CODES);
- }
- },
+ action: () => Navigation.navigate(ROUTES.SETTINGS_2FA),
},
{
translationKey: 'closeAccountPage.closeAccount',
diff --git a/src/pages/settings/Security/TwoFactorAuth/CodesPage.js b/src/pages/settings/Security/TwoFactorAuth/CodesPage.js
deleted file mode 100644
index 29cefdc156a0..000000000000
--- a/src/pages/settings/Security/TwoFactorAuth/CodesPage.js
+++ /dev/null
@@ -1,152 +0,0 @@
-import React, {useEffect, useState} from 'react';
-import {withOnyx} from 'react-native-onyx';
-import {ActivityIndicator, View} from 'react-native';
-import {ScrollView} from 'react-native-gesture-handler';
-import _ from 'underscore';
-import PropTypes from 'prop-types';
-import HeaderWithBackButton from '../../../../components/HeaderWithBackButton';
-import Navigation from '../../../../libs/Navigation/Navigation';
-import ScreenWrapper from '../../../../components/ScreenWrapper';
-import * as Expensicons from '../../../../components/Icon/Expensicons';
-import withLocalize, {withLocalizePropTypes} from '../../../../components/withLocalize';
-import compose from '../../../../libs/compose';
-import ROUTES from '../../../../ROUTES';
-import FullPageOfflineBlockingView from '../../../../components/BlockingViews/FullPageOfflineBlockingView';
-import * as Illustrations from '../../../../components/Icon/Illustrations';
-import withWindowDimensions, {windowDimensionsPropTypes} from '../../../../components/withWindowDimensions';
-import styles from '../../../../styles/styles';
-import FixedFooter from '../../../../components/FixedFooter';
-import Button from '../../../../components/Button';
-import PressableWithDelayToggle from '../../../../components/Pressable/PressableWithDelayToggle';
-import Text from '../../../../components/Text';
-import Section from '../../../../components/Section';
-import ONYXKEYS from '../../../../ONYXKEYS';
-import Clipboard from '../../../../libs/Clipboard';
-import themeColors from '../../../../styles/themes/default';
-import localFileDownload from '../../../../libs/localFileDownload';
-import * as TwoFactorAuthActions from '../../../../libs/actions/TwoFactorAuthActions';
-
-const propTypes = {
- ...withLocalizePropTypes,
- ...windowDimensionsPropTypes,
- account: PropTypes.shape({
- /** User recovery codes for setting up 2-FA */
- recoveryCodes: PropTypes.string,
-
- /** If recovery codes are loading */
- isLoading: PropTypes.bool,
- }),
-};
-
-const defaultProps = {
- account: {
- recoveryCodes: '',
- },
-};
-
-function CodesPage(props) {
- const [isNextButtonDisabled, setIsNextButtonDisabled] = useState(true);
-
- // Here, this eslint rule will make the unmount effect unreadable, possibly confusing with mount
- // eslint-disable-next-line arrow-body-style
- useEffect(() => {
- return () => {
- TwoFactorAuthActions.clearTwoFactorAuthData();
- };
- }, []);
-
- return (
-
- Navigation.goBack(ROUTES.SETTINGS_SECURITY)}
- />
-
-
-
-
- {props.translate('twoFactorAuth.codesLoseAccess')}
-
-
- {props.account.isLoading ? (
-
-
-
- ) : (
- <>
-
- {Boolean(props.account.recoveryCodes) &&
- _.map(props.account.recoveryCodes.split(', '), (code) => (
-
- {code}
-
- ))}
-
-
- {
- Clipboard.setString(props.account.recoveryCodes);
- setIsNextButtonDisabled(false);
- }}
- styles={[styles.button, styles.buttonMedium, styles.twoFactorAuthCodesButton]}
- textStyles={[styles.buttonMediumText]}
- />
- {
- localFileDownload('two-factor-auth-codes', props.account.recoveryCodes);
- setIsNextButtonDisabled(false);
- }}
- inline={false}
- styles={[styles.button, styles.buttonMedium, styles.twoFactorAuthCodesButton]}
- textStyles={[styles.buttonMediumText]}
- />
-
- >
- )}
-
-
-
-
-
-
-
- );
-}
-
-CodesPage.propTypes = propTypes;
-CodesPage.defaultProps = defaultProps;
-
-export default compose(
- withLocalize,
- withWindowDimensions,
- withOnyx({
- account: {key: ONYXKEYS.ACCOUNT},
- }),
-)(CodesPage);
diff --git a/src/pages/settings/Security/TwoFactorAuth/DisablePage.js b/src/pages/settings/Security/TwoFactorAuth/DisablePage.js
deleted file mode 100644
index 1369c4d13e65..000000000000
--- a/src/pages/settings/Security/TwoFactorAuth/DisablePage.js
+++ /dev/null
@@ -1,57 +0,0 @@
-import React, {useEffect} from 'react';
-import HeaderWithBackButton from '../../../../components/HeaderWithBackButton';
-import Navigation from '../../../../libs/Navigation/Navigation';
-import ScreenWrapper from '../../../../components/ScreenWrapper';
-import withLocalize, {withLocalizePropTypes} from '../../../../components/withLocalize';
-import ROUTES from '../../../../ROUTES';
-import FullPageOfflineBlockingView from '../../../../components/BlockingViews/FullPageOfflineBlockingView';
-import * as Illustrations from '../../../../components/Icon/Illustrations';
-import styles from '../../../../styles/styles';
-import BlockingView from '../../../../components/BlockingViews/BlockingView';
-import FixedFooter from '../../../../components/FixedFooter';
-import Button from '../../../../components/Button';
-import * as Session from '../../../../libs/actions/Session';
-import variables from '../../../../styles/variables';
-
-const propTypes = {
- ...withLocalizePropTypes,
-};
-
-const defaultProps = {};
-
-function DisablePage(props) {
- useEffect(() => {
- Session.toggleTwoFactorAuth(false);
- }, []);
-
- return (
-
- Navigation.goBack(ROUTES.SETTINGS_SECURITY)}
- />
-
-
-
-
-
-
-
- );
-}
-
-DisablePage.propTypes = propTypes;
-DisablePage.defaultProps = defaultProps;
-
-export default withLocalize(DisablePage);
diff --git a/src/pages/settings/Security/TwoFactorAuth/IsEnabledPage.js b/src/pages/settings/Security/TwoFactorAuth/IsEnabledPage.js
deleted file mode 100644
index 4d49edac1fe4..000000000000
--- a/src/pages/settings/Security/TwoFactorAuth/IsEnabledPage.js
+++ /dev/null
@@ -1,74 +0,0 @@
-import React, {useState} from 'react';
-import {View, ScrollView} from 'react-native';
-import Text from '../../../../components/Text';
-import HeaderWithBackButton from '../../../../components/HeaderWithBackButton';
-import Navigation from '../../../../libs/Navigation/Navigation';
-import ScreenWrapper from '../../../../components/ScreenWrapper';
-import withLocalize, {withLocalizePropTypes} from '../../../../components/withLocalize';
-import ROUTES from '../../../../ROUTES';
-import Section from '../../../../components/Section';
-import * as Illustrations from '../../../../components/Icon/Illustrations';
-import * as Expensicons from '../../../../components/Icon/Expensicons';
-import themeColors from '../../../../styles/themes/default';
-import styles from '../../../../styles/styles';
-import ConfirmModal from '../../../../components/ConfirmModal';
-import FullPageOfflineBlockingView from '../../../../components/BlockingViews/FullPageOfflineBlockingView';
-
-const defaultProps = {};
-
-function IsEnabledPage(props) {
- const [isConfirmModalVisible, setIsConfirmModalVisible] = useState(false);
-
- return (
-
- Navigation.goBack(ROUTES.SETTINGS_SECURITY)}
- />
-
-
- {
- setIsConfirmModalVisible(true);
- },
- icon: Expensicons.Close,
- iconFill: themeColors.danger,
- wrapperStyle: [styles.cardMenuItem],
- },
- ]}
- containerStyles={[styles.twoFactorAuthSection]}
- >
-
- {props.translate('twoFactorAuth.whatIsTwoFactorAuth')}
-
-
- {
- setIsConfirmModalVisible(false);
- Navigation.navigate(ROUTES.SETTINGS_2FA_DISABLE);
- }}
- onCancel={() => setIsConfirmModalVisible(false)}
- onModalHide={() => setIsConfirmModalVisible(false)}
- isVisible={isConfirmModalVisible}
- prompt={props.translate('twoFactorAuth.disableTwoFactorAuthConfirmation')}
- confirmText={props.translate('twoFactorAuth.disable')}
- cancelText={props.translate('common.cancel')}
- shouldShowCancelButton
- danger
- />
-
-
-
- );
-}
-
-IsEnabledPage.propTypes = withLocalizePropTypes;
-IsEnabledPage.defaultProps = defaultProps;
-
-export default withLocalize(IsEnabledPage);
diff --git a/src/pages/settings/Security/TwoFactorAuth/StepWrapper/StepWrapper.js b/src/pages/settings/Security/TwoFactorAuth/StepWrapper/StepWrapper.js
new file mode 100644
index 000000000000..16f9489d87b0
--- /dev/null
+++ b/src/pages/settings/Security/TwoFactorAuth/StepWrapper/StepWrapper.js
@@ -0,0 +1,47 @@
+import React from 'react';
+import HeaderWithBackButton from '../../../../../components/HeaderWithBackButton';
+import ScreenWrapper from '../../../../../components/ScreenWrapper';
+import FullPageOfflineBlockingView from '../../../../../components/BlockingViews/FullPageOfflineBlockingView';
+import * as TwoFactorAuthActions from '../../../../../libs/actions/TwoFactorAuthActions';
+import StepWrapperPropTypes from './StepWrapperPropTypes';
+import AnimatedStep from '../../../../../components/AnimatedStep';
+import styles from '../../../../../styles/styles';
+import useAnimatedStepContext from '../../../../../components/AnimatedStep/useAnimatedStepContext';
+
+function StepWrapper({
+ title = '',
+ stepCounter = null,
+ onBackButtonPress = TwoFactorAuthActions.quitAndNavigateBackToSettings,
+ children = null,
+ shouldEnableKeyboardAvoidingView = true,
+ onEntryTransitionEnd,
+}) {
+ const shouldShowStepCounter = Boolean(stepCounter);
+
+ const {animationDirection} = useAnimatedStepContext();
+
+ return (
+
+
+
+ {children}
+
+
+ );
+}
+
+StepWrapper.propTypes = StepWrapperPropTypes;
+
+export default StepWrapper;
diff --git a/src/pages/settings/Security/TwoFactorAuth/StepWrapper/StepWrapperPropTypes.js b/src/pages/settings/Security/TwoFactorAuth/StepWrapper/StepWrapperPropTypes.js
new file mode 100644
index 000000000000..3c06cc7bca52
--- /dev/null
+++ b/src/pages/settings/Security/TwoFactorAuth/StepWrapper/StepWrapperPropTypes.js
@@ -0,0 +1,28 @@
+import PropTypes from 'prop-types';
+
+export default {
+ /** Title of the Header */
+ title: PropTypes.string,
+
+ /** Data to display a step counter in the header */
+ stepCounter: PropTypes.shape({
+ /** Current step */
+ step: PropTypes.number,
+ /** Total number of steps */
+ total: PropTypes.number,
+ /** Text to display next to the step counter */
+ text: PropTypes.string,
+ }),
+
+ /** Method to trigger when pressing back button of the header */
+ onBackButtonPress: PropTypes.func,
+
+ /** Called when navigated Screen's transition is finished. It does not fire when user exits the page. */
+ onEntryTransitionEnd: PropTypes.func,
+
+ /** Children components */
+ children: PropTypes.node,
+
+ /** Flag to indicate if the keyboard avoiding view should be enabled */
+ shouldEnableKeyboardAvoidingView: PropTypes.bool,
+};
diff --git a/src/pages/settings/Security/TwoFactorAuth/Steps/CodesStep.js b/src/pages/settings/Security/TwoFactorAuth/Steps/CodesStep.js
new file mode 100644
index 000000000000..52d7a9806f69
--- /dev/null
+++ b/src/pages/settings/Security/TwoFactorAuth/Steps/CodesStep.js
@@ -0,0 +1,125 @@
+import React, {useEffect} from 'react';
+import {withOnyx} from 'react-native-onyx';
+import {ActivityIndicator, View} from 'react-native';
+import {ScrollView} from 'react-native-gesture-handler';
+import _ from 'underscore';
+import * as Expensicons from '../../../../../components/Icon/Expensicons';
+import * as Illustrations from '../../../../../components/Icon/Illustrations';
+import styles from '../../../../../styles/styles';
+import FixedFooter from '../../../../../components/FixedFooter';
+import Button from '../../../../../components/Button';
+import PressableWithDelayToggle from '../../../../../components/Pressable/PressableWithDelayToggle';
+import Text from '../../../../../components/Text';
+import Section from '../../../../../components/Section';
+import ONYXKEYS from '../../../../../ONYXKEYS';
+import Clipboard from '../../../../../libs/Clipboard';
+import themeColors from '../../../../../styles/themes/default';
+import localFileDownload from '../../../../../libs/localFileDownload';
+import * as Session from '../../../../../libs/actions/Session';
+import CONST from '../../../../../CONST';
+import useTwoFactorAuthContext from '../TwoFactorAuthContext/useTwoFactorAuth';
+import useLocalize from '../../../../../hooks/useLocalize';
+import useWindowDimensions from '../../../../../hooks/useWindowDimensions';
+import StepWrapper from '../StepWrapper/StepWrapper';
+import {defaultAccount, TwoFactorAuthPropTypes} from '../TwoFactorAuthPropTypes';
+import * as TwoFactorAuthActions from '../../../../../libs/actions/TwoFactorAuthActions';
+
+function CodesStep({account = defaultAccount}) {
+ const {translate} = useLocalize();
+ const {isExtraSmallScreenWidth, isSmallScreenWidth} = useWindowDimensions();
+
+ const {setStep} = useTwoFactorAuthContext();
+
+ useEffect(() => {
+ if (account.recoveryCodes) {
+ return;
+ }
+ Session.toggleTwoFactorAuth(true);
+ }, [account.recoveryCodes]);
+
+ return (
+
+
+
+
+ {translate('twoFactorAuth.codesLoseAccess')}
+
+
+ {account.isLoading ? (
+
+
+
+ ) : (
+ <>
+
+ {Boolean(account.recoveryCodes) &&
+ _.map(account.recoveryCodes.split(', '), (code) => (
+
+ {code}
+
+ ))}
+
+
+ {
+ Clipboard.setString(account.recoveryCodes);
+ TwoFactorAuthActions.setCodesAreCopied();
+ }}
+ styles={[styles.button, styles.buttonMedium, styles.twoFactorAuthCodesButton]}
+ textStyles={[styles.buttonMediumText]}
+ />
+ {
+ localFileDownload('two-factor-auth-codes', account.recoveryCodes);
+ TwoFactorAuthActions.setCodesAreCopied();
+ }}
+ inline={false}
+ styles={[styles.button, styles.buttonMedium, styles.twoFactorAuthCodesButton]}
+ textStyles={[styles.buttonMediumText]}
+ />
+
+ >
+ )}
+
+
+
+
+
+
+ );
+}
+
+CodesStep.propTypes = TwoFactorAuthPropTypes;
+
+// eslint-disable-next-line rulesdir/onyx-props-must-have-default
+export default withOnyx({
+ account: {key: ONYXKEYS.ACCOUNT},
+})(CodesStep);
diff --git a/src/pages/settings/Security/TwoFactorAuth/Steps/DisabledStep.js b/src/pages/settings/Security/TwoFactorAuth/Steps/DisabledStep.js
new file mode 100644
index 000000000000..2f3b87e69a6e
--- /dev/null
+++ b/src/pages/settings/Security/TwoFactorAuth/Steps/DisabledStep.js
@@ -0,0 +1,35 @@
+import React from 'react';
+import * as Illustrations from '../../../../../components/Icon/Illustrations';
+import styles from '../../../../../styles/styles';
+import BlockingView from '../../../../../components/BlockingViews/BlockingView';
+import FixedFooter from '../../../../../components/FixedFooter';
+import Button from '../../../../../components/Button';
+import variables from '../../../../../styles/variables';
+import StepWrapper from '../StepWrapper/StepWrapper';
+import useLocalize from '../../../../../hooks/useLocalize';
+import * as TwoFactorAuthActions from '../../../../../libs/actions/TwoFactorAuthActions';
+
+function DisabledStep() {
+ const {translate} = useLocalize();
+
+ return (
+
+
+
+
+
+
+ );
+}
+
+export default DisabledStep;
diff --git a/src/pages/settings/Security/TwoFactorAuth/Steps/EnabledStep.js b/src/pages/settings/Security/TwoFactorAuth/Steps/EnabledStep.js
new file mode 100644
index 000000000000..584d6195bbe6
--- /dev/null
+++ b/src/pages/settings/Security/TwoFactorAuth/Steps/EnabledStep.js
@@ -0,0 +1,66 @@
+import React, {useState} from 'react';
+import {Text, View, ScrollView} from 'react-native';
+import Section from '../../../../../components/Section';
+import * as Illustrations from '../../../../../components/Icon/Illustrations';
+import * as Expensicons from '../../../../../components/Icon/Expensicons';
+import themeColors from '../../../../../styles/themes/default';
+import styles from '../../../../../styles/styles';
+import ConfirmModal from '../../../../../components/ConfirmModal';
+import * as Session from '../../../../../libs/actions/Session';
+import StepWrapper from '../StepWrapper/StepWrapper';
+import CONST from '../../../../../CONST';
+import useLocalize from '../../../../../hooks/useLocalize';
+import useTwoFactorAuthContext from '../TwoFactorAuthContext/useTwoFactorAuth';
+
+function EnabledStep() {
+ const [isConfirmModalVisible, setIsConfirmModalVisible] = useState(false);
+
+ const {setStep} = useTwoFactorAuthContext();
+
+ const {translate} = useLocalize();
+
+ return (
+
+
+ {
+ setIsConfirmModalVisible(true);
+ },
+ icon: Expensicons.Close,
+ iconFill: themeColors.danger,
+ wrapperStyle: [styles.cardMenuItem],
+ },
+ ]}
+ containerStyles={[styles.twoFactorAuthSection]}
+ >
+
+ {translate('twoFactorAuth.whatIsTwoFactorAuth')}
+
+
+ {
+ setIsConfirmModalVisible(false);
+ setStep(CONST.TWO_FACTOR_AUTH_STEPS.DISABLED);
+ Session.toggleTwoFactorAuth(false);
+ }}
+ onCancel={() => setIsConfirmModalVisible(false)}
+ onModalHide={() => setIsConfirmModalVisible(false)}
+ isVisible={isConfirmModalVisible}
+ prompt={translate('twoFactorAuth.disableTwoFactorAuthConfirmation')}
+ confirmText={translate('twoFactorAuth.disable')}
+ cancelText={translate('common.cancel')}
+ shouldShowCancelButton
+ danger
+ />
+
+
+ );
+}
+
+export default EnabledStep;
diff --git a/src/pages/settings/Security/TwoFactorAuth/Steps/SuccessStep.js b/src/pages/settings/Security/TwoFactorAuth/Steps/SuccessStep.js
new file mode 100644
index 000000000000..308efecf3415
--- /dev/null
+++ b/src/pages/settings/Security/TwoFactorAuth/Steps/SuccessStep.js
@@ -0,0 +1,38 @@
+import React from 'react';
+import ConfirmationPage from '../../../../../components/ConfirmationPage';
+import * as TwoFactorAuthActions from '../../../../../libs/actions/TwoFactorAuthActions';
+import * as LottieAnimations from '../../../../../components/LottieAnimations';
+import CONST from '../../../../../CONST';
+import StepWrapper from '../StepWrapper/StepWrapper';
+import useTwoFactorAuthContext from '../TwoFactorAuthContext/useTwoFactorAuth';
+import useLocalize from '../../../../../hooks/useLocalize';
+
+function SuccessStep() {
+ const {setStep} = useTwoFactorAuthContext();
+
+ const {translate} = useLocalize();
+
+ return (
+
+ {
+ TwoFactorAuthActions.clearTwoFactorAuthData();
+ setStep(CONST.TWO_FACTOR_AUTH_STEPS.ENABLED);
+ }}
+ />
+
+ );
+}
+
+export default SuccessStep;
diff --git a/src/pages/settings/Security/TwoFactorAuth/Steps/VerifyStep.js b/src/pages/settings/Security/TwoFactorAuth/Steps/VerifyStep.js
new file mode 100644
index 000000000000..8c3d9689cfcd
--- /dev/null
+++ b/src/pages/settings/Security/TwoFactorAuth/Steps/VerifyStep.js
@@ -0,0 +1,136 @@
+import React, {useEffect} from 'react';
+import {withOnyx} from 'react-native-onyx';
+import {ScrollView, View} from 'react-native';
+import * as Session from '../../../../../libs/actions/Session';
+import styles from '../../../../../styles/styles';
+import Button from '../../../../../components/Button';
+import Text from '../../../../../components/Text';
+import ONYXKEYS from '../../../../../ONYXKEYS';
+import TextLink from '../../../../../components/TextLink';
+import Clipboard from '../../../../../libs/Clipboard';
+import FixedFooter from '../../../../../components/FixedFooter';
+import * as Expensicons from '../../../../../components/Icon/Expensicons';
+import PressableWithDelayToggle from '../../../../../components/Pressable/PressableWithDelayToggle';
+import TwoFactorAuthForm from '../TwoFactorAuthForm';
+import QRCode from '../../../../../components/QRCode';
+import expensifyLogo from '../../../../../../assets/images/expensify-logo-round-transparent.png';
+import CONST from '../../../../../CONST';
+import StepWrapper from '../StepWrapper/StepWrapper';
+import useTwoFactorAuthContext from '../TwoFactorAuthContext/useTwoFactorAuth';
+import useLocalize from '../../../../../hooks/useLocalize';
+import {defaultAccount, TwoFactorAuthPropTypes} from '../TwoFactorAuthPropTypes';
+
+const TROUBLESHOOTING_LINK = 'https://community.expensify.com/discussion/7736/faq-troubleshooting-two-factor-authentication-issues/p1?new=1';
+
+function VerifyStep({account = defaultAccount}) {
+ const {translate} = useLocalize();
+
+ const formRef = React.useRef(null);
+
+ const {setStep} = useTwoFactorAuthContext();
+
+ useEffect(() => {
+ Session.clearAccountMessages();
+ }, []);
+
+ useEffect(() => {
+ if (!account.requiresTwoFactorAuth) {
+ return;
+ }
+ setStep(CONST.TWO_FACTOR_AUTH_STEPS.SUCCESS);
+ }, [account.requiresTwoFactorAuth, setStep]);
+
+ /**
+ * Splits the two-factor auth secret key in 4 chunks
+ *
+ * @param {String} secret
+ * @returns {string}
+ */
+ function splitSecretInChunks(secret) {
+ if (secret.length !== 16) {
+ return secret;
+ }
+
+ return `${secret.slice(0, 4)} ${secret.slice(4, 8)} ${secret.slice(8, 12)} ${secret.slice(12, secret.length)}`;
+ }
+
+ /**
+ * Builds the URL string to generate the QRCode, using the otpauth:// protocol,
+ * so it can be detected by authenticator apps
+ *
+ * @returns {string}
+ */
+ function buildAuthenticatorUrl() {
+ return `otpauth://totp/Expensify:${account.primaryLogin}?secret=${account.twoFactorAuthSecretKey}&issuer=Expensify`;
+ }
+
+ return (
+ setStep(CONST.TWO_FACTOR_AUTH_STEPS.CODES, CONST.ANIMATION_DIRECTION.OUT)}
+ onEntryTransitionEnd={() => formRef.current && formRef.current.focus()}
+ >
+
+
+
+ {translate('twoFactorAuth.scanCode')}
+ {translate('twoFactorAuth.authenticatorApp')}.
+
+
+
+
+ {translate('twoFactorAuth.addKey')}
+
+ {Boolean(account.twoFactorAuthSecretKey) && {splitSecretInChunks(account.twoFactorAuthSecretKey)}}
+ Clipboard.setString(account.twoFactorAuthSecretKey)}
+ styles={[styles.button, styles.buttonMedium, styles.twoFactorAuthCopyCodeButton]}
+ textStyles={[styles.buttonMediumText]}
+ />
+
+ {translate('twoFactorAuth.enterCode')}
+
+
+
+
+
+
+
+
+ );
+}
+
+VerifyStep.propTypes = TwoFactorAuthPropTypes;
+
+// eslint-disable-next-line rulesdir/onyx-props-must-have-default
+export default withOnyx({
+ account: {key: ONYXKEYS.ACCOUNT},
+})(VerifyStep);
diff --git a/src/pages/settings/Security/TwoFactorAuth/SuccessPage.js b/src/pages/settings/Security/TwoFactorAuth/SuccessPage.js
deleted file mode 100644
index 3febc0e98c38..000000000000
--- a/src/pages/settings/Security/TwoFactorAuth/SuccessPage.js
+++ /dev/null
@@ -1,41 +0,0 @@
-import React from 'react';
-import HeaderWithBackButton from '../../../../components/HeaderWithBackButton';
-import Navigation from '../../../../libs/Navigation/Navigation';
-import ScreenWrapper from '../../../../components/ScreenWrapper';
-import withLocalize, {withLocalizePropTypes} from '../../../../components/withLocalize';
-import ROUTES from '../../../../ROUTES';
-import FullPageOfflineBlockingView from '../../../../components/BlockingViews/FullPageOfflineBlockingView';
-import * as LottieAnimations from '../../../../components/LottieAnimations';
-import ConfirmationPage from '../../../../components/ConfirmationPage';
-
-const defaultProps = {};
-
-function SuccessPage(props) {
- return (
-
- Navigation.goBack(ROUTES.SETTINGS_SECURITY)}
- />
-
- Navigation.navigate(ROUTES.SETTINGS_2FA_IS_ENABLED)}
- />
-
-
- );
-}
-
-SuccessPage.propTypes = withLocalizePropTypes;
-SuccessPage.defaultProps = defaultProps;
-
-export default withLocalize(SuccessPage);
diff --git a/src/pages/settings/Security/TwoFactorAuth/TwoFactorAuthContext/index.js b/src/pages/settings/Security/TwoFactorAuth/TwoFactorAuthContext/index.js
new file mode 100644
index 000000000000..9a2287c775f3
--- /dev/null
+++ b/src/pages/settings/Security/TwoFactorAuth/TwoFactorAuthContext/index.js
@@ -0,0 +1,4 @@
+import {createContext} from 'react';
+
+const TwoFactorAuthContext = createContext();
+export default TwoFactorAuthContext;
diff --git a/src/pages/settings/Security/TwoFactorAuth/TwoFactorAuthContext/useTwoFactorAuth.js b/src/pages/settings/Security/TwoFactorAuth/TwoFactorAuthContext/useTwoFactorAuth.js
new file mode 100644
index 000000000000..c0e45a21b050
--- /dev/null
+++ b/src/pages/settings/Security/TwoFactorAuth/TwoFactorAuthContext/useTwoFactorAuth.js
@@ -0,0 +1,6 @@
+import {useContext} from 'react';
+import TwoFactorAuthContext from './index';
+
+export default function useTwoFactorAuthContext() {
+ return useContext(TwoFactorAuthContext);
+}
diff --git a/src/pages/settings/Security/TwoFactorAuth/TwoFactorAuthPage.js b/src/pages/settings/Security/TwoFactorAuth/TwoFactorAuthPage.js
new file mode 100644
index 000000000000..99aeb4b11f89
--- /dev/null
+++ b/src/pages/settings/Security/TwoFactorAuth/TwoFactorAuthPage.js
@@ -0,0 +1,13 @@
+import React from 'react';
+import AnimatedStepProvider from '../../../../components/AnimatedStep/AnimatedStepProvider';
+import TwoFactorAuthSteps from './TwoFactorAuthSteps';
+
+function TwoFactorAuthPage() {
+ return (
+
+
+
+ );
+}
+
+export default TwoFactorAuthPage;
diff --git a/src/pages/settings/Security/TwoFactorAuth/TwoFactorAuthPropTypes.js b/src/pages/settings/Security/TwoFactorAuth/TwoFactorAuthPropTypes.js
new file mode 100644
index 000000000000..a505ca51f1e3
--- /dev/null
+++ b/src/pages/settings/Security/TwoFactorAuth/TwoFactorAuthPropTypes.js
@@ -0,0 +1,27 @@
+import PropTypes from 'prop-types';
+
+const TwoFactorAuthPropTypes = {
+ account: PropTypes.shape({
+ /** Whether this account has 2FA enabled or not */
+ requiresTwoFactorAuth: PropTypes.bool,
+
+ /** Secret key to enable 2FA within the authenticator app */
+ twoFactorAuthSecretKey: PropTypes.string,
+
+ /** User primary login to attach to the authenticator QRCode */
+ primaryLogin: PropTypes.string,
+
+ /** User is submitting the authentication code */
+ isLoading: PropTypes.bool,
+
+ /** Server-side errors in the submitted authentication code */
+ errors: PropTypes.objectOf(PropTypes.string),
+ }),
+};
+
+const defaultAccount = {
+ requiresTwoFactorAuth: false,
+ twoFactorAuthStep: '',
+};
+
+export {TwoFactorAuthPropTypes, defaultAccount};
diff --git a/src/pages/settings/Security/TwoFactorAuth/TwoFactorAuthSteps.js b/src/pages/settings/Security/TwoFactorAuth/TwoFactorAuthSteps.js
new file mode 100644
index 000000000000..e0094267742b
--- /dev/null
+++ b/src/pages/settings/Security/TwoFactorAuth/TwoFactorAuthSteps.js
@@ -0,0 +1,78 @@
+import React, {useCallback, useEffect, useState} from 'react';
+import {withOnyx} from 'react-native-onyx';
+import CodesStep from './Steps/CodesStep';
+import DisabledStep from './Steps/DisabledStep';
+import EnabledStep from './Steps/EnabledStep';
+import VerifyStep from './Steps/VerifyStep';
+import SuccessStep from './Steps/SuccessStep';
+import ONYXKEYS from '../../../../ONYXKEYS';
+import CONST from '../../../../CONST';
+import * as TwoFactorAuthActions from '../../../../libs/actions/TwoFactorAuthActions';
+import TwoFactorAuthContext from './TwoFactorAuthContext';
+import {defaultAccount, TwoFactorAuthPropTypes} from './TwoFactorAuthPropTypes';
+import useAnimatedStepContext from '../../../../components/AnimatedStep/useAnimatedStepContext';
+
+function TwoFactorAuthSteps({account = defaultAccount}) {
+ const [currentStep, setCurrentStep] = useState(CONST.TWO_FACTOR_AUTH_STEPS.CODES);
+
+ const {setAnimationDirection} = useAnimatedStepContext();
+
+ useEffect(() => () => TwoFactorAuthActions.clearTwoFactorAuthData(), []);
+
+ useEffect(() => {
+ if (account.twoFactorAuthStep) {
+ setCurrentStep(account.twoFactorAuthStep);
+ return;
+ }
+ if (account.requiresTwoFactorAuth) {
+ setCurrentStep(CONST.TWO_FACTOR_AUTH_STEPS.ENABLED);
+ } else {
+ setCurrentStep(CONST.TWO_FACTOR_AUTH_STEPS.CODES);
+ }
+ // we don't want to trigger the hook every time the step changes, only when the requiresTwoFactorAuth changes
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [account.requiresTwoFactorAuth]);
+
+ const handleSetStep = useCallback(
+ (step, animationDirection = CONST.ANIMATION_DIRECTION.IN) => {
+ setAnimationDirection(animationDirection);
+ TwoFactorAuthActions.setTwoFactorAuthStep(step);
+ setCurrentStep(step);
+ },
+ [setAnimationDirection],
+ );
+
+ const renderStep = () => {
+ switch (currentStep) {
+ case CONST.TWO_FACTOR_AUTH_STEPS.CODES:
+ return ;
+ case CONST.TWO_FACTOR_AUTH_STEPS.VERIFY:
+ return ;
+ case CONST.TWO_FACTOR_AUTH_STEPS.SUCCESS:
+ return ;
+ case CONST.TWO_FACTOR_AUTH_STEPS.ENABLED:
+ return ;
+ case CONST.TWO_FACTOR_AUTH_STEPS.DISABLED:
+ return ;
+ default:
+ return ;
+ }
+ };
+
+ return (
+
+ {renderStep()}
+
+ );
+}
+
+TwoFactorAuthSteps.propTypes = TwoFactorAuthPropTypes;
+
+// eslint-disable-next-line rulesdir/onyx-props-must-have-default
+export default withOnyx({
+ account: {key: ONYXKEYS.ACCOUNT},
+})(TwoFactorAuthSteps);
diff --git a/src/pages/settings/Security/TwoFactorAuth/VerifyPage.js b/src/pages/settings/Security/TwoFactorAuth/VerifyPage.js
deleted file mode 100644
index e67f0469db62..000000000000
--- a/src/pages/settings/Security/TwoFactorAuth/VerifyPage.js
+++ /dev/null
@@ -1,175 +0,0 @@
-import React, {useEffect} from 'react';
-import {withOnyx} from 'react-native-onyx';
-import {ScrollView, View} from 'react-native';
-import PropTypes from 'prop-types';
-import HeaderWithBackButton from '../../../../components/HeaderWithBackButton';
-import Navigation from '../../../../libs/Navigation/Navigation';
-import ScreenWrapper from '../../../../components/ScreenWrapper';
-import withLocalize, {withLocalizePropTypes} from '../../../../components/withLocalize';
-import compose from '../../../../libs/compose';
-import * as Session from '../../../../libs/actions/Session';
-import ROUTES from '../../../../ROUTES';
-import FullPageOfflineBlockingView from '../../../../components/BlockingViews/FullPageOfflineBlockingView';
-import styles from '../../../../styles/styles';
-import Button from '../../../../components/Button';
-import Text from '../../../../components/Text';
-import ONYXKEYS from '../../../../ONYXKEYS';
-import TextLink from '../../../../components/TextLink';
-import Clipboard from '../../../../libs/Clipboard';
-import FixedFooter from '../../../../components/FixedFooter';
-import * as Expensicons from '../../../../components/Icon/Expensicons';
-import PressableWithDelayToggle from '../../../../components/Pressable/PressableWithDelayToggle';
-import TwoFactorAuthForm from './TwoFactorAuthForm';
-import QRCode from '../../../../components/QRCode';
-import expensifyLogo from '../../../../../assets/images/expensify-logo-round-transparent.png';
-import CONST from '../../../../CONST';
-
-const propTypes = {
- ...withLocalizePropTypes,
- account: PropTypes.shape({
- /** Whether this account has 2FA enabled or not */
- requiresTwoFactorAuth: PropTypes.bool,
-
- /** Secret key to enable 2FA within the authenticator app */
- twoFactorAuthSecretKey: PropTypes.string,
-
- /** User primary login to attach to the authenticator QRCode */
- primaryLogin: PropTypes.string,
-
- /** User is submitting the authentication code */
- isLoading: PropTypes.bool,
-
- /** Server-side errors in the submitted authentication code */
- errors: PropTypes.objectOf(PropTypes.string),
- }),
-};
-
-const defaultProps = {
- account: {
- requiresTwoFactorAuth: false,
- twoFactorAuthSecretKey: '',
- primaryLogin: '',
- isLoading: false,
- errors: {},
- },
-};
-
-function VerifyPage(props) {
- const formRef = React.useRef(null);
-
- useEffect(() => {
- Session.clearAccountMessages();
- }, []);
-
- useEffect(() => {
- if (!props.account.requiresTwoFactorAuth) {
- return;
- }
- Navigation.navigate(ROUTES.SETTINGS_2FA_SUCCESS);
- }, [props.account.requiresTwoFactorAuth]);
-
- /**
- * Splits the two-factor auth secret key in 4 chunks
- *
- * @param {String} secret
- * @returns {string}
- */
- function splitSecretInChunks(secret) {
- if (secret.length !== 16) {
- return secret;
- }
-
- return `${secret.slice(0, 4)} ${secret.slice(4, 8)} ${secret.slice(8, 12)} ${secret.slice(12, secret.length)}`;
- }
-
- /**
- * Builds the URL string to generate the QRCode, using the otpauth:// protocol,
- * so it can be detected by authenticator apps
- *
- * @returns {string}
- */
- function buildAuthenticatorUrl() {
- return `otpauth://totp/Expensify:${props.account.primaryLogin}?secret=${props.account.twoFactorAuthSecretKey}&issuer=Expensify`;
- }
-
- return (
- formRef.current && formRef.current.focus()}
- >
- Navigation.goBack(ROUTES.SETTINGS_2FA_CODES)}
- />
-
-
-
-
- {props.translate('twoFactorAuth.scanCode')}
-
- {' '}
- {props.translate('twoFactorAuth.authenticatorApp')}
-
- .
-
-
-
-
- {props.translate('twoFactorAuth.addKey')}
-
- {Boolean(props.account.twoFactorAuthSecretKey) && {splitSecretInChunks(props.account.twoFactorAuthSecretKey)}}
- Clipboard.setString(props.account.twoFactorAuthSecretKey)}
- styles={[styles.button, styles.buttonMedium, styles.twoFactorAuthCopyCodeButton]}
- textStyles={[styles.buttonMediumText]}
- />
-
- {props.translate('twoFactorAuth.enterCode')}
-
-
-
-
-
-
-
-
-
- );
-}
-
-VerifyPage.propTypes = propTypes;
-VerifyPage.defaultProps = defaultProps;
-
-export default compose(
- withLocalize,
- withOnyx({
- account: {key: ONYXKEYS.ACCOUNT},
- }),
-)(VerifyPage);