diff --git a/src/CONST.ts b/src/CONST.ts
index 08c6a2509207..6dcf165c4ec7 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -757,6 +757,9 @@ const CONST = {
// 6 numeric digits
VALIDATE_CODE_REGEX_STRING: /^\d{6}$/,
+ // 8 alphanumeric characters
+ RECOVERY_CODE_REGEX_STRING: /^[a-zA-Z0-9]{8}$/,
+
// The server has a WAF (Web Application Firewall) which will strip out HTML/XML tags using this regex pattern.
// It's copied here so that the same regex pattern can be used in form validations to be consistent with the server.
VALIDATE_FOR_HTML_TAG_REGEX: /<([^>\s]+)(?:[^>]*?)>/g,
@@ -806,6 +809,8 @@ const CONST = {
MAGIC_CODE_LENGTH: 6,
MAGIC_CODE_EMPTY_CHAR: ' ',
+ RECOVERY_CODE_LENGTH: 8,
+
KEYBOARD_TYPE: {
PHONE_PAD: 'phone-pad',
NUMBER_PAD: 'number-pad',
diff --git a/src/languages/en.ts b/src/languages/en.ts
index b0f5801a30c3..210d82b28a7d 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -740,6 +740,15 @@ export default {
copy: 'Copy',
disable: 'Disable',
},
+ recoveryCodeForm: {
+ error: {
+ pleaseFillRecoveryCode: 'Please enter your recovery code',
+ incorrectRecoveryCode: 'Incorrect recovery code. Please try again.',
+ },
+ useRecoveryCode: 'Use recovery code',
+ recoveryCode: 'Recovery code',
+ use2fa: 'Use two-factor authentication code',
+ },
twoFactorAuthForm: {
error: {
pleaseFillTwoFactorAuth: 'Please enter your two-factor authentication code',
@@ -893,6 +902,7 @@ export default {
validateCodeForm: {
magicCodeNotReceived: "Didn't receive a magic code?",
enterAuthenticatorCode: 'Please enter your authenticator code',
+ enterRecoveryCode: 'Please enter your recovery code',
requiredWhen2FAEnabled: 'Required when 2FA is enabled',
requestNewCode: 'Request a new code in ',
requestNewCodeAfterErrorOccurred: 'Request a new code',
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 1984624e8683..0048cfbb9e23 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -735,6 +735,15 @@ export default {
copy: 'Copiar',
disable: 'Deshabilitar',
},
+ recoveryCodeForm: {
+ error: {
+ pleaseFillRecoveryCode: 'Por favor, introduce tu código de recuperación',
+ incorrectRecoveryCode: 'Código de recuperación incorrecto. Por favor, inténtalo de nuevo',
+ },
+ useRecoveryCode: 'Usar código de recuperación',
+ recoveryCode: 'Código de recuperación',
+ use2fa: 'Usar autenticación de dos factores',
+ },
twoFactorAuthForm: {
error: {
pleaseFillTwoFactorAuth: 'Por favor, introduce tu código de autenticación de dos factores',
@@ -889,6 +898,7 @@ export default {
validateCodeForm: {
magicCodeNotReceived: '¿No recibiste un código mágico?',
enterAuthenticatorCode: 'Por favor, introduce el código de autenticador',
+ enterRecoveryCode: 'Por favor, introduce tu código de recuperación',
requiredWhen2FAEnabled: 'Obligatorio cuando A2F está habilitado',
requestNewCode: 'Pedir un código nuevo en ',
requestNewCodeAfterErrorOccurred: 'Solicitar un nuevo código',
diff --git a/src/libs/ValidationUtils.js b/src/libs/ValidationUtils.js
index 7aded82fb0a9..a85a623bd3ec 100644
--- a/src/libs/ValidationUtils.js
+++ b/src/libs/ValidationUtils.js
@@ -314,6 +314,10 @@ function isValidValidateCode(validateCode) {
return validateCode.match(CONST.VALIDATE_CODE_REGEX_STRING);
}
+function isValidRecoveryCode(recoveryCode) {
+ return recoveryCode.match(CONST.RECOVERY_CODE_REGEX_STRING);
+}
+
/**
* @param {String} code
* @returns {Boolean}
@@ -484,4 +488,5 @@ export {
doesContainReservedWord,
isNumeric,
isValidAccountRoute,
+ isValidRecoveryCode,
};
diff --git a/src/pages/signin/SignInPage.js b/src/pages/signin/SignInPage.js
index 3e27e6cd253a..290c528672d2 100644
--- a/src/pages/signin/SignInPage.js
+++ b/src/pages/signin/SignInPage.js
@@ -1,4 +1,4 @@
-import React, {useEffect, useRef} from 'react';
+import React, {useEffect, useRef, useState} from 'react';
import PropTypes from 'prop-types';
import _ from 'underscore';
import {withOnyx} from 'react-native-onyx';
@@ -87,6 +87,9 @@ function SignInPage({credentials, account, isInModal}) {
const shouldShowSmallScreen = isSmallScreenWidth || isInModal;
const safeAreaInsets = useSafeAreaInsets();
const signInPageLayoutRef = useRef();
+ /** This state is needed to keep track of if user is using recovery code instead of 2fa code,
+ * and we need it here since welcome text(`welcomeText`) also depends on it */
+ const [isUsingRecoveryCode, setIsUsingRecoveryCode] = useState(false);
useEffect(() => Performance.measureTTI(), []);
useEffect(() => {
@@ -114,7 +117,7 @@ function SignInPage({credentials, account, isInModal}) {
if (account.requiresTwoFactorAuth) {
// We will only know this after a user signs in successfully, without their 2FA code
welcomeHeader = isSmallScreenWidth ? '' : translate('welcomeText.welcomeBack');
- welcomeText = translate('validateCodeForm.enterAuthenticatorCode');
+ welcomeText = isUsingRecoveryCode ? translate('validateCodeForm.enterRecoveryCode') : translate('validateCodeForm.enterAuthenticatorCode');
} else {
const userLogin = Str.removeSMSDomain(credentials.login || '');
@@ -162,7 +165,12 @@ function SignInPage({credentials, account, isInModal}) {
blurOnSubmit={account.validated === false}
scrollPageToTop={signInPageLayoutRef.current && signInPageLayoutRef.current.scrollPageToTop}
/>
- {shouldShowValidateCodeForm && }
+ {shouldShowValidateCodeForm && (
+
+ )}
{shouldShowUnlinkLoginForm && }
{shouldShowEmailDeliveryFailurePage && }
diff --git a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js
index 51cb287f9564..7815976609c5 100755
--- a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js
+++ b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js
@@ -26,6 +26,7 @@ import Terms from '../Terms';
import PressableWithFeedback from '../../../components/Pressable/PressableWithFeedback';
import usePrevious from '../../../hooks/usePrevious';
import * as StyleUtils from '../../../styles/StyleUtils';
+import TextInput from '../../../components/TextInput';
const propTypes = {
/* Onyx Props */
@@ -60,6 +61,12 @@ const propTypes = {
/** Specifies autocomplete hints for the system, so it can provide autofill */
autoComplete: PropTypes.oneOf(['sms-otp', 'one-time-code']).isRequired,
+ /** Determines if user is switched to using recovery code instead of 2fa code */
+ isUsingRecoveryCode: PropTypes.bool.isRequired,
+
+ /** Function to change `isUsingRecoveryCode` state when user toggles between 2fa code and recovery code */
+ setIsUsingRecoveryCode: PropTypes.func.isRequired,
+
...withLocalizePropTypes,
};
@@ -77,6 +84,7 @@ function BaseValidateCodeForm(props) {
const [validateCode, setValidateCode] = useState(props.credentials.validateCode || '');
const [twoFactorAuthCode, setTwoFactorAuthCode] = useState('');
const [timeRemaining, setTimeRemaining] = useState(30);
+ const [recoveryCode, setRecoveryCode] = useState('');
const prevRequiresTwoFactorAuth = usePrevious(props.account.requiresTwoFactorAuth);
const prevValidateCode = usePrevious(props.credentials.validateCode);
@@ -149,7 +157,17 @@ function BaseValidateCodeForm(props) {
* @param {String} key
*/
const onTextInput = (text, key) => {
- const setInput = key === 'validateCode' ? setValidateCode : setTwoFactorAuthCode;
+ let setInput;
+ if (key === 'validateCode') {
+ setInput = setValidateCode;
+ }
+ if (key === 'twoFactorAuthCode') {
+ setInput = setTwoFactorAuthCode;
+ }
+ if (key === 'recoveryCode') {
+ setInput = setRecoveryCode;
+ }
+
setInput(text);
setFormError((prevError) => ({...prevError, [key]: ''}));
@@ -174,6 +192,8 @@ function BaseValidateCodeForm(props) {
setTwoFactorAuthCode('');
setFormError({});
setValidateCode('');
+ props.setIsUsingRecoveryCode(false);
+ setRecoveryCode('');
};
/**
@@ -184,11 +204,30 @@ function BaseValidateCodeForm(props) {
Session.clearSignInData();
};
+ /**
+ * Switches between 2fa and recovery code, clears inputs and errors
+ */
+ const switchBetween2faAndRecoveryCode = () => {
+ props.setIsUsingRecoveryCode(!props.isUsingRecoveryCode);
+
+ setRecoveryCode('');
+ setTwoFactorAuthCode('');
+
+ setFormError((prevError) => ({...prevError, recoveryCode: '', twoFactorAuthCode: ''}));
+
+ if (props.account.errors) {
+ Session.clearAccountMessages();
+ }
+ };
+
useEffect(() => {
if (!isLoadingResendValidationForm) {
return;
}
clearLocalSignInData();
+ // `clearLocalSignInData` is not required as a dependency, and adding it
+ // overcomplicates things requiring clearLocalSignInData function to use useCallback
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [isLoadingResendValidationForm]);
/**
@@ -203,13 +242,27 @@ function BaseValidateCodeForm(props) {
if (input2FARef.current) {
input2FARef.current.blur();
}
- if (!twoFactorAuthCode.trim()) {
- setFormError({twoFactorAuthCode: 'validateCodeForm.error.pleaseFillTwoFactorAuth'});
- return;
- }
- if (!ValidationUtils.isValidTwoFactorCode(twoFactorAuthCode)) {
- setFormError({twoFactorAuthCode: 'passwordForm.error.incorrect2fa'});
- return;
+ /**
+ * User could be using either recovery code or 2fa code
+ */
+ if (!props.isUsingRecoveryCode) {
+ if (!twoFactorAuthCode.trim()) {
+ setFormError({twoFactorAuthCode: 'validateCodeForm.error.pleaseFillTwoFactorAuth'});
+ return;
+ }
+ if (!ValidationUtils.isValidTwoFactorCode(twoFactorAuthCode)) {
+ setFormError({twoFactorAuthCode: 'passwordForm.error.incorrect2fa'});
+ return;
+ }
+ } else {
+ if (!recoveryCode.trim()) {
+ setFormError({recoveryCode: 'recoveryCodeForm.error.pleaseFillRecoveryCode'});
+ return;
+ }
+ if (!ValidationUtils.isValidRecoveryCode(recoveryCode)) {
+ setFormError({recoveryCode: 'recoveryCodeForm.error.incorrectRecoveryCode'});
+ return;
+ }
}
} else {
if (inputValidateCodeRef.current) {
@@ -226,33 +279,61 @@ function BaseValidateCodeForm(props) {
}
setFormError({});
+ const recoveryCodeOr2faCode = props.isUsingRecoveryCode ? recoveryCode : twoFactorAuthCode;
+
const accountID = lodashGet(props.credentials, 'accountID');
if (accountID) {
- Session.signInWithValidateCode(accountID, validateCode, props.preferredLocale, twoFactorAuthCode);
+ Session.signInWithValidateCode(accountID, validateCode, props.preferredLocale, recoveryCodeOr2faCode);
} else {
- Session.signIn(validateCode, twoFactorAuthCode, props.preferredLocale);
+ Session.signIn(validateCode, recoveryCodeOr2faCode, props.preferredLocale);
}
- }, [props.account, props.credentials, props.preferredLocale, twoFactorAuthCode, validateCode]);
+ }, [props.account, props.credentials, props.preferredLocale, twoFactorAuthCode, validateCode, props.isUsingRecoveryCode, recoveryCode]);
return (
<>
{/* At this point, if we know the account requires 2FA we already successfully authenticated */}
{props.account.requiresTwoFactorAuth ? (
- onTextInput(text, 'twoFactorAuthCode')}
- onFulfill={validateAndSubmitForm}
- maxLength={CONST.TFA_CODE_LENGTH}
- errorText={formError.twoFactorAuthCode ? props.translate(formError.twoFactorAuthCode) : ''}
- hasError={hasError}
- autoFocus
- />
+ {props.isUsingRecoveryCode ? (
+ onTextInput(text, 'recoveryCode')}
+ maxLength={CONST.RECOVERY_CODE_LENGTH}
+ label={props.translate('recoveryCodeForm.recoveryCode')}
+ errorText={formError.recoveryCode ? props.translate(formError.recoveryCode) : ''}
+ hasError={hasError}
+ autoFocus
+ />
+ ) : (
+ onTextInput(text, 'twoFactorAuthCode')}
+ onFulfill={validateAndSubmitForm}
+ maxLength={CONST.TFA_CODE_LENGTH}
+ errorText={formError.twoFactorAuthCode ? props.translate(formError.twoFactorAuthCode) : ''}
+ hasError={hasError}
+ autoFocus
+ />
+ )}
{hasError && }
+
+ {props.isUsingRecoveryCode ? props.translate('recoveryCodeForm.use2fa') : props.translate('recoveryCodeForm.useRecoveryCode')}
+
) : (
diff --git a/src/pages/signin/ValidateCodeForm/index.android.js b/src/pages/signin/ValidateCodeForm/index.android.js
index 7ff81357725d..1e888d24bc60 100644
--- a/src/pages/signin/ValidateCodeForm/index.android.js
+++ b/src/pages/signin/ValidateCodeForm/index.android.js
@@ -1,11 +1,24 @@
import React from 'react';
+import PropTypes from 'prop-types';
import BaseValidateCodeForm from './BaseValidateCodeForm';
const defaultProps = {};
-const propTypes = {};
-function ValidateCodeForm() {
- return ;
+const propTypes = {
+ /** Determines if user is switched to using recovery code instead of 2fa code */
+ isUsingRecoveryCode: PropTypes.bool.isRequired,
+
+ /** Function to change `isUsingRecoveryCode` state when user toggles between 2fa code and recovery code */
+ setIsUsingRecoveryCode: PropTypes.func.isRequired,
+};
+function ValidateCodeForm(props) {
+ return (
+
+ );
}
ValidateCodeForm.displayName = 'ValidateCodeForm';
diff --git a/src/pages/signin/ValidateCodeForm/index.js b/src/pages/signin/ValidateCodeForm/index.js
index 6b01c7d4dec2..540b6a3e3ed6 100644
--- a/src/pages/signin/ValidateCodeForm/index.js
+++ b/src/pages/signin/ValidateCodeForm/index.js
@@ -1,11 +1,24 @@
import React from 'react';
+import PropTypes from 'prop-types';
import BaseValidateCodeForm from './BaseValidateCodeForm';
const defaultProps = {};
-const propTypes = {};
-function ValidateCodeForm() {
- return ;
+const propTypes = {
+ /** Determines if user is switched to using recovery code instead of 2fa code */
+ isUsingRecoveryCode: PropTypes.bool.isRequired,
+
+ /** Function to change `isUsingRecoveryCode` state when user toggles between 2fa code and recovery code */
+ setIsUsingRecoveryCode: PropTypes.func.isRequired,
+};
+function ValidateCodeForm(props) {
+ return (
+
+ );
}
ValidateCodeForm.displayName = 'ValidateCodeForm';