diff --git a/src/libs/MoneyRequestUtils.js b/src/libs/MoneyRequestUtils.js
new file mode 100644
index 000000000000..706c34ad912d
--- /dev/null
+++ b/src/libs/MoneyRequestUtils.js
@@ -0,0 +1,85 @@
+import lodashGet from 'lodash/get';
+import _ from 'underscore';
+import CONST from '../CONST';
+
+/**
+ * Strip comma from the amount
+ *
+ * @param {String} amount
+ * @returns {String}
+ */
+function stripCommaFromAmount(amount) {
+ return amount.replace(/,/g, '');
+}
+
+/**
+ * Strip spaces from the amount
+ *
+ * @param {String} amount
+ * @returns {String}
+ */
+function stripSpacesFromAmount(amount) {
+ return amount.replace(/\s+/g, '');
+}
+
+/**
+ * Adds a leading zero to the amount if user entered just the decimal separator
+ *
+ * @param {String} amount - Changed amount from user input
+ * @returns {String}
+ */
+function addLeadingZero(amount) {
+ return amount === '.' ? '0.' : amount;
+}
+
+/**
+ * Calculate the length of the amount with leading zeroes
+ *
+ * @param {String} amount
+ * @returns {Number}
+ */
+function calculateAmountLength(amount) {
+ const leadingZeroes = amount.match(/^0+/);
+ const leadingZeroesLength = lodashGet(leadingZeroes, '[0].length', 0);
+ const absAmount = parseFloat((stripCommaFromAmount(amount) * 100).toFixed(2)).toString();
+
+ if (/\D/.test(absAmount)) {
+ return CONST.IOU.AMOUNT_MAX_LENGTH + 1;
+ }
+
+ return leadingZeroesLength + (absAmount === '0' ? 2 : absAmount.length);
+}
+
+/**
+ * Check if amount is a decimal up to 3 digits
+ *
+ * @param {String} amount
+ * @returns {Boolean}
+ */
+function validateAmount(amount) {
+ const decimalNumberRegex = new RegExp(/^\d+(,\d+)*(\.\d{0,2})?$/, 'i');
+ return amount === '' || (decimalNumberRegex.test(amount) && calculateAmountLength(amount) <= CONST.IOU.AMOUNT_MAX_LENGTH);
+}
+
+/**
+ * Replaces each character by calling `convertFn`. If `convertFn` throws an error, then
+ * the original character will be preserved.
+ *
+ * @param {String} text
+ * @param {Function} convertFn - `fromLocaleDigit` or `toLocaleDigit`
+ * @returns {String}
+ */
+function replaceAllDigits(text, convertFn) {
+ return _.chain([...text])
+ .map((char) => {
+ try {
+ return convertFn(char);
+ } catch {
+ return char;
+ }
+ })
+ .join('')
+ .value();
+}
+
+export {stripCommaFromAmount, stripSpacesFromAmount, addLeadingZero, validateAmount, replaceAllDigits};
diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js
index 31d2057ddd5a..6c49fb4c7b57 100644
--- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js
+++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js
@@ -43,7 +43,7 @@ const MoneyRequestModalStackNavigator = createModalStackNavigator([
},
{
getComponent: () => {
- const MoneyRequestEditAmountPage = require('../../../pages/iou/steps/MoneyRequestAmount').default;
+ const MoneyRequestEditAmountPage = require('../../../pages/iou/steps/NewRequestAmountPage').default;
return MoneyRequestEditAmountPage;
},
name: 'Money_Request_Amount',
diff --git a/src/pages/iou/MoneyRequestSelectorPage.js b/src/pages/iou/MoneyRequestSelectorPage.js
index 3d59721e404a..a9fbfb15cbc5 100644
--- a/src/pages/iou/MoneyRequestSelectorPage.js
+++ b/src/pages/iou/MoneyRequestSelectorPage.js
@@ -13,13 +13,13 @@ import useLocalize from '../../hooks/useLocalize';
import * as IOUUtils from '../../libs/IOUUtils';
import Navigation from '../../libs/Navigation/Navigation';
import styles from '../../styles/styles';
-import MoneyRequestAmount from './steps/MoneyRequestAmount';
import ReceiptSelector from './ReceiptSelector';
import * as IOU from '../../libs/actions/IOU';
import DragAndDropProvider from '../../components/DragAndDrop/Provider';
import usePermissions from '../../hooks/usePermissions';
import OnyxTabNavigator, {TopTab} from '../../libs/Navigation/OnyxTabNavigator';
import participantPropTypes from '../../components/participantPropTypes';
+import NewRequestAmountPage from './steps/NewRequestAmountPage';
const propTypes = {
/** React Navigation route */
@@ -98,7 +98,7 @@ function MoneyRequestSelectorPage(props) {
>
) : (
-
+
)}
diff --git a/src/pages/iou/steps/MoneyRequestAmount.js b/src/pages/iou/steps/MoneyRequestAmount.js
deleted file mode 100755
index 966f8068ea21..000000000000
--- a/src/pages/iou/steps/MoneyRequestAmount.js
+++ /dev/null
@@ -1,510 +0,0 @@
-import React, {useEffect, useState, useRef, useCallback} from 'react';
-import {View, InteractionManager} from 'react-native';
-import PropTypes from 'prop-types';
-import {withOnyx} from 'react-native-onyx';
-import lodashGet from 'lodash/get';
-import _ from 'underscore';
-import {useFocusEffect} from '@react-navigation/native';
-import ONYXKEYS from '../../../ONYXKEYS';
-import styles from '../../../styles/styles';
-import BigNumberPad from '../../../components/BigNumberPad';
-import Navigation from '../../../libs/Navigation/Navigation';
-import ROUTES from '../../../ROUTES';
-import compose from '../../../libs/compose';
-import * as ReportUtils from '../../../libs/ReportUtils';
-import * as CurrencyUtils from '../../../libs/CurrencyUtils';
-import Button from '../../../components/Button';
-import CONST from '../../../CONST';
-import * as DeviceCapabilities from '../../../libs/DeviceCapabilities';
-import TextInputWithCurrencySymbol from '../../../components/TextInputWithCurrencySymbol';
-import reportPropTypes from '../../reportPropTypes';
-import * as IOU from '../../../libs/actions/IOU';
-import useLocalize from '../../../hooks/useLocalize';
-import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '../../../components/withCurrentUserPersonalDetails';
-import FullPageNotFoundView from '../../../components/BlockingViews/FullPageNotFoundView';
-import ScreenWrapper from '../../../components/ScreenWrapper';
-import HeaderWithBackButton from '../../../components/HeaderWithBackButton';
-import * as IOUUtils from '../../../libs/IOUUtils';
-
-const propTypes = {
- /** The report on which the request is initiated on */
- report: reportPropTypes,
-
- /** Holds data related to Money Request view state, rather than the underlying Money Request data. */
- iou: PropTypes.shape({
- id: PropTypes.string,
- amount: PropTypes.number,
- currency: PropTypes.string,
- participants: PropTypes.arrayOf(
- PropTypes.shape({
- accountID: PropTypes.number,
- login: PropTypes.string,
- isPolicyExpenseChat: PropTypes.bool,
- isOwnPolicyExpenseChat: PropTypes.bool,
- selected: PropTypes.bool,
- }),
- ),
- }),
-
- route: PropTypes.shape({
- params: PropTypes.shape({
- iouType: PropTypes.string,
- reportID: PropTypes.string,
- }),
- }),
-
- ...withCurrentUserPersonalDetailsPropTypes,
-};
-
-const defaultProps = {
- report: {},
- iou: {
- id: '',
- amount: 0,
- currency: CONST.CURRENCY.USD,
- participants: [],
- },
- route: {
- params: {
- iouType: '',
- reportID: '',
- },
- },
- ...withCurrentUserPersonalDetailsDefaultProps,
-};
-
-const amountViewID = 'amountView';
-const numPadContainerViewID = 'numPadContainerView';
-const numPadViewID = 'numPadView';
-
-/**
- * Returns the new selection object based on the updated amount's length
- *
- * @param {Object} oldSelection
- * @param {Number} prevLength
- * @param {Number} newLength
- * @returns {Object}
- */
-const getNewSelection = (oldSelection, prevLength, newLength) => {
- const cursorPosition = oldSelection.end + (newLength - prevLength);
- return {start: cursorPosition, end: cursorPosition};
-};
-
-/**
- * Strip comma from the amount
- *
- * @param {String} newAmount
- * @returns {String}
- */
-const stripCommaFromAmount = (newAmount) => newAmount.replace(/,/g, '');
-
-/**
- * Strip spaces from the amount
- *
- * @param {String} newAmount
- * @returns {String}
- */
-const stripSpacesFromAmount = (newAmount) => newAmount.replace(/\s+/g, '');
-
-/**
- * Adds a leading zero to the amount if user entered just the decimal separator
- *
- * @param {String} newAmount - Changed amount from user input
- * @returns {String}
- */
-const addLeadingZero = (newAmount) => (newAmount === '.' ? '0.' : newAmount);
-
-/**
- * @param {String} newAmount
- * @returns {Number}
- */
-const calculateAmountLength = (newAmount) => {
- const leadingZeroes = newAmount.match(/^0+/);
- const leadingZeroesLength = lodashGet(leadingZeroes, '[0].length', 0);
- const absAmount = parseFloat((stripCommaFromAmount(newAmount) * 100).toFixed(2)).toString();
-
- // The following logic will prevent users from pasting an amount that is excessively long in length,
- // which would result in the 'absAmount' value being expressed in scientific notation or becoming infinity.
- if (/\D/.test(absAmount)) {
- return CONST.IOU.AMOUNT_MAX_LENGTH + 1;
- }
-
- /*
- Return the sum of leading zeroes length and absolute amount length(including fraction digits).
- When the absolute amount is 0, add 2 to the leading zeroes length to represent fraction digits.
- */
- return leadingZeroesLength + (absAmount === '0' ? 2 : absAmount.length);
-};
-
-/**
- * Check if amount is a decimal up to 3 digits
- *
- * @param {String} newAmount
- * @returns {Boolean}
- */
-const validateAmount = (newAmount) => {
- const decimalNumberRegex = new RegExp(/^\d+(,\d+)*(\.\d{0,2})?$/, 'i');
- return newAmount === '' || (decimalNumberRegex.test(newAmount) && calculateAmountLength(newAmount) <= CONST.IOU.AMOUNT_MAX_LENGTH);
-};
-
-/**
- * Replaces each character by calling `convertFn`. If `convertFn` throws an error, then
- * the original character will be preserved.
- *
- * @param {String} text
- * @param {Function} convertFn - `fromLocaleDigit` or `toLocaleDigit`
- * @returns {String}
- */
-const replaceAllDigits = (text, convertFn) =>
- _.chain([...text])
- .map((char) => {
- try {
- return convertFn(char);
- } catch {
- return char;
- }
- })
- .join('')
- .value();
-
-function MoneyRequestAmount(props) {
- const {translate, toLocaleDigit, fromLocaleDigit, numberFormat} = useLocalize();
- const selectedAmountAsString = props.iou.amount ? CurrencyUtils.convertToWholeUnit(props.iou.currency, props.iou.amount).toString() : '';
-
- const prevMoneyRequestID = useRef(props.iou.id);
- const textInput = useRef(null);
- const iouType = useRef(lodashGet(props.route, 'params.iouType', ''));
- const reportID = useRef(lodashGet(props.route, 'params.reportID', ''));
- const isEditing = useRef(lodashGet(props.route, 'path', '').includes('amount'));
-
- const [amount, setAmount] = useState(selectedAmountAsString);
- const [selectedCurrencyCode, setSelectedCurrencyCode] = useState(props.iou.currency || CONST.CURRENCY.USD);
- const [shouldUpdateSelection, setShouldUpdateSelection] = useState(true);
- const [selection, setSelection] = useState({start: selectedAmountAsString.length, end: selectedAmountAsString.length});
-
- /**
- * Event occurs when a user presses a mouse button over an DOM element.
- *
- * @param {Event} event
- * @param {Array} nativeIds
- */
- const onMouseDown = (event, nativeIds) => {
- const relatedTargetId = lodashGet(event, 'nativeEvent.target.id');
- if (!_.contains(nativeIds, relatedTargetId)) {
- return;
- }
- event.preventDefault();
- if (!textInput.current.isFocused()) {
- textInput.current.focus();
- }
- };
-
- const title = {
- [CONST.IOU.MONEY_REQUEST_TYPE.REQUEST]: translate('iou.requestMoney'),
- [CONST.IOU.MONEY_REQUEST_TYPE.SEND]: translate('iou.sendMoney'),
- [CONST.IOU.MONEY_REQUEST_TYPE.SPLIT]: translate('iou.splitBill'),
- };
- const titleForStep = isEditing.current ? translate('iou.amount') : title[iouType.current];
-
- /**
- * Check and dismiss modal
- */
- useEffect(() => {
- if (!ReportUtils.shouldHideComposer(props.report, props.errors)) {
- return;
- }
- Navigation.dismissModal(reportID.current);
- }, [props.errors, props.report]);
-
- /**
- * Focus text input
- */
- const focusTextInput = () => {
- // Component may not initialized due to navigation transitions
- // Wait until interactions are complete before trying to focus
- InteractionManager.runAfterInteractions(() => {
- // Focus text input
- if (!textInput.current) {
- return;
- }
-
- textInput.current.focus();
- });
- };
-
- /**
- * Convert amount to whole unit and update selection
- *
- * @param {String} currencyCode
- * @param {Number} amountInCurrencyUnits
- */
- const saveAmountToState = (currencyCode, amountInCurrencyUnits) => {
- if (!currencyCode || !amountInCurrencyUnits) {
- return;
- }
- const amountAsStringForState = CurrencyUtils.convertToWholeUnit(currencyCode, amountInCurrencyUnits).toString();
- setAmount(amountAsStringForState);
- setSelection({
- start: amountAsStringForState.length,
- end: amountAsStringForState.length,
- });
- };
-
- useEffect(() => {
- if (isEditing.current) {
- if (prevMoneyRequestID.current !== props.iou.id) {
- // The ID is cleared on completing a request. In that case, we will do nothing.
- if (props.iou.id) {
- Navigation.goBack(ROUTES.getMoneyRequestRoute(iouType.current, reportID.current), true);
- }
- return;
- }
- const moneyRequestID = `${iouType.current}${reportID.current}`;
- const shouldReset = props.iou.id !== moneyRequestID;
- if (shouldReset) {
- IOU.resetMoneyRequestInfo(moneyRequestID);
- }
-
- if (_.isEmpty(props.iou.participants) || props.iou.amount === 0 || shouldReset) {
- Navigation.goBack(ROUTES.getMoneyRequestRoute(iouType.current, reportID.current), true);
- }
- }
-
- return () => {
- prevMoneyRequestID.current = props.iou.id;
- };
- }, [props.iou.participants, props.iou.amount, props.iou.id]);
-
- useEffect(() => {
- if (props.route.params.currency) {
- setSelectedCurrencyCode(props.route.params.currency);
- return;
- }
- if (props.iou.currency) {
- setSelectedCurrencyCode(props.iou.currency);
- }
- }, [props.route.params.currency, props.iou.currency]);
-
- useEffect(() => {
- saveAmountToState(props.iou.currency, props.iou.amount);
- }, [props.iou.amount, props.iou.currency]);
-
- useFocusEffect(
- useCallback(() => {
- focusTextInput();
- }, []),
- );
-
- /**
- * Sets the state according to amount that is passed
- * @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 = stripSpacesFromAmount(newAmount);
- // Use a shallow copy of selection to trigger setSelection
- // More info: https://github.com/Expensify/App/issues/16385
- if (!validateAmount(newAmountWithoutSpaces)) {
- setAmount((prevAmount) => prevAmount);
- setSelection((prevSelection) => ({...prevSelection}));
- return;
- }
- setAmount((prevAmount) => {
- setSelection((prevSelection) => getNewSelection(prevSelection, prevAmount.length, newAmountWithoutSpaces.length));
- return stripCommaFromAmount(newAmountWithoutSpaces);
- });
- };
-
- /**
- * 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
- *
- * @param {String} key
- */
- const updateAmountNumberPad = useCallback(
- (key) => {
- if (shouldUpdateSelection && !textInput.current.isFocused()) {
- textInput.current.focus();
- }
- // Backspace button is pressed
- if (key === '<' || key === 'Backspace') {
- if (amount.length > 0) {
- const selectionStart = selection.start === selection.end ? selection.start - 1 : selection.start;
- const newAmount = `${amount.substring(0, selectionStart)}${amount.substring(selection.end)}`;
- setNewAmount(newAmount);
- }
- return;
- }
- const newAmount = addLeadingZero(`${amount.substring(0, selection.start)}${key}${amount.substring(selection.end)}`);
- setNewAmount(newAmount);
- },
- [amount, selection, shouldUpdateSelection],
- );
-
- /**
- * Update long press value, to remove items pressing on <
- *
- * @param {Boolean} value - Changed text from user input
- */
- const updateLongPressHandlerState = useCallback((value) => {
- setShouldUpdateSelection(!value);
- if (!value && !textInput.current.isFocused()) {
- textInput.current.focus();
- }
- }, []);
-
- /**
- * Update amount on amount change
- * Validate new amount with decimal number regex up to 6 digits and 2 decimal digit
- *
- * @param {String} text - Changed text from user input
- */
- const updateAmount = (text) => {
- const newAmount = addLeadingZero(replaceAllDigits(text, fromLocaleDigit));
- setNewAmount(newAmount);
- };
-
- const navigateBack = () => {
- Navigation.goBack(isEditing.current ? ROUTES.getMoneyRequestConfirmationRoute(iouType.current, reportID.current) : null);
- };
-
- const navigateToCurrencySelectionPage = () => {
- // Remove query from the route and encode it.
- const activeRoute = encodeURIComponent(Navigation.getActiveRoute().replace(/\?.*/, ''));
- Navigation.navigate(ROUTES.getMoneyRequestCurrencyRoute(iouType.current, reportID.current, selectedCurrencyCode, activeRoute));
- };
-
- const navigateToNextPage = () => {
- const amountInSmallestCurrencyUnits = CurrencyUtils.convertToSmallestUnit(selectedCurrencyCode, Number.parseFloat(amount));
- IOU.setMoneyRequestAmount(amountInSmallestCurrencyUnits);
- IOU.setMoneyRequestCurrency(selectedCurrencyCode);
-
- saveAmountToState(selectedCurrencyCode, amountInSmallestCurrencyUnits);
-
- if (isEditing.current) {
- Navigation.goBack(ROUTES.getMoneyRequestConfirmationRoute(iouType.current, reportID.current));
- return;
- }
-
- const moneyRequestID = `${iouType.current}${reportID.current}`;
- const shouldReset = props.iou.id !== moneyRequestID;
- // If the money request ID in Onyx does not match the ID from params, we want to start a new request
- // with the ID from params. We need to clear the participants in case the new request is initiated from FAB.
- if (shouldReset) {
- IOU.setMoneyRequestId(moneyRequestID);
- IOU.setMoneyRequestDescription('');
- IOU.setMoneyRequestParticipants([]);
- }
-
- // If a request is initiated on a report, skip the participants selection step and navigate to the confirmation page.
- if (props.report.reportID) {
- // Reinitialize the participants when the money request ID in Onyx does not match the ID from params
- if (_.isEmpty(props.iou.participants) || shouldReset) {
- const currentUserAccountID = props.currentUserPersonalDetails.accountID;
- const participants = ReportUtils.isPolicyExpenseChat(props.report)
- ? [{reportID: props.report.reportID, isPolicyExpenseChat: true, selected: true}]
- : _.chain(props.report.participantAccountIDs)
- .filter((accountID) => currentUserAccountID !== accountID)
- .map((accountID) => ({accountID, selected: true}))
- .value();
- IOU.setMoneyRequestParticipants(participants);
- }
- Navigation.navigate(ROUTES.getMoneyRequestConfirmationRoute(iouType.current, reportID.current));
- return;
- }
- Navigation.navigate(ROUTES.getMoneyRequestParticipantsRoute(iouType.current));
- };
-
- const formattedAmount = replaceAllDigits(amount, toLocaleDigit);
- const buttonText = isEditing.current ? translate('common.save') : translate('common.next');
-
- const content = (
- <>
- onMouseDown(event, [amountViewID])}
- style={[styles.flex1, styles.flexRow, styles.w100, styles.alignItemsCenter, styles.justifyContentCenter]}
- >
- (textInput.current = el)}
- selectedCurrencyCode={selectedCurrencyCode}
- selection={selection}
- onSelectionChange={(e) => {
- if (!shouldUpdateSelection) {
- return;
- }
- setSelection(e.nativeEvent.selection);
- }}
- />
-
- onMouseDown(event, [numPadContainerViewID, numPadViewID])}
- style={[styles.w100, styles.justifyContentEnd, styles.pageWrapper]}
- nativeID={numPadContainerViewID}
- >
- {DeviceCapabilities.canUseTouchScreen() ? (
-
- ) : (
-
- )}
-
-
-
- >
- );
-
- // ScreenWrapper is only needed in edit mode because we have a dedicated route for the edit amount page (MoneyRequestEditAmountPage).
- // The rest of the cases this component is rendered through which has it's own ScreenWrapper
- if (!isEditing.current) {
- return content;
- }
-
- return (
-
- {({safeAreaPaddingBottomStyle}) => (
-
-
-
- {content}
-
-
- )}
-
- );
-}
-
-MoneyRequestAmount.propTypes = propTypes;
-MoneyRequestAmount.defaultProps = defaultProps;
-MoneyRequestAmount.displayName = 'MoneyRequestAmount';
-
-export default compose(
- withCurrentUserPersonalDetails,
- withOnyx({
- iou: {key: ONYXKEYS.IOU},
- report: {
- key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${lodashGet(route, 'params.reportID', '')}`,
- },
- }),
-)(MoneyRequestAmount);
diff --git a/src/pages/iou/steps/MoneyRequestAmountForm.js b/src/pages/iou/steps/MoneyRequestAmountForm.js
new file mode 100644
index 000000000000..7178ed0e0158
--- /dev/null
+++ b/src/pages/iou/steps/MoneyRequestAmountForm.js
@@ -0,0 +1,266 @@
+import React, {useEffect, useState, useCallback, useRef} from 'react';
+import {View} from 'react-native';
+import PropTypes from 'prop-types';
+import lodashGet from 'lodash/get';
+import _ from 'underscore';
+import styles from '../../../styles/styles';
+import BigNumberPad from '../../../components/BigNumberPad';
+import * as CurrencyUtils from '../../../libs/CurrencyUtils';
+import * as MoneyRequestUtils from '../../../libs/MoneyRequestUtils';
+import Button from '../../../components/Button';
+import * as DeviceCapabilities from '../../../libs/DeviceCapabilities';
+import TextInputWithCurrencySymbol from '../../../components/TextInputWithCurrencySymbol';
+import useLocalize from '../../../hooks/useLocalize';
+import CONST from '../../../CONST';
+
+const propTypes = {
+ /** IOU amount saved in Onyx */
+ amount: PropTypes.number,
+
+ /** Currency chosen by user or saved in Onyx */
+ currency: PropTypes.string,
+
+ /** Whether the amount is being edited or not */
+ isEditing: PropTypes.bool,
+
+ /** Refs forwarded to the TextInputWithCurrencySymbol */
+ forwardedRef: PropTypes.oneOfType([PropTypes.func, PropTypes.shape({current: PropTypes.instanceOf(React.Component)})]),
+
+ /** Fired when back button pressed, navigates to currency selection page */
+ onCurrencyButtonPress: PropTypes.func.isRequired,
+
+ /** Fired when submit button pressed, saves the given amount and navigates to the next page */
+ onSubmitButtonPress: PropTypes.func.isRequired,
+};
+
+const defaultProps = {
+ amount: 0,
+ currency: CONST.CURRENCY.USD,
+ forwardedRef: null,
+ isEditing: false,
+};
+
+/**
+ * Returns the new selection object based on the updated amount's length
+ *
+ * @param {Object} oldSelection
+ * @param {Number} prevLength
+ * @param {Number} newLength
+ * @returns {Object}
+ */
+const getNewSelection = (oldSelection, prevLength, newLength) => {
+ const cursorPosition = oldSelection.end + (newLength - prevLength);
+ return {start: cursorPosition, end: cursorPosition};
+};
+
+const AMOUNT_VIEW_ID = 'amountView';
+const NUM_PAD_CONTAINER_VIEW_ID = 'numPadContainerView';
+const NUM_PAD_VIEW_ID = 'numPadView';
+
+function MoneyRequestAmountForm({amount, currency, isEditing, forwardedRef, onCurrencyButtonPress, onSubmitButtonPress}) {
+ const {translate, toLocaleDigit, fromLocaleDigit, numberFormat} = useLocalize();
+
+ const textInput = useRef(null);
+
+ const selectedAmountAsString = amount ? CurrencyUtils.convertToWholeUnit(currency, amount).toString() : '';
+
+ const [currentAmount, setCurrentAmount] = useState(selectedAmountAsString);
+ const [shouldUpdateSelection, setShouldUpdateSelection] = useState(true);
+
+ const [selection, setSelection] = useState({
+ start: selectedAmountAsString.length,
+ end: selectedAmountAsString.length,
+ });
+
+ /**
+ * Event occurs when a user presses a mouse button over an DOM element.
+ *
+ * @param {Event} event
+ * @param {Array} nativeIds
+ */
+ const onMouseDown = (event, nativeIds) => {
+ const relatedTargetId = lodashGet(event, 'nativeEvent.target.id');
+ if (!_.contains(nativeIds, relatedTargetId)) {
+ return;
+ }
+ event.preventDefault();
+ if (!textInput.current) {
+ return;
+ }
+ if (!textInput.current.isFocused()) {
+ textInput.current.focus();
+ }
+ };
+
+ /**
+ * Convert amount to whole unit and update selection
+ *
+ * @param {String} currencyCode
+ * @param {Number} amountInCurrencyUnits
+ */
+ const saveAmountToState = (currencyCode, amountInCurrencyUnits) => {
+ if (!currencyCode || !amountInCurrencyUnits) {
+ return;
+ }
+ const amountAsStringForState = CurrencyUtils.convertToWholeUnit(currencyCode, amountInCurrencyUnits).toString();
+ setCurrentAmount(amountAsStringForState);
+ setSelection({
+ start: amountAsStringForState.length,
+ end: amountAsStringForState.length,
+ });
+ };
+
+ useEffect(() => {
+ saveAmountToState(currency, amount);
+ // we want to update the state only when the amount is changed
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [amount]);
+
+ /**
+ * Sets the state according to amount that is passed
+ * @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)) {
+ setCurrentAmount((prevAmount) => prevAmount);
+ setSelection((prevSelection) => ({...prevSelection}));
+ return;
+ }
+ setCurrentAmount((prevAmount) => {
+ setSelection((prevSelection) => getNewSelection(prevSelection, prevAmount.length, newAmountWithoutSpaces.length));
+ return MoneyRequestUtils.stripCommaFromAmount(newAmountWithoutSpaces);
+ });
+ };
+
+ /**
+ * 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
+ *
+ * @param {String} key
+ */
+ const updateAmountNumberPad = useCallback(
+ (key) => {
+ if (shouldUpdateSelection && !textInput.current.isFocused()) {
+ textInput.current.focus();
+ }
+ // Backspace button is pressed
+ if (key === '<' || key === 'Backspace') {
+ if (currentAmount.length > 0) {
+ const selectionStart = selection.start === selection.end ? selection.start - 1 : selection.start;
+ const newAmount = `${currentAmount.substring(0, selectionStart)}${currentAmount.substring(selection.end)}`;
+ setNewAmount(newAmount);
+ }
+ return;
+ }
+ const newAmount = MoneyRequestUtils.addLeadingZero(`${currentAmount.substring(0, selection.start)}${key}${currentAmount.substring(selection.end)}`);
+ setNewAmount(newAmount);
+ },
+ [currentAmount, selection, shouldUpdateSelection],
+ );
+
+ /**
+ * Update long press value, to remove items pressing on <
+ *
+ * @param {Boolean} value - Changed text from user input
+ */
+ const updateLongPressHandlerState = useCallback((value) => {
+ setShouldUpdateSelection(!value);
+ if (!value && !textInput.current.isFocused()) {
+ textInput.current.focus();
+ }
+ }, []);
+
+ /**
+ * Update amount on amount change
+ * Validate new amount with decimal number regex up to 6 digits and 2 decimal digit
+ *
+ * @param {String} text - Changed text from user input
+ */
+ const updateAmount = (text) => {
+ const newAmount = MoneyRequestUtils.addLeadingZero(MoneyRequestUtils.replaceAllDigits(text, fromLocaleDigit));
+ setNewAmount(newAmount);
+ };
+
+ /**
+ * Submit amount and navigate to a proper page
+ *
+ */
+ const submitAndNavigateToNextPage = useCallback(() => {
+ onSubmitButtonPress(currentAmount);
+ }, [onSubmitButtonPress, currentAmount]);
+
+ const formattedAmount = MoneyRequestUtils.replaceAllDigits(currentAmount, toLocaleDigit);
+ const buttonText = isEditing ? translate('common.save') : translate('common.next');
+
+ return (
+ <>
+ onMouseDown(event, [AMOUNT_VIEW_ID])}
+ style={[styles.flex1, styles.flexRow, styles.w100, styles.alignItemsCenter, styles.justifyContentCenter]}
+ >
+ {
+ if (typeof forwardedRef === 'function') {
+ forwardedRef(ref);
+ } else if (forwardedRef && _.has(forwardedRef, 'current')) {
+ // eslint-disable-next-line no-param-reassign
+ forwardedRef.current = ref;
+ }
+ textInput.current = ref;
+ }}
+ selectedCurrencyCode={currency}
+ selection={selection}
+ onSelectionChange={(e) => {
+ if (!shouldUpdateSelection) {
+ return;
+ }
+ setSelection(e.nativeEvent.selection);
+ }}
+ />
+
+ onMouseDown(event, [NUM_PAD_CONTAINER_VIEW_ID, NUM_PAD_VIEW_ID])}
+ style={[styles.w100, styles.justifyContentEnd, styles.pageWrapper]}
+ nativeID={NUM_PAD_CONTAINER_VIEW_ID}
+ >
+ {DeviceCapabilities.canUseTouchScreen() ? (
+
+ ) : null}
+
+
+ >
+ );
+}
+
+MoneyRequestAmountForm.propTypes = propTypes;
+MoneyRequestAmountForm.defaultProps = defaultProps;
+MoneyRequestAmountForm.displayName = 'MoneyRequestAmountForm';
+
+export default React.forwardRef((props, ref) => (
+
+));
diff --git a/src/pages/iou/steps/NewRequestAmountPage.js b/src/pages/iou/steps/NewRequestAmountPage.js
new file mode 100644
index 000000000000..90cc4c20b837
--- /dev/null
+++ b/src/pages/iou/steps/NewRequestAmountPage.js
@@ -0,0 +1,208 @@
+import React, {useCallback, useEffect, useRef} from 'react';
+import {InteractionManager, View} from 'react-native';
+import PropTypes from 'prop-types';
+import {withOnyx} from 'react-native-onyx';
+import {useFocusEffect} from '@react-navigation/native';
+import lodashGet from 'lodash/get';
+import _ from 'underscore';
+import ONYXKEYS from '../../../ONYXKEYS';
+import Navigation from '../../../libs/Navigation/Navigation';
+import ROUTES from '../../../ROUTES';
+import * as ReportUtils from '../../../libs/ReportUtils';
+import * as CurrencyUtils from '../../../libs/CurrencyUtils';
+import CONST from '../../../CONST';
+import reportPropTypes from '../../reportPropTypes';
+import * as IOU from '../../../libs/actions/IOU';
+import useLocalize from '../../../hooks/useLocalize';
+import MoneyRequestAmountForm from './MoneyRequestAmountForm';
+import * as IOUUtils from '../../../libs/IOUUtils';
+import FullPageNotFoundView from '../../../components/BlockingViews/FullPageNotFoundView';
+import styles from '../../../styles/styles';
+import HeaderWithBackButton from '../../../components/HeaderWithBackButton';
+import ScreenWrapper from '../../../components/ScreenWrapper';
+
+const propTypes = {
+ route: PropTypes.shape({
+ params: PropTypes.shape({
+ iouType: PropTypes.string,
+ reportID: PropTypes.string,
+ }),
+ }),
+
+ /** The report on which the request is initiated on */
+ report: reportPropTypes,
+
+ /** Holds data related to Money Request view state, rather than the underlying Money Request data. */
+ iou: PropTypes.shape({
+ id: PropTypes.string,
+ amount: PropTypes.number,
+ currency: PropTypes.string,
+ participants: PropTypes.arrayOf(
+ PropTypes.shape({
+ accountID: PropTypes.number,
+ login: PropTypes.string,
+ isPolicyExpenseChat: PropTypes.bool,
+ isOwnPolicyExpenseChat: PropTypes.bool,
+ selected: PropTypes.bool,
+ }),
+ ),
+ }),
+
+ // eslint-disable-next-line react/forbid-prop-types
+ errors: PropTypes.object,
+};
+
+const defaultProps = {
+ route: {
+ params: {
+ iouType: '',
+ reportID: '',
+ },
+ },
+ report: {},
+ iou: {
+ id: '',
+ amount: 0,
+ currency: CONST.CURRENCY.USD,
+ participants: [],
+ },
+ errors: {},
+};
+
+function NewRequestAmountPage({route, iou, report, errors}) {
+ const {translate} = useLocalize();
+
+ const prevMoneyRequestID = useRef(iou.id);
+ const textInput = useRef(null);
+
+ const iouType = lodashGet(route, 'params.iouType', '');
+ const reportID = lodashGet(route, 'params.reportID', '');
+ const isEditing = lodashGet(route, 'path', '').includes('amount');
+ const currentCurrency = lodashGet(route, 'params.currency', '');
+
+ const currency = currentCurrency || iou.currency;
+
+ const focusTextInput = () => {
+ // Component may not be initialized due to navigation transitions
+ // Wait until interactions are complete before trying to focus
+ InteractionManager.runAfterInteractions(() => {
+ // Focus text input
+ if (!textInput.current) {
+ return;
+ }
+
+ textInput.current.focus();
+ });
+ };
+
+ useFocusEffect(
+ useCallback(() => {
+ focusTextInput();
+ }, []),
+ );
+
+ // Check and dismiss modal
+ useEffect(() => {
+ if (!ReportUtils.shouldHideComposer(report, errors)) {
+ return;
+ }
+ Navigation.dismissModal(reportID);
+ }, [errors, report, reportID]);
+
+ // Because we use Onyx to store iou info, when we try to make two different money requests from different tabs, it can result in many bugs.
+ useEffect(() => {
+ if (isEditing) {
+ if (prevMoneyRequestID.current !== iou.id) {
+ // The ID is cleared on completing a request. In that case, we will do nothing.
+ if (!iou.id) {
+ return;
+ }
+ Navigation.goBack(ROUTES.getMoneyRequestRoute(iouType, reportID), true);
+ return;
+ }
+ const moneyRequestID = `${iouType}${reportID}`;
+ const shouldReset = iou.id !== moneyRequestID;
+ if (shouldReset) {
+ IOU.resetMoneyRequestInfo(moneyRequestID);
+ }
+
+ if (_.isEmpty(iou.participants) || iou.amount === 0 || shouldReset) {
+ Navigation.goBack(ROUTES.getMoneyRequestRoute(iouType, reportID), true);
+ }
+ }
+
+ return () => {
+ prevMoneyRequestID.current = iou.id;
+ };
+ }, [iou.participants, iou.amount, iou.id, isEditing, iouType, reportID]);
+
+ const navigateBack = () => {
+ Navigation.goBack(isEditing ? ROUTES.getMoneyRequestConfirmationRoute(iouType, reportID) : null);
+ };
+
+ const navigateToCurrencySelectionPage = () => {
+ // Remove query from the route and encode it.
+ const activeRoute = encodeURIComponent(Navigation.getActiveRoute().replace(/\?.*/, ''));
+ Navigation.navigate(ROUTES.getMoneyRequestCurrencyRoute(iouType, reportID, currency, activeRoute));
+ };
+
+ const navigateToNextPage = (currentAmount) => {
+ const amountInSmallestCurrencyUnits = CurrencyUtils.convertToSmallestUnit(currency, Number.parseFloat(currentAmount));
+ IOU.setMoneyRequestAmount(amountInSmallestCurrencyUnits);
+ IOU.setMoneyRequestCurrency(currency);
+
+ if (isEditing) {
+ Navigation.goBack(ROUTES.getMoneyRequestConfirmationRoute(iouType, reportID));
+ return;
+ }
+
+ IOU.navigateToNextPage(iou, iouType, reportID, report);
+ };
+
+ const content = (
+ (textInput.current = e)}
+ onCurrencyButtonPress={navigateToCurrencySelectionPage}
+ onSubmitButtonPress={navigateToNextPage}
+ />
+ );
+
+ // ScreenWrapper is only needed in edit mode because we have a dedicated route for the edit amount page (MoneyRequestEditAmountPage).
+ // The rest of the cases this component is rendered through which has it's own ScreenWrapper
+ if (!isEditing) {
+ return content;
+ }
+
+ return (
+
+ {({safeAreaPaddingBottomStyle}) => (
+
+
+
+ {content}
+
+
+ )}
+
+ );
+}
+
+NewRequestAmountPage.propTypes = propTypes;
+NewRequestAmountPage.defaultProps = defaultProps;
+NewRequestAmountPage.displayName = 'NewRequestAmountPage';
+
+export default withOnyx({
+ iou: {key: ONYXKEYS.IOU},
+ report: {
+ key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${lodashGet(route, 'params.reportID', '')}`,
+ },
+})(NewRequestAmountPage);