diff --git a/src/ONYXKEYS.js b/src/ONYXKEYS.js index cd4114be508c..6fe8a0625cd8 100755 --- a/src/ONYXKEYS.js +++ b/src/ONYXKEYS.js @@ -30,7 +30,7 @@ export default { // Credentials to authenticate the user CREDENTIALS: 'credentials', - // Contains loading data for the IOU feature (IOUModal, IOUDetail, & IOUPreview Components) + // Contains loading data for the IOU feature (MoneyRequestModal, IOUDetail, & IOUPreview Components) IOU: 'iou', // Keeps track if there is modal currently visible or not diff --git a/src/ROUTES.js b/src/ROUTES.js index 2412e7e42550..d5ba2406e322 100644 --- a/src/ROUTES.js +++ b/src/ROUTES.js @@ -6,9 +6,9 @@ import * as Url from './libs/Url'; */ const REPORT = 'r'; -const IOU_REQUEST = 'iou/request'; -const IOU_BILL = 'iou/split'; -const IOU_SEND = 'iou/send'; +const IOU_REQUEST = 'request/new'; +const IOU_BILL = 'split/new'; +const IOU_SEND = 'send/new'; const IOU_DETAILS = 'iou/details'; const IOU_REQUEST_CURRENCY = `${IOU_REQUEST}/currency`; const IOU_BILL_CURRENCY = `${IOU_BILL}/currency`; diff --git a/src/components/IOUConfirmationList.js b/src/components/IOUConfirmationList.js index eae3f48d84ca..40439bfd39f6 100755 --- a/src/components/IOUConfirmationList.js +++ b/src/components/IOUConfirmationList.js @@ -25,10 +25,10 @@ const propTypes = { /** Callback to parent modal to send money */ onSendMoney: PropTypes.func.isRequired, - /** Callback to update comment from IOUModal */ + /** Callback to update comment from MoneyRequestModal */ onUpdateComment: PropTypes.func, - /** Comment value from IOUModal */ + /** Comment value from MoneyRequestModal */ comment: PropTypes.string, /** Should we request a single or multiple participant selection from user */ @@ -40,7 +40,7 @@ const propTypes = { /** IOU type */ iouType: PropTypes.string, - /** Selected participants from IOUModal with login */ + /** Selected participants from MoneyRequestModal with login */ participants: PropTypes.arrayOf(PropTypes.shape({ login: PropTypes.string.isRequired, alternateText: PropTypes.string, diff --git a/src/libs/actions/PersonalDetails.js b/src/libs/actions/PersonalDetails.js index ce3151da9c66..c6e1be4f6f41 100644 --- a/src/libs/actions/PersonalDetails.js +++ b/src/libs/actions/PersonalDetails.js @@ -258,7 +258,7 @@ function updateSelectedTimezone(selectedTimezone) { /** * Fetches the local currency based on location and sets currency code/symbol to Onyx */ -function openIOUModalPage() { +function openMoneyRequestModalPage() { API.read('OpenIOUModalPage'); } @@ -368,7 +368,7 @@ export { getDisplayName, updateAvatar, deleteAvatar, - openIOUModalPage, + openMoneyRequestModalPage, openPersonalDetailsPage, extractFirstAndLastNameFromAvailableDetails, updateDisplayName, diff --git a/src/pages/iou/IOUBillPage.js b/src/pages/iou/IOUBillPage.js index d1938ec41a15..6ca3bc6ec916 100644 --- a/src/pages/iou/IOUBillPage.js +++ b/src/pages/iou/IOUBillPage.js @@ -1,7 +1,7 @@ import React from 'react'; -import IOUModal from './IOUModal'; +import MoneyRequestModal from './MoneyRequestModal'; // eslint-disable-next-line react/jsx-props-no-spreading -const IOUBillPage = props => ; +const IOUBillPage = props => ; IOUBillPage.displayName = 'IOUBillPage'; export default IOUBillPage; diff --git a/src/pages/iou/IOUModal.js b/src/pages/iou/IOUModal.js deleted file mode 100755 index 069ff51c7e57..000000000000 --- a/src/pages/iou/IOUModal.js +++ /dev/null @@ -1,536 +0,0 @@ -import _ from 'underscore'; -import React, {Component} from 'react'; -import {View, TouchableOpacity} from 'react-native'; -import PropTypes from 'prop-types'; -import lodashGet from 'lodash/get'; -import {withOnyx} from 'react-native-onyx'; -import Str from 'expensify-common/lib/str'; -import IOUAmountPage from './steps/IOUAmountPage'; -import IOUParticipantsPage from './steps/IOUParticipantsPage/IOUParticipantsPage'; -import IOUConfirmPage from './steps/IOUConfirmPage'; -import Header from '../../components/Header'; -import styles from '../../styles/styles'; -import Icon from '../../components/Icon'; -import * as IOU from '../../libs/actions/IOU'; -import * as Expensicons from '../../components/Icon/Expensicons'; -import Navigation from '../../libs/Navigation/Navigation'; -import ONYXKEYS from '../../ONYXKEYS'; -import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize'; -import compose from '../../libs/compose'; -import * as OptionsListUtils from '../../libs/OptionsListUtils'; -import FullScreenLoadingIndicator from '../../components/FullscreenLoadingIndicator'; -import AnimatedStep from '../../components/AnimatedStep'; -import ScreenWrapper from '../../components/ScreenWrapper'; -import Tooltip from '../../components/Tooltip'; -import CONST from '../../CONST'; -import * as PersonalDetails from '../../libs/actions/PersonalDetails'; -import withCurrentUserPersonalDetails from '../../components/withCurrentUserPersonalDetails'; -import networkPropTypes from '../../components/networkPropTypes'; -import {withNetwork} from '../../components/OnyxProvider'; -import reportPropTypes from '../reportPropTypes'; -import * as ReportUtils from '../../libs/ReportUtils'; -import * as ReportScrollManager from '../../libs/ReportScrollManager'; - -/** - * IOU modal for requesting money and splitting bills. - */ -const propTypes = { - /** Whether the IOU is for a single request or a group bill split */ - hasMultipleParticipants: PropTypes.bool, - - /** The type of IOU report, i.e. bill, request, send */ - iouType: PropTypes.string, - - /** The report passed via the route */ - // eslint-disable-next-line react/no-unused-prop-types - report: reportPropTypes, - - /** Information about the network */ - network: networkPropTypes.isRequired, - - // Holds data related to IOU view state, rather than the underlying IOU data. - iou: PropTypes.shape({ - /** Whether or not transaction creation has started */ - creatingIOUTransaction: PropTypes.bool, - - /** Whether or not transaction creation has resulted to error */ - error: PropTypes.bool, - - // Selected Currency Code of the current IOU - selectedCurrencyCode: PropTypes.string, - }), - - /** Personal details of all the users */ - personalDetails: PropTypes.shape({ - /** Primary login of participant */ - login: PropTypes.string, - - /** Display Name of participant */ - displayName: PropTypes.string, - - /** Avatar url of participant */ - avatar: PropTypes.string, - }), - - /** Personal details of the current user */ - currentUserPersonalDetails: PropTypes.shape({ - // Local Currency Code of the current user - localCurrencyCode: PropTypes.string, - }), - - ...withLocalizePropTypes, -}; - -const defaultProps = { - hasMultipleParticipants: false, - report: { - participants: [], - }, - iouType: CONST.IOU.IOU_TYPE.REQUEST, - currentUserPersonalDetails: { - localCurrencyCode: CONST.CURRENCY.USD, - }, - personalDetails: {}, - iou: { - creatingIOUTransaction: false, - error: false, - selectedCurrencyCode: null, - }, -}; - -// Determines type of step to display within Modal, value provides the title for that page. -const Steps = { - IOUAmount: 'iou.amount', - IOUParticipants: 'iou.participants', - IOUConfirm: 'iou.confirm', -}; - -class IOUModal extends Component { - constructor(props) { - super(props); - this.navigateToPreviousStep = this.navigateToPreviousStep.bind(this); - this.navigateToNextStep = this.navigateToNextStep.bind(this); - this.addParticipants = this.addParticipants.bind(this); - this.createTransaction = this.createTransaction.bind(this); - this.updateComment = this.updateComment.bind(this); - this.sendMoney = this.sendMoney.bind(this); - - const participants = lodashGet(props, 'report.participants', []); - const participantsWithDetails = _.map(OptionsListUtils.getPersonalDetailsForLogins(participants, props.personalDetails), personalDetails => ({ - login: personalDetails.login, - text: personalDetails.displayName, - firstName: lodashGet(personalDetails, 'firstName', ''), - lastName: lodashGet(personalDetails, 'lastName', ''), - alternateText: Str.isSMSLogin(personalDetails.login) ? Str.removeSMSDomain(personalDetails.login) : personalDetails.login, - icons: [{ - source: ReportUtils.getAvatar(personalDetails.avatar, personalDetails.login), - name: personalDetails.login, - type: CONST.ICON_TYPE_AVATAR, - }], - keyForList: personalDetails.login, - payPalMeAddress: lodashGet(personalDetails, 'payPalMeAddress', ''), - phoneNumber: lodashGet(personalDetails, 'phoneNumber', ''), - })); - - this.state = { - previousStepIndex: 0, - currentStepIndex: 0, - participants: participantsWithDetails, - - // amount is currency in decimal format - amount: '', - comment: '', - }; - - // Skip IOUParticipants step if participants are passed in - if (participants.length) { - // The steps to be shown within the create IOU flow. - this.steps = [Steps.IOUAmount, Steps.IOUConfirm]; - } else { - this.steps = [Steps.IOUAmount, Steps.IOUParticipants, Steps.IOUConfirm]; - } - } - - componentDidMount() { - PersonalDetails.openIOUModalPage(); - IOU.setIOUSelectedCurrency(this.props.currentUserPersonalDetails.localCurrencyCode); - } - - componentDidUpdate(prevProps) { - const wasCreatingIOUTransaction = lodashGet(prevProps, 'iou.creatingIOUTransaction'); - const iouError = lodashGet(this.props, 'iou.error'); - if (prevProps.network.isOffline && !this.props.network.isOffline) { - PersonalDetails.openIOUModalPage(); - } - - // Successfully close the modal if transaction creation has ended and there is no error - if (wasCreatingIOUTransaction && !lodashGet(this.props, 'iou.creatingIOUTransaction') && !iouError) { - Navigation.dismissModal(); - } - - // If transaction fails, handling it here - if (wasCreatingIOUTransaction && iouError === true) { - // Navigating to Enter Amount Page - // eslint-disable-next-line react/no-did-update-set-state - this.setState({currentStepIndex: 0}); - this.creatingIOUTransaction = false; - } - - const currentSelectedCurrencyCode = lodashGet(this.props, 'iou.selectedCurrencyCode'); - if (lodashGet(prevProps, 'iou.selectedCurrencyCode') !== currentSelectedCurrencyCode) { - IOU.setIOUSelectedCurrency(currentSelectedCurrencyCode); - } - } - - /** - * Decides our animation type based on whether we're increasing or decreasing - * our step index. - * @returns {String} - */ - getDirection() { - if (this.state.previousStepIndex < this.state.currentStepIndex) { - return 'in'; - } - if (this.state.previousStepIndex > this.state.currentStepIndex) { - return 'out'; - } - - // Doesn't animate the step when first opening the modal - if (this.state.previousStepIndex === this.state.currentStepIndex) { - return null; - } - } - - /** - * Retrieve title for current step, based upon current step and type of IOU - * - * @returns {String} - */ - getTitleForStep() { - const currentStepIndex = this.state.currentStepIndex; - const isSendingMoney = this.props.iouType === CONST.IOU.IOU_TYPE.SEND; - if (currentStepIndex === 1 || currentStepIndex === 2) { - const formattedAmount = this.props.numberFormat( - this.state.amount, { - style: 'currency', - currency: this.props.iou.selectedCurrencyCode, - }, - ); - if (isSendingMoney) { - return this.props.translate('iou.send', { - amount: formattedAmount, - }); - } - return this.props.translate( - this.props.hasMultipleParticipants ? 'iou.split' : 'iou.request', { - amount: formattedAmount, - }, - ); - } - if (currentStepIndex === 0) { - if (isSendingMoney) { - return this.props.translate('iou.sendMoney'); - } - return this.props.translate(this.props.hasMultipleParticipants ? 'iou.splitBill' : 'iou.requestMoney'); - } - - return this.props.translate(this.steps[currentStepIndex]) || ''; - } - - /** - * Update comment whenever user enters any new text - * - * @param {String} comment - */ - updateComment(comment) { - this.setState({ - comment, - }); - } - - /** - * Update participants whenever user selects the payment recipient - * - * @param {Array} participants - */ - addParticipants(participants) { - this.setState({ - participants, - }); - } - - /** - * Navigate to the next IOU step if possible - */ - navigateToPreviousStep() { - if (this.state.currentStepIndex <= 0) { - return; - } - this.setState(prevState => ({ - previousStepIndex: prevState.currentStepIndex, - currentStepIndex: prevState.currentStepIndex - 1, - })); - } - - /** - * Navigate to the previous IOU step if possible - */ - navigateToNextStep() { - if (this.state.currentStepIndex >= this.steps.length - 1) { - return; - } - - this.setState(prevState => ({ - previousStepIndex: prevState.currentStepIndex, - currentStepIndex: prevState.currentStepIndex + 1, - })); - } - - /** - * Checks if user has a GOLD wallet then creates a paid IOU report on the fly - * - * @param {String} paymentMethodType - */ - sendMoney(paymentMethodType) { - const amount = Math.round(this.state.amount * 100); - const currency = this.props.iou.selectedCurrencyCode; - const comment = this.state.comment.trim(); - const participant = this.state.participants[0]; - - if (paymentMethodType === CONST.IOU.PAYMENT_TYPE.ELSEWHERE) { - IOU.sendMoneyElsewhere( - this.props.report, - amount, - currency, - comment, - this.props.currentUserPersonalDetails.login, - participant, - ); - return; - } - - if (paymentMethodType === CONST.IOU.PAYMENT_TYPE.PAYPAL_ME) { - IOU.sendMoneyViaPaypal( - this.props.report, - amount, - currency, - comment, - this.props.currentUserPersonalDetails.login, - participant, - ); - return; - } - - if (paymentMethodType === CONST.IOU.PAYMENT_TYPE.EXPENSIFY) { - IOU.sendMoneyWithWallet( - this.props.report, - amount, - currency, - comment, - this.props.currentUserPersonalDetails.login, - participant, - ); - } - } - - /** - * @param {Array} selectedParticipants - */ - createTransaction(selectedParticipants) { - const reportID = lodashGet(this.props, 'route.params.reportID', ''); - const comment = this.state.comment.trim(); - - // IOUs created from a group report will have a reportID param in the route. - // Since the user is already viewing the report, we don't need to navigate them to the report - if (this.props.hasMultipleParticipants && CONST.REGEX.NUMBER.test(reportID)) { - IOU.splitBill( - selectedParticipants, - this.props.currentUserPersonalDetails.login, - this.state.amount, - comment, - this.props.iou.selectedCurrencyCode, - this.props.preferredLocale, - reportID, - ); - return; - } - - // If the IOU is created from the global create menu, we also navigate the user to the group report - if (this.props.hasMultipleParticipants) { - IOU.splitBillAndOpenReport( - selectedParticipants, - this.props.currentUserPersonalDetails.login, - this.state.amount, - comment, - this.props.iou.selectedCurrencyCode, - this.props.preferredLocale, - ); - return; - } - IOU.requestMoney( - this.props.report, - Math.round(this.state.amount * 100), - this.props.iou.selectedCurrencyCode, - this.props.currentUserPersonalDetails.login, - selectedParticipants[0], - comment, - ); - } - - renderHeader() { - return ( - - - {this.state.currentStepIndex > 0 - && ( - - - - - - - - )} -
- - - Navigation.dismissModal()} - style={[styles.touchableButtonImage]} - accessibilityRole="button" - accessibilityLabel={this.props.translate('common.close')} - > - - - - - - - ); - } - - render() { - const currentStep = this.steps[this.state.currentStepIndex]; - const reportID = lodashGet(this.props, 'route.params.reportID', ''); - return ( - - {({didScreenTransitionEnd, safeAreaPaddingBottomStyle}) => ( - <> - - {!didScreenTransitionEnd && } - {didScreenTransitionEnd && ( - <> - {currentStep === Steps.IOUAmount && ( - - {this.renderHeader()} - { - this.setState({amount}); - this.navigateToNextStep(); - }} - reportID={reportID} - hasMultipleParticipants={this.props.hasMultipleParticipants} - selectedAmount={this.state.amount} - navigation={this.props.navigation} - iouType={this.props.iouType} - /> - - )} - {currentStep === Steps.IOUParticipants && ( - - {this.renderHeader()} - - - )} - {currentStep === Steps.IOUConfirm && ( - - {this.renderHeader()} - { - // Prevent creating multiple transactions if the button is pressed repeatedly - if (this.creatingIOUTransaction) { - return; - } - this.creatingIOUTransaction = true; - this.createTransaction(selectedParticipants); - ReportScrollManager.scrollToBottom(); - }} - onSendMoney={(paymentMethodType) => { - if (this.creatingIOUTransaction) { - return; - } - this.creatingIOUTransaction = true; - this.sendMoney(paymentMethodType); - ReportScrollManager.scrollToBottom(); - }} - hasMultipleParticipants={this.props.hasMultipleParticipants} - participants={_.filter(this.state.participants, email => this.props.currentUserPersonalDetails.login !== email.login)} - iouAmount={this.state.amount} - comment={this.state.comment} - onUpdateComment={this.updateComment} - iouType={this.props.iouType} - - // The participants can only be modified when the action is initiated from directly within a group chat and not the floating-action-button. - // This is because when there is a group of people, say they are on a trip, and you have some shared expenses with some of the people, - // but not all of them (maybe someone skipped out on dinner). Then it's nice to be able to select/deselect people from the group chat bill - // split rather than forcing the user to create a new group, just for that expense. The reportID is empty, when the action was initiated from - // the floating-action-button (since it is something that exists outside the context of a report). - canModifyParticipants={!_.isEmpty(reportID)} - /> - - )} - - )} - - - )} - - ); - } -} - -IOUModal.propTypes = propTypes; -IOUModal.defaultProps = defaultProps; - -export default compose( - withLocalize, - withNetwork(), - withCurrentUserPersonalDetails, - withOnyx({ - report: { - key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${lodashGet(route, 'params.reportID', '')}`, - }, - iou: { - key: ONYXKEYS.IOU, - }, - personalDetails: { - key: ONYXKEYS.PERSONAL_DETAILS, - }, - }), -)(IOUModal); diff --git a/src/pages/iou/IOURequestPage.js b/src/pages/iou/IOURequestPage.js index 7b6e32ac9ab3..0d45ba531878 100644 --- a/src/pages/iou/IOURequestPage.js +++ b/src/pages/iou/IOURequestPage.js @@ -1,7 +1,7 @@ import React from 'react'; -import IOUModal from './IOUModal'; +import MoneyRequestModal from './MoneyRequestModal'; // eslint-disable-next-line react/jsx-props-no-spreading -const IOURequestPage = props => ; +const IOURequestPage = props => ; IOURequestPage.displayName = 'IOURequestPage'; export default IOURequestPage; diff --git a/src/pages/iou/IOUSendPage.js b/src/pages/iou/IOUSendPage.js index 79a83acaf8ea..c9aec6022293 100644 --- a/src/pages/iou/IOUSendPage.js +++ b/src/pages/iou/IOUSendPage.js @@ -1,8 +1,8 @@ import React from 'react'; import CONST from '../../CONST'; -import IOUModal from './IOUModal'; +import MoneyRequestModal from './MoneyRequestModal'; // eslint-disable-next-line react/jsx-props-no-spreading -const IOUSendPage = props => ; +const IOUSendPage = props => ; IOUSendPage.displayName = 'IOUSendPage'; export default IOUSendPage; diff --git a/src/pages/iou/ModalHeader.js b/src/pages/iou/ModalHeader.js new file mode 100644 index 000000000000..a89b184fb897 --- /dev/null +++ b/src/pages/iou/ModalHeader.js @@ -0,0 +1,74 @@ +import React from 'react'; +import {View, TouchableOpacity} from 'react-native'; +import PropTypes from 'prop-types'; +import styles from '../../styles/styles'; +import Icon from '../../components/Icon'; +import Header from '../../components/Header'; +import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize'; +import * as Expensicons from '../../components/Icon/Expensicons'; +import Tooltip from '../../components/Tooltip'; +import Navigation from '../../libs/Navigation/Navigation'; + +const propTypes = { + /** Title of the header */ + title: PropTypes.string.isRequired, + + /** Should we show the back button? */ + shouldShowBackButton: PropTypes.bool, + + /** Callback to fire on back button press */ + onBackButtonPress: PropTypes.func, + + ...withLocalizePropTypes, +}; + +const defaultProps = { + shouldShowBackButton: true, + onBackButtonPress: () => Navigation.goBack(), +}; + +const ModalHeader = props => ( + + + {props.shouldShowBackButton + && ( + + + + + + + + )} +
+ + + Navigation.dismissModal()} + style={[styles.touchableButtonImage]} + accessibilityRole="button" + accessibilityLabel={props.translate('common.close')} + > + + + + + + +); + +ModalHeader.displayName = 'ModalHeader'; +ModalHeader.propTypes = propTypes; +ModalHeader.defaultProps = defaultProps; +export default withLocalize(ModalHeader); diff --git a/src/pages/iou/MoneyRequestModal.js b/src/pages/iou/MoneyRequestModal.js new file mode 100644 index 000000000000..00e92b26cecc --- /dev/null +++ b/src/pages/iou/MoneyRequestModal.js @@ -0,0 +1,449 @@ +import _ from 'underscore'; +import React, { + useState, useEffect, useRef, useCallback, useMemo, +} from 'react'; +import {View} from 'react-native'; +import PropTypes from 'prop-types'; +import lodashGet from 'lodash/get'; +import {withOnyx} from 'react-native-onyx'; +import Str from 'expensify-common/lib/str'; +import IOUAmountPage from './steps/IOUAmountPage'; +import IOUParticipantsPage from './steps/IOUParticipantsPage/IOUParticipantsPage'; +import IOUConfirmPage from './steps/IOUConfirmPage'; +import ModalHeader from './ModalHeader'; +import styles from '../../styles/styles'; +import * as IOU from '../../libs/actions/IOU'; +import Navigation from '../../libs/Navigation/Navigation'; +import ONYXKEYS from '../../ONYXKEYS'; +import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize'; +import compose from '../../libs/compose'; +import * as OptionsListUtils from '../../libs/OptionsListUtils'; +import FullScreenLoadingIndicator from '../../components/FullscreenLoadingIndicator'; +import AnimatedStep from '../../components/AnimatedStep'; +import ScreenWrapper from '../../components/ScreenWrapper'; +import CONST from '../../CONST'; +import * as PersonalDetails from '../../libs/actions/PersonalDetails'; +import withCurrentUserPersonalDetails from '../../components/withCurrentUserPersonalDetails'; +import networkPropTypes from '../../components/networkPropTypes'; +import {withNetwork} from '../../components/OnyxProvider'; +import reportPropTypes from '../reportPropTypes'; +import * as ReportUtils from '../../libs/ReportUtils'; +import * as ReportScrollManager from '../../libs/ReportScrollManager'; + +/** + * A modal used for requesting money, splitting bills or sending money. + */ +const propTypes = { + /** Whether the request is for a single request or a group bill split */ + hasMultipleParticipants: PropTypes.bool, + + /** The type of IOU report, i.e. bill, request, send */ + iouType: PropTypes.string, + + /** The report passed via the route */ + // eslint-disable-next-line react/no-unused-prop-types + report: reportPropTypes, + + /** Information about the network */ + network: networkPropTypes.isRequired, + + // Holds data related to request view state, rather than the underlying request data. + iou: PropTypes.shape({ + /** Whether or not transaction creation has started */ + creatingIOUTransaction: PropTypes.bool, + + /** Whether or not transaction creation has resulted to error */ + error: PropTypes.bool, + + // Selected Currency Code of the current request + selectedCurrencyCode: PropTypes.string, + }), + + /** Personal details of all the users */ + personalDetails: PropTypes.shape({ + /** Primary login of participant */ + login: PropTypes.string, + + /** Display Name of participant */ + displayName: PropTypes.string, + + /** Avatar url of participant */ + avatar: PropTypes.string, + }), + + /** Personal details of the current user */ + currentUserPersonalDetails: PropTypes.shape({ + // Local Currency Code of the current user + localCurrencyCode: PropTypes.string, + }), + + ...withLocalizePropTypes, +}; + +const defaultProps = { + hasMultipleParticipants: false, + report: { + participants: [], + }, + iouType: CONST.IOU.IOU_TYPE.REQUEST, + currentUserPersonalDetails: { + localCurrencyCode: CONST.CURRENCY.USD, + }, + personalDetails: {}, + iou: { + creatingIOUTransaction: false, + error: false, + selectedCurrencyCode: null, + }, +}; + +// Determines type of step to display within Modal, value provides the title for that page. +const Steps = { + IOUAmount: 'iou.amount', + IOUParticipants: 'iou.participants', + IOUConfirm: 'iou.confirm', +}; + +const MoneyRequestModal = (props) => { + const reportParticipants = lodashGet(props, 'report.participants', []); + const participantsWithDetails = _.map(OptionsListUtils.getPersonalDetailsForLogins(reportParticipants, props.personalDetails), personalDetails => ({ + login: personalDetails.login, + text: personalDetails.displayName, + firstName: lodashGet(personalDetails, 'firstName', ''), + lastName: lodashGet(personalDetails, 'lastName', ''), + alternateText: Str.isSMSLogin(personalDetails.login) ? Str.removeSMSDomain(personalDetails.login) : personalDetails.login, + icons: [{ + source: ReportUtils.getAvatar(personalDetails.avatar, personalDetails.login), + name: personalDetails.login, + type: CONST.ICON_TYPE_AVATAR, + }], + keyForList: personalDetails.login, + payPalMeAddress: lodashGet(personalDetails, 'payPalMeAddress', ''), + phoneNumber: lodashGet(personalDetails, 'phoneNumber', ''), + })); + + // Skip IOUParticipants step if participants are passed in + const steps = reportParticipants.length ? [Steps.IOUAmount, Steps.IOUConfirm] : [Steps.IOUAmount, Steps.IOUParticipants, Steps.IOUConfirm]; + + const prevCreatingIOUTransactionStatusRef = useRef(lodashGet(props.iou, 'creatingIOUTransaction')); + const prevNetworkStatusRef = useRef(props.network.isOffline); + + const [previousStepIndex, setPreviousStepIndex] = useState(0); + const [currentStepIndex, setCurrentStepIndex] = useState(0); + const [participants, setParticipants] = useState(participantsWithDetails); + const [amount, setAmount] = useState(''); + const [comment, setComment] = useState(''); + + useEffect(() => { + PersonalDetails.openMoneyRequestModalPage(); + IOU.setIOUSelectedCurrency(props.currentUserPersonalDetails.localCurrencyCode); + // eslint-disable-next-line react-hooks/exhaustive-deps -- props.currentUserPersonalDetails will always exist from Onyx and we don't want this effect to run again + }, []); + + useEffect(() => { + // We only want to check if we just finished creating an IOU transaction + // We check it within this effect because we're sending the request optimistically but if an error occurs from the API, we will update the iou state with the error later + if (!prevCreatingIOUTransactionStatusRef.current || lodashGet(props.iou, 'creatingIOUTransaction')) { + return; + } + + if (lodashGet(props.iou, 'error') === true) { + setCurrentStepIndex(0); + } else { + Navigation.dismissModal(); + } + }, [props.iou]); + + useEffect(() => { + if (props.network.isOffline || !prevNetworkStatusRef.current) { + return; + } + + // User came back online, so let's refetch the currency details based on location + PersonalDetails.openMoneyRequestModalPage(); + }, [props.network.isOffline]); + + useEffect(() => { + // Used to store previous prop values to compare on next render + prevNetworkStatusRef.current = props.network.isOffline; + prevCreatingIOUTransactionStatusRef.current = lodashGet(props.iou, 'creatingIOUTransaction'); + }); + + /** + * Decides our animation type based on whether we're increasing or decreasing + * our step index. + * @returns {String|null} + */ + const direction = useMemo(() => { + if (previousStepIndex < currentStepIndex) { + return 'in'; + } + if (previousStepIndex > currentStepIndex) { + return 'out'; + } + + // Doesn't animate the step when first opening the modal + if (previousStepIndex === currentStepIndex) { + return null; + } + }, [previousStepIndex, currentStepIndex]); + + /** + * Retrieve title for current step, based upon current step and type of request + * + * @returns {String} + */ + const titleForStep = useMemo(() => { + const isSendingMoney = props.iouType === CONST.IOU.IOU_TYPE.SEND; + if (currentStepIndex === 1 || currentStepIndex === 2) { + const formattedAmount = props.numberFormat( + amount, { + style: 'currency', + currency: props.iou.selectedCurrencyCode, + }, + ); + if (isSendingMoney) { + return props.translate('iou.send', { + amount: formattedAmount, + }); + } + return props.translate( + props.hasMultipleParticipants ? 'iou.split' : 'iou.request', { + amount: formattedAmount, + }, + ); + } + if (currentStepIndex === 0) { + if (isSendingMoney) { + return props.translate('iou.sendMoney'); + } + return props.translate(props.hasMultipleParticipants ? 'iou.splitBill' : 'iou.requestMoney'); + } + + return props.translate(steps[currentStepIndex]) || ''; + // eslint-disable-next-line react-hooks/exhaustive-deps -- props does not need to be a dependency as it will always exist + }, [amount, currentStepIndex, props.hasMultipleParticipants, props.iou.selectedCurrencyCode, props.iouType, props.numberFormat, steps]); + + /** + * Navigate to the previous request step if possible + */ + const navigateToPreviousStep = useCallback(() => { + if (currentStepIndex <= 0) { + return; + } + + setPreviousStepIndex(currentStepIndex); + setCurrentStepIndex(currentStepIndex - 1); + }, [currentStepIndex]); + + /** + * Navigate to the next request step if possible + */ + const navigateToNextStep = useCallback(() => { + if (currentStepIndex >= steps.length - 1) { + return; + } + + setPreviousStepIndex(currentStepIndex); + setCurrentStepIndex(currentStepIndex + 1); + }, [currentStepIndex, steps.length]); + + /** + * Checks if user has a GOLD wallet then creates a paid IOU report on the fly + * + * @param {String} paymentMethodType + */ + const sendMoney = useCallback((paymentMethodType) => { + const amountInDollars = Math.round(amount * 100); + const currency = props.iou.selectedCurrencyCode; + const trimmedComment = comment.trim(); + const participant = participants[0]; + + if (paymentMethodType === CONST.IOU.PAYMENT_TYPE.ELSEWHERE) { + IOU.sendMoneyElsewhere( + props.report, + amountInDollars, + currency, + trimmedComment, + props.currentUserPersonalDetails.login, + participant, + ); + return; + } + + if (paymentMethodType === CONST.IOU.PAYMENT_TYPE.PAYPAL_ME) { + IOU.sendMoneyViaPaypal( + props.report, + amountInDollars, + currency, + trimmedComment, + props.currentUserPersonalDetails.login, + participant, + ); + return; + } + + if (paymentMethodType === CONST.IOU.PAYMENT_TYPE.EXPENSIFY) { + IOU.sendMoneyWithWallet( + props.report, + amountInDollars, + currency, + trimmedComment, + props.currentUserPersonalDetails.login, + participant, + ); + } + }, [amount, comment, participants, props.currentUserPersonalDetails.login, props.iou.selectedCurrencyCode, props.report]); + + /** + * @param {Array} selectedParticipants + */ + const createTransaction = useCallback((selectedParticipants) => { + const reportID = lodashGet(props.route, 'params.reportID', ''); + const trimmedComment = comment.trim(); + + // IOUs created from a group report will have a reportID param in the route. + // Since the user is already viewing the report, we don't need to navigate them to the report + if (props.hasMultipleParticipants && CONST.REGEX.NUMBER.test(reportID)) { + IOU.splitBill( + selectedParticipants, + props.currentUserPersonalDetails.login, + amount, + trimmedComment, + props.iou.selectedCurrencyCode, + props.preferredLocale, + reportID, + ); + return; + } + + // If the request is created from the global create menu, we also navigate the user to the group report + if (props.hasMultipleParticipants) { + IOU.splitBillAndOpenReport( + selectedParticipants, + props.currentUserPersonalDetails.login, + amount, + trimmedComment, + props.iou.selectedCurrencyCode, + props.preferredLocale, + ); + return; + } + IOU.requestMoney( + props.report, + Math.round(amount * 100), + props.iou.selectedCurrencyCode, + props.currentUserPersonalDetails.login, + selectedParticipants[0], + trimmedComment, + ); + }, [amount, comment, props.currentUserPersonalDetails.login, props.hasMultipleParticipants, props.iou.selectedCurrencyCode, props.preferredLocale, props.report, props.route]); + + const currentStep = steps[currentStepIndex]; + const reportID = lodashGet(props, 'route.params.reportID', ''); + const shouldShowBackButton = currentStepIndex > 0; + const modalHeader = ; + return ( + + {({didScreenTransitionEnd, safeAreaPaddingBottomStyle}) => ( + <> + + {!didScreenTransitionEnd && } + {didScreenTransitionEnd && ( + <> + {currentStep === Steps.IOUAmount && ( + + {modalHeader} + { + setAmount(value); + navigateToNextStep(); + }} + reportID={reportID} + hasMultipleParticipants={props.hasMultipleParticipants} + selectedAmount={amount} + navigation={props.navigation} + iouType={props.iouType} + /> + + )} + {currentStep === Steps.IOUParticipants && ( + + {modalHeader} + setParticipants(selectedParticipants)} + onStepComplete={navigateToNextStep} + safeAreaPaddingBottomStyle={safeAreaPaddingBottomStyle} + /> + + )} + {currentStep === Steps.IOUConfirm && ( + + {modalHeader} + { + // TODO: ADD HANDLING TO DISABLE BUTTON FUNCTIONALITY WHILE REQUEST IS IN FLIGHT + createTransaction(selectedParticipants); + ReportScrollManager.scrollToBottom(); + }} + onSendMoney={(paymentMethodType) => { + // TODO: ADD HANDLING TO DISABLE BUTTON FUNCTIONALITY WHILE REQUEST IS IN FLIGHT + sendMoney(paymentMethodType); + ReportScrollManager.scrollToBottom(); + }} + hasMultipleParticipants={props.hasMultipleParticipants} + participants={_.filter(participants, email => props.currentUserPersonalDetails.login !== email.login)} + iouAmount={amount} + comment={comment} + onUpdateComment={value => setComment(value)} + iouType={props.iouType} + + // The participants can only be modified when the action is initiated from directly within a group chat and not the floating-action-button. + // This is because when there is a group of people, say they are on a trip, and you have some shared expenses with some of the people, + // but not all of them (maybe someone skipped out on dinner). Then it's nice to be able to select/deselect people from the group chat bill + // split rather than forcing the user to create a new group, just for that expense. The reportID is empty, when the action was initiated from + // the floating-action-button (since it is something that exists outside the context of a report). + canModifyParticipants={!_.isEmpty(reportID)} + /> + + )} + + )} + + + )} + + ); +}; + +MoneyRequestModal.displayName = 'MoneyRequestModal'; +MoneyRequestModal.propTypes = propTypes; +MoneyRequestModal.defaultProps = defaultProps; + +export default compose( + withLocalize, + withNetwork(), + withCurrentUserPersonalDetails, + withOnyx({ + report: { + key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${lodashGet(route, 'params.reportID', '')}`, + }, + iou: { + key: ONYXKEYS.IOU, + }, + personalDetails: { + key: ONYXKEYS.PERSONAL_DETAILS, + }, + }), +)(MoneyRequestModal); diff --git a/src/pages/iou/steps/IOUConfirmPage.js b/src/pages/iou/steps/IOUConfirmPage.js index ea321b0e0a9b..f176f73f93fb 100644 --- a/src/pages/iou/steps/IOUConfirmPage.js +++ b/src/pages/iou/steps/IOUConfirmPage.js @@ -11,10 +11,10 @@ const propTypes = { /** Callback to to parent modal to send money */ onSendMoney: PropTypes.func.isRequired, - /** Callback to update comment from IOUModal */ + /** Callback to update comment from MoneyRequestModal */ onUpdateComment: PropTypes.func, - /** Comment value from IOUModal */ + /** Comment value from MoneyRequestModal */ comment: PropTypes.string, /** Should we request a single or multiple participant selection from user */ @@ -23,7 +23,7 @@ const propTypes = { /** IOU amount */ iouAmount: PropTypes.string.isRequired, - /** Selected participants from IOUMOdal with login */ + /** Selected participants from MoneyRequestModal with login */ participants: PropTypes.arrayOf(PropTypes.shape({ login: PropTypes.string.isRequired, alternateText: PropTypes.string, diff --git a/src/pages/iou/steps/IOUParticipantsPage/IOUParticipantsPage.js b/src/pages/iou/steps/IOUParticipantsPage/IOUParticipantsPage.js index 2d948552611e..93a27f88946f 100644 --- a/src/pages/iou/steps/IOUParticipantsPage/IOUParticipantsPage.js +++ b/src/pages/iou/steps/IOUParticipantsPage/IOUParticipantsPage.js @@ -16,10 +16,10 @@ const propTypes = { /** Should we request a single or multiple participant selection from user */ hasMultipleParticipants: PropTypes.bool.isRequired, - /** Callback to add participants in IOUModal */ + /** Callback to add participants in MoneyRequestModal */ onAddParticipants: PropTypes.func.isRequired, - /** Selected participants from IOUModal with login */ + /** Selected participants from MoneyRequestModal with login */ participants: PropTypes.arrayOf(PropTypes.shape({ login: PropTypes.string.isRequired, alternateText: PropTypes.string, diff --git a/src/pages/iou/steps/IOUParticipantsPage/IOUParticipantsRequest.js b/src/pages/iou/steps/IOUParticipantsPage/IOUParticipantsRequest.js index 8d094f3cf466..8929889884e7 100755 --- a/src/pages/iou/steps/IOUParticipantsPage/IOUParticipantsRequest.js +++ b/src/pages/iou/steps/IOUParticipantsPage/IOUParticipantsRequest.js @@ -18,7 +18,7 @@ const propTypes = { /** Callback to inform parent modal of success */ onStepComplete: PropTypes.func.isRequired, - /** Callback to add participants in IOUModal */ + /** Callback to add participants in MoneyRequestModal */ onAddParticipants: PropTypes.func.isRequired, /** All of the personal details for everyone */ diff --git a/src/pages/iou/steps/IOUParticipantsPage/IOUParticipantsSplit.js b/src/pages/iou/steps/IOUParticipantsPage/IOUParticipantsSplit.js index fed5ed23c072..4d7c05c461e1 100755 --- a/src/pages/iou/steps/IOUParticipantsPage/IOUParticipantsSplit.js +++ b/src/pages/iou/steps/IOUParticipantsPage/IOUParticipantsSplit.js @@ -22,10 +22,10 @@ const propTypes = { /** Callback to inform parent modal of success */ onStepComplete: PropTypes.func.isRequired, - /** Callback to add participants in IOUModal */ + /** Callback to add participants in MoneyRequestModal */ onAddParticipants: PropTypes.func.isRequired, - /** Selected participants from IOUModal with login */ + /** Selected participants from MoneyRequestModal with login */ participants: PropTypes.arrayOf(PropTypes.shape({ login: PropTypes.string.isRequired, alternateText: PropTypes.string,