diff --git a/src/CONST.js b/src/CONST.js index f8fbc930c9a3..8da0da9b3f1b 100755 --- a/src/CONST.js +++ b/src/CONST.js @@ -680,6 +680,7 @@ const CONST = { CARD_SECURITY_CODE: /^[0-9]{3,4}$/, CARD_EXPIRATION_DATE: /^(0[1-9]|1[0-2])([^0-9])?([0-9]{4}|([0-9]{2}))$/, PAYPAL_ME_USERNAME: /^[a-zA-Z0-9]+$/, + RATE_VALUE: /^\d{1,8}(\.\d*)?$/, // Adapted from: https://gist.github.com/dperini/729294 // eslint-disable-next-line max-len diff --git a/src/components/BigNumberPad.js b/src/components/BigNumberPad.js index 61a7a159e9ee..2e8082a963d7 100644 --- a/src/components/BigNumberPad.js +++ b/src/components/BigNumberPad.js @@ -63,7 +63,7 @@ class BigNumberPad extends React.Component { style={[styles.flex1, marginLeft]} text={column === '<' ? column : this.props.toLocaleDigit(column)} onLongPress={() => this.handleLongPress(column)} - onPress={() => this.props.numberPressed(column === '<' ? column : this.props.toLocaleDigit(column))} + onPress={() => this.props.numberPressed(column)} onPressIn={ControlSelection.block} onPressOut={() => { clearInterval(this.state.timer); diff --git a/src/pages/iou/steps/IOUAmountPage.js b/src/pages/iou/steps/IOUAmountPage.js index 90d00cf77e04..c1b42042e108 100755 --- a/src/pages/iou/steps/IOUAmountPage.js +++ b/src/pages/iou/steps/IOUAmountPage.js @@ -6,6 +6,7 @@ import { import PropTypes from 'prop-types'; import {withOnyx} from 'react-native-onyx'; import lodashGet from 'lodash/get'; +import _ from 'underscore'; import ONYXKEYS from '../../../ONYXKEYS'; import styles from '../../../styles/styles'; import BigNumberPad from '../../../components/BigNumberPad'; @@ -52,11 +53,12 @@ class IOUAmountPage extends React.Component { this.updateAmountNumberPad = this.updateAmountNumberPad.bind(this); this.updateAmount = this.updateAmount.bind(this); + this.stripCommaFromAmount = this.stripCommaFromAmount.bind(this); this.focusTextInput = this.focusTextInput.bind(this); this.navigateToCurrencySelectionPage = this.navigateToCurrencySelectionPage.bind(this); this.state = { - amount: props.selectedAmount.replace('.', this.props.fromLocaleDigit('.')), + amount: props.selectedAmount, }; } @@ -111,11 +113,20 @@ class IOUAmountPage extends React.Component { * @returns {Boolean} */ validateAmount(amount) { - const decimalSeparator = this.props.fromLocaleDigit('.'); - const decimalNumberRegex = RegExp(String.raw`^\d+([${decimalSeparator}]\d{0,2})?$`, 'i'); + const decimalNumberRegex = new RegExp(/^\d+(,\d+)*(\.\d{0,2})?$/, 'i'); return amount === '' || (decimalNumberRegex.test(amount) && this.calculateAmountLength(amount) <= CONST.IOU.AMOUNT_MAX_LENGTH); } + /** + * Strip comma from the amount + * + * @param {String} amount + * @returns {String} + */ + stripCommaFromAmount(amount) { + return amount.replace(/,/g, ''); + } + /** * Update amount with number or Backspace pressed for BigNumberPad. * Validate new amount with decimal number regex up to 6 digits and 2 decimal digit to enable Next button @@ -135,7 +146,7 @@ class IOUAmountPage extends React.Component { this.setState((prevState) => { const amount = `${prevState.amount}${key}`; - return this.validateAmount(amount) ? {amount} : prevState; + return this.validateAmount(amount) ? {amount: this.stripCommaFromAmount(amount)} : prevState; }); } @@ -143,10 +154,36 @@ class IOUAmountPage extends React.Component { * Update amount on amount change * Validate new amount with decimal number regex up to 6 digits and 2 decimal digit * - * @param {String} amount - Changed amount from user input + * @param {String} text - Changed text from user input + */ + updateAmount(text) { + this.setState((prevState) => { + const amount = this.replaceAllDigits(text, this.props.fromLocaleDigit); + return this.validateAmount(amount) + ? {amount: this.stripCommaFromAmount(amount)} + : prevState; + }); + } + + /** + * Replaces each character by calling `convertFn`. If `convertFn` throws an error, then + * the original character will be preserved. + * + * @param {String} text + * @param {Function} convertFn - `this.props.fromLocaleDigit` or `this.props.toLocaleDigit` + * @returns {String} */ - updateAmount(amount) { - this.setState(prevState => (this.validateAmount(amount) ? {amount} : prevState)); + replaceAllDigits(text, convertFn) { + return _.chain([...text]) + .map((char) => { + try { + return convertFn(char); + } catch { + return char; + } + }) + .join('') + .value(); } navigateToCurrencySelectionPage() { @@ -160,6 +197,8 @@ class IOUAmountPage extends React.Component { } render() { + const formattedAmount = this.replaceAllDigits(this.state.amount, this.props.toLocaleDigit); + return ( <> this.props.onStepComplete(this.state.amount.replace(this.props.fromLocaleDigit('.'), '.'))} + onPress={() => this.props.onStepComplete(this.state.amount)} pressOnEnter isDisabled={!this.state.amount.length || parseFloat(this.state.amount) < 0.01} text={this.props.translate('common.next')} diff --git a/src/pages/workspace/reimburse/WorkspaceReimburseView.js b/src/pages/workspace/reimburse/WorkspaceReimburseView.js index d01190a221c9..9ae0b280e24f 100644 --- a/src/pages/workspace/reimburse/WorkspaceReimburseView.js +++ b/src/pages/workspace/reimburse/WorkspaceReimburseView.js @@ -77,13 +77,16 @@ class WorkspaceReimburseView extends React.Component { } getRateDisplayValue(value) { - return value.toString().replace('.', this.props.fromLocaleDigit('.')); + const numValue = parseFloat(value); + if (Number.isNaN(numValue)) { + return ''; + } + + return numValue.toFixed(3); } setRate(value) { - const decimalSeparator = this.props.fromLocaleDigit('.'); - const rateValueRegex = RegExp(String.raw`^\d{1,8}([${decimalSeparator}]\d{0,3})?$`, 'i'); - const isInvalidRateValue = value !== '' && !rateValueRegex.test(value); + const isInvalidRateValue = value !== '' && !CONST.REGEX.RATE_VALUE.test(value); this.setState(prevState => ({ rateValue: !isInvalidRateValue ? value : prevState.rateValue, @@ -112,12 +115,16 @@ class WorkspaceReimburseView extends React.Component { } updateRateValue(value) { - const numValue = parseFloat(value.replace(this.props.fromLocaleDigit('.'), '.')); + const numValue = parseFloat(value); if (_.isNaN(numValue)) { return; } + this.setState({ + rateValue: numValue.toFixed(3), + }); + Policy.setCustomUnitRate(this.props.policyID, this.state.unitID, { customUnitRateID: this.state.rateID, name: this.state.rateName,