diff --git a/assets/images/expensify-logo-round-dark.png b/assets/images/expensify-logo-round-dark.png new file mode 100644 index 000000000000..106d28274fd3 Binary files /dev/null and b/assets/images/expensify-logo-round-dark.png differ diff --git a/src/ROUTES.js b/src/ROUTES.js index ec4730289e06..272b0f9b6c70 100644 --- a/src/ROUTES.js +++ b/src/ROUTES.js @@ -55,6 +55,11 @@ 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', NEW_GROUP: 'new/group', NEW_CHAT: 'new/chat', NEW_TASK, diff --git a/src/components/HeaderWithCloseButton.js b/src/components/HeaderWithCloseButton.js index e1c87c0c4728..78f9d9a04d85 100755 --- a/src/components/HeaderWithCloseButton.js +++ b/src/components/HeaderWithCloseButton.js @@ -77,6 +77,7 @@ const propTypes = { stepCounter: PropTypes.shape({ step: PropTypes.number, total: PropTypes.number, + text: PropTypes.string, }), /** Whether we should show an avatar */ diff --git a/src/components/Pressable/PressableWithFeedback.js b/src/components/Pressable/PressableWithFeedback.js index 0f4c07c63f64..9e475b7169cf 100644 --- a/src/components/Pressable/PressableWithFeedback.js +++ b/src/components/Pressable/PressableWithFeedback.js @@ -46,10 +46,9 @@ const PressableWithFeedback = forwardRef((props, ref) => { setDisabled(props.disabled); return; } - onPress - .finally(() => { - setDisabled(props.disabled); - }); + onPress.finally(() => { + setDisabled(props.disabled); + }); }); }} > diff --git a/src/components/QRCode/index.js b/src/components/QRCode/index.js new file mode 100644 index 000000000000..41686f7c8502 --- /dev/null +++ b/src/components/QRCode/index.js @@ -0,0 +1,63 @@ +import React from 'react'; +import QRCodeLibrary from 'react-native-qrcode-svg'; +import PropTypes from 'prop-types'; +import defaultTheme from '../../styles/themes/default'; + +const propTypes = { + /** + * The QR code URL + */ + url: PropTypes.string.isRequired, + /** + * The logo which will be displayed in the middle of the QR code. + * Follows `ImageSourcePropType` from react-native. + */ + logo: PropTypes.oneOfType([PropTypes.shape({uri: PropTypes.string}), PropTypes.number]), + /** + * The QRCode size + */ + size: PropTypes.number, + /** + * The QRCode color + */ + color: PropTypes.string, + /** + * The QRCode background color + */ + backgroundColor: PropTypes.string, + /** + * Function to retrieve the internal component ref and be able to call it's + * methods + */ + getRef: PropTypes.func, +}; + +const defaultProps = { + logo: undefined, + size: 120, + color: defaultTheme.text, + backgroundColor: defaultTheme.highlightBG, + getRef: undefined, +}; + +function QRCode(props) { + return ( + + ); +} + +QRCode.displayName = 'QRCode'; +QRCode.propTypes = propTypes; +QRCode.defaultProps = defaultProps; + +export default QRCode; diff --git a/src/components/QRShare/index.js b/src/components/QRShare/index.js index 66ffaea692f3..d6999952a4d6 100644 --- a/src/components/QRShare/index.js +++ b/src/components/QRShare/index.js @@ -1,5 +1,4 @@ import React, {Component} from 'react'; -import QrCode from 'react-native-qrcode-svg'; import {View} from 'react-native'; import withLocalize, {withLocalizePropTypes} from '../withLocalize'; import defaultTheme from '../../styles/themes/default'; @@ -10,6 +9,7 @@ import compose from '../../libs/compose'; import variables from '../../styles/variables'; import ExpensifyWordmark from '../../../assets/images/expensify-wordmark.svg'; import {qrSharePropTypes, qrShareDefaultProps} from './propTypes'; +import QRCode from '../QRCode'; const propTypes = { ...qrSharePropTypes, @@ -61,16 +61,11 @@ class QRShare extends Component { /> - (this.svg = svg)} - logoBackgroundColor="transparent" - logoSize={this.state.qrCodeSize * 0.3} - logoBorderRadius={this.state.qrCodeSize} + url={this.props.url} + logo={this.props.logo} size={this.state.qrCodeSize} - backgroundColor={defaultTheme.highlightBG} - color={defaultTheme.text} /> { @@ -45,7 +50,7 @@ const Section = (props) => { {props.title} - + {Boolean(props.icon) && ( `Step ${step} of ${total}`, + stepCounter: ({step, total, text}) => { + let result = `Step ${step}`; + + if (total) { + result = `${result} of ${total}`; + } + + if (text) { + result = `${result}: ${text}`; + } + return result; + }, bankAccount: { accountNumber: 'Account number', routingNumber: 'Routing number', @@ -742,7 +782,6 @@ export default { 'In order to finish setting up your bank account, you must validate your account. Please check your email to validate your account, and return here to finish up!', hasPhoneLoginError: 'To add a verified bank account please ensure your primary login is a valid email and try again. You can add your phone number as a secondary login.', hasBeenThrottledError: 'There was an error adding your bank account. Please wait a few minutes and try again.', - buttonConfirm: 'Got it', error: { noBankAccountAvailable: 'Sorry, no bank account is available', noBankAccountSelected: 'Please choose an account', diff --git a/src/languages/es.js b/src/languages/es.js index 43ccdfcdf2db..427e2ff00f05 100644 --- a/src/languages/es.js +++ b/src/languages/es.js @@ -8,6 +8,7 @@ export default { yes: 'Sí', no: 'No', ok: 'OK', + buttonConfirm: 'Ok, entendido', attachment: 'Archivo adjunto', to: 'A', optional: 'Opcional', @@ -24,6 +25,7 @@ export default { zoom: 'Zoom', password: 'Contraseña', magicCode: 'Código mágico', + twoFactorCode: 'Autenticación de dos factores', workspaces: 'Espacios de trabajo', profile: 'Perfil', payments: 'Pagos', @@ -495,10 +497,40 @@ export default { newPassword: 'Su contraseña debe tener al menos 8 caracteres, 1 letra mayúscula, 1 letra minúscula y 1 número.', }, }, + twoFactorAuth: { + headerTitle: 'Autenticación de dos factores', + twoFactorAuthEnabled: 'Autenticación de dos factores habilitada', + whatIsTwoFactorAuth: + '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.', + disableTwoFactorAuth: 'Deshabilitar la autenticación de dos factores', + disableTwoFactorAuthConfirmation: 'La autenticación de dos factores mantiene tu cuenta más segura. ¿Estás seguro de que quieres desactivarla?', + disabled: 'La autenticación de dos factores ahora está deshabilitada', + noAuthenticatorApp: 'Ya no necesitarás una aplicación de autenticación para iniciar sesión en Expensify.', + stepCodes: 'Códigos de recuperación', + keepCodesSafe: '¡Guarda los códigos de recuperación en un lugar seguro!', + codesLoseAccess: + 'Si pierdes el acceso a tu aplicación de autenticación y no tienes estos códigos, perderás el acceso a tu cuenta. \n\nNota: Configurar la autenticación de dos factores cerrará la sesión de todas las demás sesiones activas.', + stepVerify: 'Verificar', + scanCode: 'Escanea el código QR usando tu', + authenticatorApp: 'aplicación de autenticación', + addKey: 'O agrega esta clave secreta a su aplicación de autenticación:', + enterCode: 'Luego ingresa el código de seis dígitos generado por su aplicación de autenticación.', + stepSuccess: 'Finalizado', + enabled: '¡La autenticación de dos factores ahora está habilitada!', + congrats: 'Felicidades, ahora tienes esa seguridad adicional.', + copyCodes: 'Copiar códigos', + copy: 'Copiar', + disable: 'Deshabilitar', + }, + twoFactorAuthForm: { + error: { + pleaseFillTwoFactorAuth: 'Por favor, introduce tu código 2 factores', + incorrect2fa: 'Código de autenticación de dos factores incorrecto. Por favor, inténtalo de nuevo', + }, + }, passwordConfirmationScreen: { passwordUpdated: 'Contraseña actualizada!', allSet: 'Todo está listo. Guarda tu contraseña en un lugar seguro.', - gotIt: 'Ok, entendido', }, addPayPalMePage: { enterYourUsernameToGetPaidViaPayPal: 'Recibe pagos vía PayPal.', @@ -626,7 +658,6 @@ export default { validateCodeForm: { magicCodeNotReceived: '¿No recibiste un código mágico?', enterAuthenticatorCode: 'Por favor, introduce el código de autenticador', - twoFactorCode: 'Autenticación de 2 factores', requiredWhen2FAEnabled: 'Obligatorio cuando A2F está habilitado', codeSent: '¡Código mágico enviado!', error: { @@ -641,12 +672,11 @@ export default { pleaseFillTwoFactorAuth: 'Por favor, introduce tu código 2 factores', enterYourTwoFactorAuthenticationCodeToContinue: 'Introduce el código de autenticación de dos factores para continuar', forgot: '¿Has olvidado la contraseña?', - twoFactorCode: 'Autenticación de 2 factores', requiredWhen2FAEnabled: 'Obligatorio cuando A2F está habilitado', error: { incorrectPassword: 'Contraseña incorrecta. Por favor, inténtalo de nuevo.', incorrectLoginOrPassword: 'Usuario o contraseña incorrectos. Por favor, inténtalo de nuevo', - incorrect2fa: 'Código de autenticación de 2 factores incorrecto. Por favor, inténtalo de nuevo', + incorrect2fa: 'Código de autenticación de dos factores incorrecto. Por favor, inténtalo de nuevo', twoFactorAuthenticationEnabled: 'Tienes autenticación de 2 factores activada en esta cuenta. Por favor, conéctate usando tu email o número de teléfono', invalidLoginOrPassword: 'Usuario o clave incorrectos. Por favor, inténtalo de nuevo o restablece la contraseña', unableToResetPassword: @@ -725,7 +755,18 @@ export default { setPasswordLinkInvalid: 'El enlace para configurar tu contraseña ha expirado. Te hemos enviado un nuevo enlace a tu correo.', validateAccount: 'Verificar cuenta', }, - stepCounter: ({step, total}) => `Paso ${step} de ${total}`, + stepCounter: ({step, total, text}) => { + let result = `Paso ${step}`; + + if (total) { + result = `${result} de ${total}`; + } + + if (text) { + result = `${result}: ${text}`; + } + return result; + }, bankAccount: { accountNumber: 'Número de cuenta', routingNumber: 'Número de ruta', @@ -743,7 +784,6 @@ export default { hasPhoneLoginError: 'Para agregar una cuenta bancaria verificada, asegúrate de que tu nombre de usuario principal sea un correo electrónico válido y vuelve a intentarlo. Puedes agregar tu número de teléfono como nombre de usuario secundario.', hasBeenThrottledError: 'Se produjo un error al intentar agregar tu cuenta bancaria. Por favor, espera unos minutos e inténtalo de nuevo.', - buttonConfirm: 'OK', error: { noBankAccountAvailable: 'Lo sentimos, no hay ninguna cuenta bancaria disponible', noBankAccountSelected: 'Por favor, elige una cuenta bancaria', diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js index 7b33335fb86d..20332e12916d 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js @@ -598,6 +598,41 @@ const SettingsModalStackNavigator = createModalStackNavigator([ }, name: 'GetAssistance', }, + { + getComponent: () => { + const SettingsTwoFactorAuthIsEnabled = require('../../../pages/settings/Security/TwoFactorAuth/IsEnabledPage').default; + return SettingsTwoFactorAuthIsEnabled; + }, + 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', + }, ]); const EnablePaymentsStackNavigator = createModalStackNavigator([ diff --git a/src/libs/Navigation/linkingConfig.js b/src/libs/Navigation/linkingConfig.js index 1a7c7398caf0..6c1da26c6058 100644 --- a/src/libs/Navigation/linkingConfig.js +++ b/src/libs/Navigation/linkingConfig.js @@ -145,6 +145,26 @@ 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, + exact: true, + }, Workspace_Initial: { path: ROUTES.WORKSPACE_INITIAL, }, diff --git a/src/libs/actions/Session/index.js b/src/libs/actions/Session/index.js index 9f8a70a0cd47..348cafc13d8f 100644 --- a/src/libs/actions/Session/index.js +++ b/src/libs/actions/Session/index.js @@ -772,7 +772,91 @@ function unlinkLogin(accountID, validateCode) { }, ]; - API.write('UnlinkLogin', {accountID, validateCode}, {optimisticData, successData, failureData}); + API.write( + 'UnlinkLogin', + { + accountID, + validateCode, + }, + { + optimisticData, + successData, + failureData, + }, + ); +} + +/** + * Toggles two-factor authentication based on the `enable` parameter + * + * @param {Boolean} enable + */ +function toggleTwoFactorAuth(enable) { + const optimisticData = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.ACCOUNT, + value: { + isLoading: true, + }, + }, + ]; + + const successData = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.ACCOUNT, + value: { + isLoading: false, + }, + }, + ]; + + const failureData = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.ACCOUNT, + value: { + isLoading: false, + }, + }, + ]; + + API.write(enable ? 'EnableTwoFactorAuth' : 'DisableTwoFactorAuth', {}, {optimisticData, successData, failureData}); +} + +function validateTwoFactorAuth(twoFactorAuthCode) { + const optimisticData = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.ACCOUNT, + value: { + isLoading: true, + }, + }, + ]; + + const successData = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.ACCOUNT, + value: { + isLoading: false, + }, + }, + ]; + + const failureData = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.ACCOUNT, + value: { + isLoading: false, + }, + }, + ]; + + API.write('TwoFactorAuth_Validate', {twoFactorAuthCode}, {optimisticData, successData, failureData}); } export { @@ -799,4 +883,6 @@ export { reauthenticatePusher, invalidateCredentials, invalidateAuthToken, + toggleTwoFactorAuth, + validateTwoFactorAuth, }; diff --git a/src/libs/actions/TwoFactorAuthActions.js b/src/libs/actions/TwoFactorAuthActions.js new file mode 100644 index 000000000000..f778b1f2ceae --- /dev/null +++ b/src/libs/actions/TwoFactorAuthActions.js @@ -0,0 +1,14 @@ +import Onyx from 'react-native-onyx'; +import ONYXKEYS from '../../ONYXKEYS'; + +/** + * Clear 2FA data if the flow is interrupted without finishing + */ +function clearTwoFactorAuthData() { + Onyx.merge(ONYXKEYS.ACCOUNT, {recoveryCodes: '', twoFactorAuthSecretKey: ''}); +} + +export { + // eslint-disable-next-line import/prefer-default-export + clearTwoFactorAuthData, +}; diff --git a/src/libs/localFileDownload/index.android.js b/src/libs/localFileDownload/index.android.js new file mode 100644 index 000000000000..acdc9c89e6a4 --- /dev/null +++ b/src/libs/localFileDownload/index.android.js @@ -0,0 +1,36 @@ +import RNFetchBlob from 'react-native-blob-util'; +import * as FileUtils from '../fileDownload/FileUtils'; + +/** + * Writes a local file to the app's internal directory with the given fileName + * and textContent, so we're able to copy it to the Android public download dir. + * After the file is copied, it is removed from the internal dir. + * + * @param {String} fileName + * @param {String} textContent + */ +export default function localFileDownload(fileName, textContent) { + const dir = RNFetchBlob.fs.dirs.DocumentDir; + const path = `${dir}/${fileName}.txt`; + + RNFetchBlob.fs.writeFile(path, textContent, 'utf8').then(() => { + RNFetchBlob.MediaCollection.copyToMediaStore( + { + name: fileName, + parentFolder: '', // subdirectory in the Media Store, empty goes to 'Downloads' + mimeType: 'text/plain', + }, + 'Download', + path, + ) + .then(() => { + FileUtils.showSuccessAlert(); + }) + .catch(() => { + FileUtils.showGeneralErrorAlert(); + }) + .finally(() => { + RNFetchBlob.fs.unlink(path); + }); + }); +} diff --git a/src/libs/localFileDownload/index.ios.js b/src/libs/localFileDownload/index.ios.js new file mode 100644 index 000000000000..7113db49ac8b --- /dev/null +++ b/src/libs/localFileDownload/index.ios.js @@ -0,0 +1,21 @@ +import {Share} from 'react-native'; +import RNFetchBlob from 'react-native-blob-util'; + +/** + * Writes a local file to the app's internal directory with the given fileName + * and textContent, so we're able to share it using iOS' share API. + * After the file is shared, it is removed from the internal dir. + * + * @param {String} fileName + * @param {String} textContent + */ +export default function localFileDownload(fileName, textContent) { + const dir = RNFetchBlob.fs.dirs.DocumentDir; + const path = `${dir}/${fileName}.txt`; + + RNFetchBlob.fs.writeFile(path, textContent, 'utf8').then(() => { + Share.share({url: path, title: fileName}).finally(() => { + RNFetchBlob.fs.unlink(path); + }); + }); +} diff --git a/src/libs/localFileDownload/index.js b/src/libs/localFileDownload/index.js new file mode 100644 index 000000000000..f52d2fa0115e --- /dev/null +++ b/src/libs/localFileDownload/index.js @@ -0,0 +1,16 @@ +/** + * Creates a Blob with the given fileName and textContent, then dynamically + * creates a temporary anchor, just to programmatically click it, so the file + * is downloaded by the browser. + * + * @param {String} fileName + * @param {String} textContent + */ +export default function localFileDownload(fileName, textContent) { + const blob = new Blob([textContent], {type: 'text/plain'}); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.download = `${fileName}.txt`; + link.href = url; + link.click(); +} diff --git a/src/pages/settings/PasswordPage.js b/src/pages/settings/PasswordPage.js index a2d740dc97b9..0d5826a2e00d 100755 --- a/src/pages/settings/PasswordPage.js +++ b/src/pages/settings/PasswordPage.js @@ -158,7 +158,7 @@ class PasswordPage extends Component { heading={this.props.translate('passwordConfirmationScreen.passwordUpdated')} shouldShowButton onButtonPress={Navigation.goBack} - buttonText={this.props.translate('passwordConfirmationScreen.gotIt')} + buttonText={this.props.translate('common.buttonConfirm')} description={this.props.translate('passwordConfirmationScreen.allSet')} /> ) : ( diff --git a/src/pages/settings/Security/SecuritySettingsPage.js b/src/pages/settings/Security/SecuritySettingsPage.js index d6d4b0104bee..ae7dac591ac5 100644 --- a/src/pages/settings/Security/SecuritySettingsPage.js +++ b/src/pages/settings/Security/SecuritySettingsPage.js @@ -1,6 +1,8 @@ import _ from 'underscore'; import React from 'react'; import {View, ScrollView} from 'react-native'; +import {withOnyx} from 'react-native-onyx'; +import PropTypes from 'prop-types'; import HeaderWithCloseButton from '../../../components/HeaderWithCloseButton'; import Navigation from '../../../libs/Navigation/Navigation'; import ROUTES from '../../../ROUTES'; @@ -9,13 +11,40 @@ import * as Expensicons from '../../../components/Icon/Expensicons'; import ScreenWrapper from '../../../components/ScreenWrapper'; import withLocalize, {withLocalizePropTypes} from '../../../components/withLocalize'; import MenuItem from '../../../components/MenuItem'; +import compose from '../../../libs/compose'; +import ONYXKEYS from '../../../ONYXKEYS'; +import * as Session from '../../../libs/actions/Session'; const propTypes = { ...withLocalizePropTypes, + + /* Onyx Props */ + + /** Holds information about the users account that is logging in */ + account: PropTypes.shape({ + /** Whether this account has 2FA enabled or not */ + requiresTwoFactorAuth: PropTypes.bool, + }), }; -const SecuritySettingsPage = (props) => { +const defaultProps = { + account: {}, +}; + +function SecuritySettingsPage(props) { const menuItems = [ + { + 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); + } + }, + }, { translationKey: 'passwordPage.changePassword', icon: Expensicons.Key, @@ -56,9 +85,15 @@ const SecuritySettingsPage = (props) => { ); -}; +} SecuritySettingsPage.propTypes = propTypes; +SecuritySettingsPage.defaultProps = defaultProps; SecuritySettingsPage.displayName = 'SettingSecurityPage'; -export default withLocalize(SecuritySettingsPage); +export default compose( + withLocalize, + withOnyx({ + account: {key: ONYXKEYS.ACCOUNT}, + }), +)(SecuritySettingsPage); diff --git a/src/pages/settings/Security/TwoFactorAuth/CodesPage.js b/src/pages/settings/Security/TwoFactorAuth/CodesPage.js new file mode 100644 index 000000000000..915d2534fa0d --- /dev/null +++ b/src/pages/settings/Security/TwoFactorAuth/CodesPage.js @@ -0,0 +1,139 @@ +import React, {useEffect, useState} from 'react'; +import {withOnyx} from 'react-native-onyx'; +import {ActivityIndicator, View} from 'react-native'; +import _ from 'underscore'; +import PropTypes from 'prop-types'; +import HeaderWithCloseButton from '../../../../components/HeaderWithCloseButton'; +import Navigation from '../../../../libs/Navigation/Navigation'; +import ScreenWrapper from '../../../../components/ScreenWrapper'; +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 styles from '../../../../styles/styles'; +import FixedFooter from '../../../../components/FixedFooter'; +import Button from '../../../../components/Button'; +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, + 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.navigate(ROUTES.SETTINGS_SECURITY)} + onCloseButtonPress={() => Navigation.dismissModal(true)} + /> + +
+ + {props.translate('twoFactorAuth.codesLoseAccess')} + + + {props.account.isLoading ? ( + + + + ) : ( + <> + + {Boolean(props.account.recoveryCodes) && + _.map(props.account.recoveryCodes.split(', '), (code) => ( + + {code} + + ))} + + +
+ +