From 06ae05511ad1edb37f19dfdebd4cee12953172f4 Mon Sep 17 00:00:00 2001 From: Ali Toshmatov Date: Sat, 22 Jul 2023 10:09:53 +0500 Subject: [PATCH 001/644] Added recovery code feature --- src/CONST.js | 5 + src/languages/en.js | 9 ++ src/languages/es.js | 9 ++ src/libs/ValidationUtils.js | 5 + .../ValidateCodeForm/BaseValidateCodeForm.js | 117 ++++++++++++++---- 5 files changed, 121 insertions(+), 24 deletions(-) diff --git a/src/CONST.js b/src/CONST.js index 46aa9a1943e9..042728e3ddcb 100755 --- a/src/CONST.js +++ b/src/CONST.js @@ -760,6 +760,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, @@ -801,6 +804,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.js b/src/languages/en.js index 47cc8d209735..3e01f8ceb722 100755 --- a/src/languages/en.js +++ b/src/languages/en.js @@ -567,6 +567,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', diff --git a/src/languages/es.js b/src/languages/es.js index 5eea74099e4c..7c112f5658ff 100644 --- a/src/languages/es.js +++ b/src/languages/es.js @@ -568,6 +568,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', diff --git a/src/libs/ValidationUtils.js b/src/libs/ValidationUtils.js index c120f649b401..229813c64dd6 100644 --- a/src/libs/ValidationUtils.js +++ b/src/libs/ValidationUtils.js @@ -307,6 +307,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} @@ -478,4 +482,5 @@ export { doesContainReservedWord, isNumeric, isValidAccountRoute, + isValidRecoveryCode, }; diff --git a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js index 9abbf5ea4957..f211f154a8a1 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 */ @@ -77,6 +78,8 @@ function BaseValidateCodeForm(props) { const [validateCode, setValidateCode] = useState(props.credentials.validateCode || ''); const [twoFactorAuthCode, setTwoFactorAuthCode] = useState(''); const [timeRemaining, setTimeRemaining] = useState(30); + const [isUsingRecoveryCode, setIsUsingRecoveryCode] = useState(false); + const [recoveryCode, setRecoveryCode] = useState(''); const prevRequiresTwoFactorAuth = usePrevious(props.account.requiresTwoFactorAuth); const prevValidateCode = usePrevious(props.credentials.validateCode); @@ -148,7 +151,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]: ''})); @@ -183,6 +196,22 @@ function BaseValidateCodeForm(props) { Session.clearSignInData(); }; + /** + * Switches between 2fa and recovery code, clears inputs and errors + */ + const switchBetween2faAndRecoveryCode = () => { + setIsUsingRecoveryCode(!isUsingRecoveryCode); + + setRecoveryCode(''); + setTwoFactorAuthCode(''); + + setFormError((prevError) => ({...prevError, recoveryCode: '', twoFactorAuthCode: ''})); + + if (props.account.errors) { + Session.clearAccountMessages(); + } + }; + useEffect(() => { if (!isLoadingResendValidationForm) { return; @@ -199,13 +228,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 (!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) { @@ -222,33 +265,59 @@ function BaseValidateCodeForm(props) { } setFormError({}); + const recoveryCodeOr2faCode = 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.requiresTwoFactorAuth, props.credentials, props.preferredLocale, twoFactorAuthCode, validateCode]); + }, [props.account.requiresTwoFactorAuth, props.credentials, props.preferredLocale, twoFactorAuthCode, validateCode, 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 - /> + {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 && } + + {isUsingRecoveryCode ? props.translate('recoveryCodeForm.use2fa') : props.translate('recoveryCodeForm.useRecoveryCode')} + ) : ( From d21046da995658519255fd53e0c59dcae2b1d508 Mon Sep 17 00:00:00 2001 From: Ali Toshmatov Date: Sat, 22 Jul 2023 10:58:38 +0500 Subject: [PATCH 002/644] Added focus delay so that keyboard behaves smoothlier --- src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js index f211f154a8a1..458863d1eae0 100755 --- a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js +++ b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js @@ -282,6 +282,7 @@ function BaseValidateCodeForm(props) { {isUsingRecoveryCode ? ( onTextInput(text, 'recoveryCode')} @@ -293,6 +294,7 @@ function BaseValidateCodeForm(props) { /> ) : ( Date: Thu, 27 Jul 2023 10:21:00 +0700 Subject: [PATCH 003/644] not disable next button --- src/languages/en.js | 2 ++ src/languages/es.js | 2 ++ src/pages/iou/steps/MoneyRequestAmountPage.js | 35 ++++++++++++++----- .../Security/TwoFactorAuth/CodesPage.js | 19 ++++++++-- 4 files changed, 47 insertions(+), 11 deletions(-) diff --git a/src/languages/en.js b/src/languages/en.js index b7a130addf18..467256434465 100755 --- a/src/languages/en.js +++ b/src/languages/en.js @@ -378,6 +378,7 @@ export default { threadRequestReportName: ({formattedAmount, comment}) => `${formattedAmount} request${comment ? ` for ${comment}` : ''}`, threadSentMoneyReportName: ({formattedAmount, comment}) => `${formattedAmount} sent${comment ? ` for ${comment}` : ''}`, error: { + invalidAmount: 'Please enter a valid amount', invalidSplit: 'Split amounts do not equal total amount', other: 'Unexpected error, please try again later', genericCreateFailureMessage: 'Unexpected error requesting money, please try again later', @@ -561,6 +562,7 @@ export default { keepCodesSafe: 'Keep these recovery codes safe!', codesLoseAccess: 'If you lose access to your authenticator app and don’t have these codes, you will lose access to your account. \n\nNote: Setting up two-factor authentication will log you out of all other active sessions.', + errorStepCodes: 'Please copy or download codes before continuing.', stepVerify: 'Verify', scanCode: 'Scan the QR code using your', authenticatorApp: 'authenticator app', diff --git a/src/languages/es.js b/src/languages/es.js index 4006f559eb1f..d69bb00a17dc 100644 --- a/src/languages/es.js +++ b/src/languages/es.js @@ -377,6 +377,7 @@ export default { threadRequestReportName: ({formattedAmount, comment}) => `Solicitud de ${formattedAmount}${comment ? ` para ${comment}` : ''}`, threadSentMoneyReportName: ({formattedAmount, comment}) => `${formattedAmount} enviado${comment ? ` para ${comment}` : ''}`, error: { + invalidAmount: 'Por favor ingrese una cantidad válida', invalidSplit: 'La suma de las partes no equivale al monto total', other: 'Error inesperado, por favor inténtalo más tarde', genericCreateFailureMessage: 'Error inesperado solicitando dinero, Por favor, inténtalo más tarde', @@ -562,6 +563,7 @@ export default { 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.', + errorStepCodes: 'Copie o descargue los códigos antes de continuar.', stepVerify: 'Verificar', scanCode: 'Escanea el código QR usando tu', authenticatorApp: 'aplicación de autenticación', diff --git a/src/pages/iou/steps/MoneyRequestAmountPage.js b/src/pages/iou/steps/MoneyRequestAmountPage.js index e25f7acb0553..5a83a6f5db17 100755 --- a/src/pages/iou/steps/MoneyRequestAmountPage.js +++ b/src/pages/iou/steps/MoneyRequestAmountPage.js @@ -24,6 +24,7 @@ import HeaderWithBackButton from '../../../components/HeaderWithBackButton'; import reportPropTypes from '../../reportPropTypes'; import * as IOU from '../../../libs/actions/IOU'; import useLocalize from '../../../hooks/useLocalize'; +import FormHelpMessage from '../../../components/FormHelpMessage'; import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '../../../components/withCurrentUserPersonalDetails'; const propTypes = { @@ -178,6 +179,8 @@ function MoneyRequestAmountPage(props) { const isEditing = useRef(lodashGet(props.route, 'path', '').includes('amount')); const [amount, setAmount] = useState(selectedAmountAsString); + const [isInvaidAmount, setIsInvalidAmount] = useState(!selectedAmountAsString.length || parseFloat(selectedAmountAsString) < 0.01); + const [error, setError] = useState(''); const [selectedCurrencyCode, setSelectedCurrencyCode] = useState(props.iou.currency); const [shouldUpdateSelection, setShouldUpdateSelection] = useState(true); const [selection, setSelection] = useState({start: selectedAmountAsString.length, end: selectedAmountAsString.length}); @@ -339,6 +342,8 @@ function MoneyRequestAmountPage(props) { return; } const newAmount = addLeadingZero(`${amount.substring(0, selection.start)}${key}${amount.substring(selection.end)}`); + setIsInvalidAmount(!newAmount.length || parseFloat(newAmount) < 0.01); + setError(''); setNewAmount(newAmount); }, [amount, selection, shouldUpdateSelection], @@ -364,6 +369,8 @@ function MoneyRequestAmountPage(props) { */ const updateAmount = (text) => { const newAmount = addLeadingZero(replaceAllDigits(text, fromLocaleDigit)); + setIsInvalidAmount(!newAmount.length || parseFloat(newAmount) < 0.01); + setError(''); setNewAmount(newAmount); }; @@ -378,6 +385,11 @@ function MoneyRequestAmountPage(props) { }; const navigateToNextPage = () => { + if (isInvaidAmount) { + setError(translate('iou.error.invalidAmount')); + return; + } + const amountInSmallestCurrencyUnits = CurrencyUtils.convertToSmallestUnit(selectedCurrencyCode, Number.parseFloat(amount)); IOU.setMoneyRequestAmount(amountInSmallestCurrencyUnits); IOU.setMoneyRequestCurrency(selectedCurrencyCode); @@ -468,15 +480,20 @@ function MoneyRequestAmountPage(props) { ) : ( )} - -