diff --git a/src/libs/MoneyRequestUtils.ts b/src/libs/MoneyRequestUtils.ts index b8a6a3da303f..c41fe73947d1 100644 --- a/src/libs/MoneyRequestUtils.ts +++ b/src/libs/MoneyRequestUtils.ts @@ -15,6 +15,13 @@ function stripSpacesFromAmount(amount: string): string { return amount.replace(/\s+/g, ''); } +/** + * Strip decimals from the amount + */ +function stripDecimalsFromAmount(amount: string): string { + return amount.replace(/\.\d*$/, ''); +} + /** * Adds a leading zero to the amount if user entered just the decimal separator * @@ -42,8 +49,12 @@ function calculateAmountLength(amount: string): number { /** * Check if amount is a decimal up to 3 digits */ -function validateAmount(amount: string): boolean { - const decimalNumberRegex = new RegExp(/^\d+(,\d+)*(\.\d{0,2})?$/, 'i'); +function validateAmount(amount: string, decimals: number): boolean { + const regexString = + decimals === 0 + ? `^\\d+(,\\d+)*$` // Don't allow decimal point if decimals === 0 + : `^\\d+(,\\d+)*(\\.\\d{0,${decimals}})?$`; // Allow the decimal point and the desired number of digits after the point + const decimalNumberRegex = new RegExp(regexString, 'i'); return amount === '' || (decimalNumberRegex.test(amount) && calculateAmountLength(amount) <= CONST.IOU.AMOUNT_MAX_LENGTH); } @@ -78,4 +89,4 @@ function isScanRequest(selectedTab: ValueOf): boolean { return selectedTab === CONST.TAB.SCAN; } -export {stripCommaFromAmount, stripSpacesFromAmount, addLeadingZero, validateAmount, replaceAllDigits, isDistanceRequest, isScanRequest}; +export {stripCommaFromAmount, stripDecimalsFromAmount, stripSpacesFromAmount, addLeadingZero, validateAmount, replaceAllDigits, isDistanceRequest, isScanRequest}; diff --git a/src/pages/iou/steps/MoneyRequestAmountForm.js b/src/pages/iou/steps/MoneyRequestAmountForm.js index e08fd5bde881..5079cb9a3d82 100644 --- a/src/pages/iou/steps/MoneyRequestAmountForm.js +++ b/src/pages/iou/steps/MoneyRequestAmountForm.js @@ -70,6 +70,7 @@ function MoneyRequestAmountForm({amount, currency, isEditing, forwardedRef, onCu const textInput = useRef(null); + const decimals = CurrencyUtils.getCurrencyDecimals(currency); const selectedAmountAsString = amount ? CurrencyUtils.convertToFrontendAmount(amount).toString() : ''; const [currentAmount, setCurrentAmount] = useState(selectedAmountAsString); @@ -123,26 +124,43 @@ function MoneyRequestAmountForm({amount, currency, isEditing, forwardedRef, onCu * Sets the selection and the amount accordingly to the value passed to the input * @param {String} newAmount - Changed amount from user input */ - const setNewAmount = (newAmount) => { - // Remove spaces from the newAmount value because Safari on iOS adds spaces when pasting a copied value - // More info: https://github.com/Expensify/App/issues/16974 - const newAmountWithoutSpaces = MoneyRequestUtils.stripSpacesFromAmount(newAmount); - // Use a shallow copy of selection to trigger setSelection - // More info: https://github.com/Expensify/App/issues/16385 - if (!MoneyRequestUtils.validateAmount(newAmountWithoutSpaces)) { - setSelection((prevSelection) => ({...prevSelection})); + const setNewAmount = useCallback( + (newAmount) => { + // Remove spaces from the newAmount value because Safari on iOS adds spaces when pasting a copied value + // More info: https://github.com/Expensify/App/issues/16974 + const newAmountWithoutSpaces = MoneyRequestUtils.stripSpacesFromAmount(newAmount); + // Use a shallow copy of selection to trigger setSelection + // More info: https://github.com/Expensify/App/issues/16385 + if (!MoneyRequestUtils.validateAmount(newAmountWithoutSpaces, decimals)) { + setSelection((prevSelection) => ({...prevSelection})); + return; + } + const checkInvalidAmount = isAmountValid(newAmountWithoutSpaces); + setIsInvalidAmount(checkInvalidAmount); + setFormError(checkInvalidAmount ? 'iou.error.invalidAmount' : ''); + setCurrentAmount((prevAmount) => { + const strippedAmount = MoneyRequestUtils.stripCommaFromAmount(newAmountWithoutSpaces); + const isForwardDelete = prevAmount.length > strippedAmount.length && forwardDeletePressedRef.current; + setSelection((prevSelection) => getNewSelection(prevSelection, isForwardDelete ? strippedAmount.length : prevAmount.length, strippedAmount.length)); + return strippedAmount; + }); + }, + [decimals], + ); + + // Modifies the amount to match the decimals for changed currency. + useEffect(() => { + // If the changed currency supports decimals, we can return + if (MoneyRequestUtils.validateAmount(currentAmount, decimals)) { return; } - const checkInvalidAmount = isAmountValid(newAmountWithoutSpaces); - setIsInvalidAmount(checkInvalidAmount); - setFormError(checkInvalidAmount ? 'iou.error.invalidAmount' : ''); - setCurrentAmount((prevAmount) => { - const strippedAmount = MoneyRequestUtils.stripCommaFromAmount(newAmountWithoutSpaces); - const isForwardDelete = prevAmount.length > strippedAmount.length && forwardDeletePressedRef.current; - setSelection((prevSelection) => getNewSelection(prevSelection, isForwardDelete ? strippedAmount.length : prevAmount.length, strippedAmount.length)); - return strippedAmount; - }); - }; + + // If the changed currency doesn't support decimals, we can strip the decimals + setNewAmount(MoneyRequestUtils.stripDecimalsFromAmount(currentAmount)); + + // we want to update only when decimals change (setNewAmount also changes when decimals change). + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [setNewAmount]); /** * Update amount with number or Backspace pressed for BigNumberPad. @@ -167,7 +185,7 @@ function MoneyRequestAmountForm({amount, currency, isEditing, forwardedRef, onCu const newAmount = MoneyRequestUtils.addLeadingZero(`${currentAmount.substring(0, selection.start)}${key}${currentAmount.substring(selection.end)}`); setNewAmount(newAmount); }, - [currentAmount, selection, shouldUpdateSelection], + [currentAmount, selection, shouldUpdateSelection, setNewAmount], ); /**