diff --git a/src/ROUTES.js b/src/ROUTES.js index 966c3d0c5a1a..eb436284cfba 100644 --- a/src/ROUTES.js +++ b/src/ROUTES.js @@ -9,9 +9,6 @@ const REPORT = 'r'; const IOU_REQUEST = 'request/new'; const IOU_BILL = 'split/new'; const IOU_SEND = 'send/new'; -const IOU_REQUEST_CURRENCY = `${IOU_REQUEST}/currency`; -const IOU_BILL_CURRENCY = `${IOU_BILL}/currency`; -const IOU_SEND_CURRENCY = `${IOU_SEND}/currency`; const NEW_TASK = 'new/task'; const SETTINGS_PERSONAL_DETAILS = 'settings/profile/personal-details'; const SETTINGS_CONTACT_METHODS = 'settings/profile/contact-methods'; @@ -77,22 +74,23 @@ export default { IOU_REQUEST, IOU_BILL, IOU_SEND, - IOU_REQUEST_WITH_REPORT_ID: `${IOU_REQUEST}/:reportID?`, - IOU_BILL_WITH_REPORT_ID: `${IOU_BILL}/:reportID?`, - IOU_SEND_WITH_REPORT_ID: `${IOU_SEND}/:reportID?`, - getIouRequestRoute: (reportID) => `${IOU_REQUEST}/${reportID}`, - getIouSplitRoute: (reportID) => `${IOU_BILL}/${reportID}`, - getIOUSendRoute: (reportID) => `${IOU_SEND}/${reportID}`, - IOU_BILL_CURRENCY: `${IOU_BILL_CURRENCY}/:reportID?`, - IOU_REQUEST_CURRENCY: `${IOU_REQUEST_CURRENCY}/:reportID?`, - MONEY_REQUEST_DESCRIPTION: `${IOU_REQUEST}/description`, - IOU_SEND_CURRENCY: `${IOU_SEND_CURRENCY}/:reportID?`, + + // To see the available iouType, please refer to CONST.IOU.MONEY_REQUEST_TYPE + MONEY_REQUEST: ':iouType/new/:reportID?', + MONEY_REQUEST_AMOUNT: ':iouType/new/amount/:reportID?', + MONEY_REQUEST_PARTICIPANTS: ':iouType/new/participants/:reportID?', + MONEY_REQUEST_CONFIRMATION: ':iouType/new/confirmation/:reportID?', + MONEY_REQUEST_CURRENCY: ':iouType/new/currency/:reportID?', + MONEY_REQUEST_DESCRIPTION: ':iouType/new/description/:reportID?', IOU_SEND_ADD_BANK_ACCOUNT: `${IOU_SEND}/add-bank-account`, IOU_SEND_ADD_DEBIT_CARD: `${IOU_SEND}/add-debit-card`, IOU_SEND_ENABLE_PAYMENTS: `${IOU_SEND}/enable-payments`, - getIouRequestCurrencyRoute: (reportID, currency, backTo) => `${IOU_REQUEST_CURRENCY}/${reportID}?currency=${currency}&backTo=${backTo}`, - getIouBillCurrencyRoute: (reportID, currency, backTo) => `${IOU_BILL_CURRENCY}/${reportID}?currency=${currency}&backTo=${backTo}`, - getIouSendCurrencyRoute: (reportID, currency, backTo) => `${IOU_SEND_CURRENCY}/${reportID}?currency=${currency}&backTo=${backTo}`, + getMoneyRequestRoute: (iouType, reportID = '') => `${iouType}/new/${reportID}`, + getMoneyRequestAmountRoute: (iouType, reportID = '') => `${iouType}/new/amount/${reportID}`, + getMoneyRequestParticipantsRoute: (iouType, reportID = '') => `${iouType}/new/participants/${reportID}`, + getMoneyRequestConfirmationRoute: (iouType, reportID = '') => `${iouType}/new/confirmation/${reportID}`, + getMoneyRequestCurrencyRoute: (iouType, reportID = '', currency, backTo) => `${iouType}/new/currency/${reportID}?currency=${currency}&backTo=${backTo}`, + getMoneyRequestDescriptionRoute: (iouType, reportID = '') => `${iouType}/new/description/${reportID}`, SPLIT_BILL_DETAILS: `r/:reportID/split/:reportActionID`, getSplitBillDetailsRoute: (reportID, reportActionID) => `r/${reportID}/split/${reportActionID}`, getNewTaskRoute: (reportID) => `${NEW_TASK}/${reportID}`, diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js index 05d56f390b2a..7b4f31e57d25 100755 --- a/src/components/MoneyRequestConfirmationList.js +++ b/src/components/MoneyRequestConfirmationList.js @@ -28,6 +28,9 @@ const propTypes = { /** Callback to parent modal to send money */ onSendMoney: PropTypes.func, + /** Callback to inform a participant is selected */ + onSelectParticipant: PropTypes.func, + /** Should we request a single or multiple participant selection from user */ hasMultipleParticipants: PropTypes.bool.isRequired, @@ -66,31 +69,22 @@ const propTypes = { /* Onyx Props */ - /** Holds data related to IOU view state, rather than the underlying IOU data. */ - iou: PropTypes.shape({ - /** Whether or not the IOU step is loading (creating the IOU Report) */ - loading: PropTypes.bool, - }), - /** Current user session */ session: PropTypes.shape({ email: PropTypes.string.isRequired, }), - /** Callback function to navigate to a provided step in the MoneyRequestModal flow */ - navigateToStep: PropTypes.func, - /** The policyID of the request */ policyID: PropTypes.string, + + /** The reportID of the request */ + reportID: PropTypes.string, }; const defaultProps = { onConfirm: () => {}, onSendMoney: () => {}, - navigateToStep: () => {}, - iou: { - loading: false, - }, + onSelectParticipant: () => {}, iouType: CONST.IOU.MONEY_REQUEST_TYPE.REQUEST, payeePersonalDetails: null, canModifyParticipants: false, @@ -100,13 +94,14 @@ const defaultProps = { email: null, }, policyID: '', + reportID: '', ...withCurrentUserPersonalDetailsDefaultProps, }; function MoneyRequestConfirmationList(props) { // Destructure functions from props to pass it as a dependecy to useCallback/useMemo hooks. // Prop functions pass props itself as a "this" value to the function which means they change every time props change. - const {translate, onSendMoney, onConfirm} = props; + const {translate, onSendMoney, onConfirm, onSelectParticipant} = props; /** * Returns the participants with amount @@ -121,13 +116,6 @@ function MoneyRequestConfirmationList(props) { [props.iouAmount, props.iouCurrencyCode], ); - const getFormattedParticipants = () => - _.map(getParticipantsWithAmount(props.participants), (participant) => ({ - ...participant, - selected: true, - })); - - const [participants, setParticipants] = useState(getFormattedParticipants); const [didConfirm, setDidConfirm] = useState(false); const splitOrRequestOptions = useMemo(() => { @@ -142,17 +130,15 @@ function MoneyRequestConfirmationList(props) { ]; }, [props.hasMultipleParticipants, props.iouAmount, props.iouCurrencyCode, translate]); - const selectedParticipants = useMemo(() => _.filter(participants, (participant) => participant.selected), [participants]); - const getParticipantsWithoutAmount = useCallback((participantsList) => _.map(participantsList, (option) => _.omit(option, 'descriptiveText')), []); + const selectedParticipants = useMemo(() => _.filter(props.participants, (participant) => participant.selected), [props.participants]); const payeePersonalDetails = useMemo(() => props.payeePersonalDetails || props.currentUserPersonalDetails, [props.payeePersonalDetails, props.currentUserPersonalDetails]); const optionSelectorSections = useMemo(() => { const sections = []; - const unselectedParticipants = _.filter(participants, (participant) => !participant.selected); + const unselectedParticipants = _.filter(props.participants, (participant) => !participant.selected); if (props.hasMultipleParticipants) { const formattedSelectedParticipants = getParticipantsWithAmount(selectedParticipants); - const formattedUnselectedParticipants = getParticipantsWithoutAmount(unselectedParticipants); - const formattedParticipantsList = _.union(formattedSelectedParticipants, formattedUnselectedParticipants); + const formattedParticipantsList = _.union(formattedSelectedParticipants, unselectedParticipants); const myIOUAmount = IOUUtils.calculateAmount(selectedParticipants.length, props.iouAmount, true); const formattedPayeeOption = OptionsListUtils.getIOUConfirmationOptionsFromPayeePersonalDetail( @@ -175,27 +161,15 @@ function MoneyRequestConfirmationList(props) { }, ); } else { - const formattedParticipantsList = getParticipantsWithoutAmount(props.participants); sections.push({ title: translate('common.to'), - data: formattedParticipantsList, + data: props.participants, shouldShow: true, indexOffset: 0, }); } return sections; - }, [ - selectedParticipants, - getParticipantsWithAmount, - getParticipantsWithoutAmount, - props.hasMultipleParticipants, - props.iouAmount, - props.iouCurrencyCode, - props.participants, - participants, - translate, - payeePersonalDetails, - ]); + }, [selectedParticipants, getParticipantsWithAmount, props.hasMultipleParticipants, props.iouAmount, props.iouCurrencyCode, props.participants, translate, payeePersonalDetails]); const selectedOptions = useMemo(() => { if (!props.hasMultipleParticipants) { @@ -205,27 +179,17 @@ function MoneyRequestConfirmationList(props) { }, [selectedParticipants, props.hasMultipleParticipants, payeePersonalDetails]); /** - * Toggle selected option's selected prop. * @param {Object} option */ - const toggleOption = useCallback( + const selectParticipant = useCallback( (option) => { // Return early if selected option is currently logged in user. if (option.accountID === props.session.accountID) { return; } - - setParticipants((prevParticipants) => { - const newParticipants = _.map(prevParticipants, (participant) => { - if (participant.accountID === option.accountID) { - return {...participant, selected: !participant.selected}; - } - return participant; - }); - return newParticipants; - }); + onSelectParticipant(option); }, - [props.session.accountID], + [props.session.accountID, onSelectParticipant], ); /** @@ -274,7 +238,7 @@ function MoneyRequestConfirmationList(props) { const shouldShowSettlementButton = props.iouType === CONST.IOU.MONEY_REQUEST_TYPE.SEND; const shouldDisableButton = selectedParticipants.length === 0; - const recipient = participants[0]; + const recipient = props.participants[0] || {}; return shouldShowSettlementButton ? ( ); - }, [confirm, participants, props.bankAccountRoute, props.iouCurrencyCode, props.iouType, props.isReadOnly, props.policyID, selectedParticipants, splitOrRequestOptions]); + }, [confirm, props.participants, props.bankAccountRoute, props.iouCurrencyCode, props.iouType, props.isReadOnly, props.policyID, selectedParticipants, splitOrRequestOptions]); return ( props.navigateToStep(0)} + onPress={() => Navigation.navigate(ROUTES.getMoneyRequestAmountRoute(props.iouType, props.reportID))} style={[styles.moneyRequestMenuItem, styles.mt2]} titleStyle={styles.moneyRequestConfirmationAmount} disabled={didConfirm || props.isReadOnly} @@ -327,7 +291,7 @@ function MoneyRequestConfirmationList(props) { shouldShowRightIcon={!props.isReadOnly} title={props.iouComment} description={translate('common.description')} - onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_DESCRIPTION)} + onPress={() => Navigation.navigate(ROUTES.getMoneyRequestDescriptionRoute(props.iouType, props.reportID))} style={[styles.moneyRequestMenuItem, styles.mb2]} disabled={didConfirm || props.isReadOnly} /> @@ -343,7 +307,6 @@ export default compose( withWindowDimensions, withCurrentUserPersonalDetails, withOnyx({ - iou: {key: ONYXKEYS.IOU}, session: { key: ONYXKEYS.SESSION, }, diff --git a/src/libs/IOUUtils.js b/src/libs/IOUUtils.js index 7f7782dee996..371243345eb6 100644 --- a/src/libs/IOUUtils.js +++ b/src/libs/IOUUtils.js @@ -121,4 +121,13 @@ function isIOUReportPendingCurrencyConversion(reportActions, iouReport) { return hasPendingRequests; } -export {calculateAmount, updateIOUOwnerAndTotal, getIOUReportActions, isIOUReportPendingCurrencyConversion}; +/** + * Checks if the iou type is one of request, send, or split. + * @param {String} iouType + * @returns {Boolean} + */ +function isValidMoneyRequestType(iouType) { + return [CONST.IOU.MONEY_REQUEST_TYPE.REQUEST, CONST.IOU.MONEY_REQUEST_TYPE.SEND, CONST.IOU.MONEY_REQUEST_TYPE.SPLIT].includes(iouType); +} + +export {calculateAmount, updateIOUOwnerAndTotal, getIOUReportActions, isIOUReportPendingCurrencyConversion, isValidMoneyRequestType}; diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js index b983ffd14968..03d415250fdc 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js @@ -32,37 +32,41 @@ function createModalStackNavigator(screens) { } // We use getComponent/require syntax so that file used by screens are not loaded until we need them. -const IOUBillStackNavigator = createModalStackNavigator([ +const MoneyRequestModalStackNavigator = createModalStackNavigator([ { getComponent: () => { - const IOUBillPage = require('../../../pages/iou/IOUBillPage').default; - return IOUBillPage; + const MoneyRequestAmountPage = require('../../../pages/iou/steps/MoneyRequestAmountPage').default; + return MoneyRequestAmountPage; }, - name: 'IOU_Bill_Root', + name: 'Money_Request', }, { getComponent: () => { - const IOUCurrencySelection = require('../../../pages/iou/IOUCurrencySelection').default; - return IOUCurrencySelection; + const MoneyRequestEditAmountPage = require('../../../pages/iou/steps/MoneyRequestAmountPage').default; + return MoneyRequestEditAmountPage; }, - name: 'IOU_Bill_Currency', + name: 'Money_Request_Amount', + }, + { + getComponent: () => { + const MoneyRequestParticipantsPage = require('../../../pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage').default; + return MoneyRequestParticipantsPage; + }, + name: 'Money_Request_Participants', }, -]); - -const IOURequestModalStackNavigator = createModalStackNavigator([ { getComponent: () => { - const IOURequestPage = require('../../../pages/iou/IOURequestPage').default; - return IOURequestPage; + const MoneyRequestConfirmPage = require('../../../pages/iou/steps/MoneyRequestConfirmPage').default; + return MoneyRequestConfirmPage; }, - name: 'IOU_Request_Root', + name: 'Money_Request_Confirmation', }, { getComponent: () => { const IOUCurrencySelection = require('../../../pages/iou/IOUCurrencySelection').default; return IOUCurrencySelection; }, - name: 'IOU_Request_Currency', + name: 'Money_Request_Currency', }, { getComponent: () => { @@ -71,23 +75,6 @@ const IOURequestModalStackNavigator = createModalStackNavigator([ }, name: 'Money_Request_Description', }, -]); - -const IOUSendModalStackNavigator = createModalStackNavigator([ - { - getComponent: () => { - const IOUSendPage = require('../../../pages/iou/IOUSendPage').default; - return IOUSendPage; - }, - name: 'IOU_Send_Root', - }, - { - getComponent: () => { - const IOUCurrencySelection = require('../../../pages/iou/IOUCurrencySelection').default; - return IOUCurrencySelection; - }, - name: 'IOU_Send_Currency', - }, { getComponent: () => { const AddPersonalBankAccountPage = require('../../../pages/AddPersonalBankAccountPage').default; @@ -730,9 +717,7 @@ const FlagCommentStackNavigator = createModalStackNavigator([ ]); export { - IOUBillStackNavigator, - IOURequestModalStackNavigator, - IOUSendModalStackNavigator, + MoneyRequestModalStackNavigator, SplitDetailsModalStackNavigator, DetailsModalStackNavigator, ProfileModalStackNavigator, diff --git a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.js b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.js index cb82795936c2..ec9902170bef 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.js +++ b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.js @@ -60,9 +60,9 @@ function RigthModalNavigator() { component={ModalStackNavigators.ReportParticipantsModalStackNavigator} /> - - policyExpenseReport.policyID === report.policyID && policyExpenseReport.isOwnPolicyExpenseChat, - ); - return _.map(filteredPolicyExpenseReports, (expenseReport) => { - const policyExpenseChatAvatarSource = ReportUtils.getWorkspaceAvatar(expenseReport); - return { + const expenseReport = policyExpenseReports[`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]; + const policyExpenseChatAvatarSource = ReportUtils.getWorkspaceAvatar(expenseReport); + return [ + { ...expenseReport, keyForList: expenseReport.policyID, text: expenseReport.displayName, @@ -130,8 +124,10 @@ function getPolicyExpenseReportOptions(report) { type: CONST.ICON_TYPE_WORKSPACE, }, ], - }; - }); + selected: report.selected, + isPolicyExpenseChat: true, + }, + ]; } /** @@ -209,31 +205,37 @@ function isPersonalDetailsReady(personalDetails) { /** * Get the participant options for a report. - * @param {Object} report + * @param {Array} participants * @param {Array} personalDetails * @returns {Array} */ -function getParticipantsOptions(report, personalDetails) { - const participants = lodashGet(report, 'participantAccountIDs', []); - return _.map(getPersonalDetailsForAccountIDs(participants, personalDetails), (details) => ({ - keyForList: String(details.accountID), - login: details.login, - accountID: details.accountID, - text: details.displayName, - firstName: lodashGet(details, 'firstName', ''), - lastName: lodashGet(details, 'lastName', ''), - alternateText: Str.isSMSLogin(details.login || '') ? LocalePhoneNumber.formatPhoneNumber(details.login) : details.login || details.displayName, - icons: [ - { - source: UserUtils.getAvatar(details.avatar, details.accountID), - name: details.login, - type: CONST.ICON_TYPE_AVATAR, - id: details.accountID, - }, - ], - payPalMeAddress: lodashGet(details, 'payPalMeAddress', ''), - phoneNumber: lodashGet(details, 'phoneNumber', ''), - })); +function getParticipantsOptions(participants, personalDetails) { + const details = getPersonalDetailsForAccountIDs(_.pluck(participants, 'accountID'), personalDetails); + return _.map(participants, (participant) => { + const detail = details[participant.accountID]; + const login = detail.login || participant.login; + const displayName = detail.displayName || LocalePhoneNumber.formatPhoneNumber(login); + return { + keyForList: String(detail.accountID), + login, + accountID: detail.accountID, + text: displayName, + firstName: lodashGet(detail, 'firstName', ''), + lastName: lodashGet(detail, 'lastName', ''), + alternateText: LocalePhoneNumber.formatPhoneNumber(login) || displayName, + icons: [ + { + source: UserUtils.getAvatar(detail.avatar, detail.accountID), + name: login, + type: CONST.ICON_TYPE_AVATAR, + id: detail.accountID, + }, + ], + payPalMeAddress: lodashGet(detail, 'payPalMeAddress', ''), + phoneNumber: lodashGet(detail, 'phoneNumber', ''), + selected: participant.selected, + }; + }); } /** diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index eecd26069724..34d6924dff54 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -3,6 +3,7 @@ import _ from 'underscore'; import lodashGet from 'lodash/get'; import Str from 'expensify-common/lib/str'; import CONST from '../../CONST'; +import ROUTES from '../../ROUTES'; import ONYXKEYS from '../../ONYXKEYS'; import Navigation from '../Navigation/Navigation'; import * as Localize from '../Localize'; @@ -45,6 +46,36 @@ Onyx.connect({ }, }); +let userAccountID = ''; +Onyx.connect({ + key: ONYXKEYS.SESSION, + callback: (val) => { + userAccountID = lodashGet(val, 'accountID', ''); + }, +}); + +let currentUserPersonalDetails = {}; +Onyx.connect({ + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + callback: (val) => { + currentUserPersonalDetails = lodashGet(val, userAccountID, {}); + }, +}); + +/** + * Reset money request info from the store with its initial value + * @param {String} id + */ +function resetMoneyRequestInfo(id = '') { + Onyx.merge(ONYXKEYS.IOU, { + id, + amount: 0, + currency: lodashGet(currentUserPersonalDetails, 'localCurrencyCode', CONST.CURRENCY.USD), + comment: '', + participants: [], + }); +} + function buildOnyxDataForMoneyRequest( chatReport, iouReport, @@ -281,7 +312,7 @@ function buildOnyxDataForMoneyRequest( function requestMoney(report, amount, currency, payeeEmail, payeeAccountID, participant, comment) { const payerEmail = OptionsListUtils.addSMSDomainIfPhoneNumber(participant.login); const payerAccountID = Number(participant.accountID); - const isPolicyExpenseChat = participant.isPolicyExpenseChat || participant.isOwnPolicyExpenseChat; + const isPolicyExpenseChat = participant.isPolicyExpenseChat; // STEP 1: Get existing chat report OR build a new optimistic one let isNewChatReport = false; @@ -394,6 +425,7 @@ function requestMoney(report, amount, currency, payeeEmail, payeeAccountID, part }, {optimisticData, successData, failureData}, ); + resetMoneyRequestInfo(); Navigation.dismissModal(chatReport.reportID); } @@ -699,6 +731,7 @@ function splitBill(participants, currentUserLogin, currentUserAccountID, amount, onyxData, ); + resetMoneyRequestInfo(); Navigation.dismissModal(); } @@ -728,6 +761,7 @@ function splitBillAndOpenReport(participants, currentUserLogin, currentUserAccou onyxData, ); + resetMoneyRequestInfo(); Navigation.dismissModal(groupData.chatReportID); } @@ -839,24 +873,6 @@ function deleteMoneyRequest(chatReportID, iouReportID, moneyRequestAction, shoul } } -/** - * Sets IOU'S selected currency - * - * @param {String} selectedCurrencyCode - */ -function setIOUSelectedCurrency(selectedCurrencyCode) { - Onyx.merge(ONYXKEYS.IOU, {selectedCurrencyCode}); -} - -/** - * Sets Money Request description - * - * @param {String} comment - */ -function setMoneyRequestDescription(comment) { - Onyx.merge(ONYXKEYS.IOU, {comment: comment.trim()}); -} - /** * @param {Number} amount * @param {String} submitterPayPalMeAddress @@ -1209,6 +1225,7 @@ function sendMoneyElsewhere(report, amount, currency, comment, managerID, recipi API.write('SendMoneyElsewhere', params, {optimisticData, successData, failureData}); + resetMoneyRequestInfo(); Navigation.dismissModal(params.chatReportID); } @@ -1225,6 +1242,7 @@ function sendMoneyWithWallet(report, amount, currency, comment, managerID, recip API.write('SendMoneyWithWallet', params, {optimisticData, successData, failureData}); + resetMoneyRequestInfo(); Navigation.dismissModal(params.chatReportID); } @@ -1241,6 +1259,7 @@ function sendMoneyViaPaypal(report, amount, currency, comment, managerID, recipi API.write('SendMoneyViaPaypal', params, {optimisticData, successData, failureData}); + resetMoneyRequestInfo(); Navigation.dismissModal(params.chatReportID); asyncOpenURL(Promise.resolve(), buildPayPalPaymentUrl(amount, recipient.payPalMeAddress, currency)); @@ -1271,6 +1290,51 @@ function payMoneyRequest(paymentType, chatReport, iouReport) { } } +/** + * Initialize money request info and navigate to the MoneyRequest page + * @param {String} iouType + * @param {String} reportID + */ +function startMoneyRequest(iouType, reportID = '') { + resetMoneyRequestInfo(`${iouType}${reportID}`); + Navigation.navigate(ROUTES.getMoneyRequestRoute(iouType, reportID)); +} + +/** + * @param {String} id + */ +function setMoneyRequestId(id) { + Onyx.merge(ONYXKEYS.IOU, {id}); +} + +/** + * @param {Number} amount + */ +function setMoneyRequestAmount(amount) { + Onyx.merge(ONYXKEYS.IOU, {amount}); +} + +/** + * @param {String} currency + */ +function setMoneyRequestCurrency(currency) { + Onyx.merge(ONYXKEYS.IOU, {currency}); +} + +/** + * @param {String} comment + */ +function setMoneyRequestDescription(comment) { + Onyx.merge(ONYXKEYS.IOU, {comment: comment.trim()}); +} + +/** + * @param {Object[]} participants + */ +function setMoneyRequestParticipants(participants) { + Onyx.merge(ONYXKEYS.IOU, {participants}); +} + export { deleteMoneyRequest, splitBill, @@ -1279,7 +1343,12 @@ export { sendMoneyElsewhere, sendMoneyViaPaypal, payMoneyRequest, - setIOUSelectedCurrency, - setMoneyRequestDescription, sendMoneyWithWallet, + startMoneyRequest, + resetMoneyRequestInfo, + setMoneyRequestId, + setMoneyRequestAmount, + setMoneyRequestCurrency, + setMoneyRequestDescription, + setMoneyRequestParticipants, }; diff --git a/src/libs/actions/PersonalDetails.js b/src/libs/actions/PersonalDetails.js index b9c5f89a62eb..afc6391f3003 100644 --- a/src/libs/actions/PersonalDetails.js +++ b/src/libs/actions/PersonalDetails.js @@ -321,13 +321,6 @@ function updateSelectedTimezone(selectedTimezone) { Navigation.navigate(ROUTES.SETTINGS_TIMEZONE); } -/** - * Fetches the local currency based on location and sets currency code/symbol to Onyx - */ -function openMoneyRequestModalPage() { - API.read('OpenIOUModalPage'); -} - /** * Fetches additional personal data like legal name, date of birth, address */ @@ -490,7 +483,6 @@ export { getDisplayNameForTypingIndicator, updateAvatar, deleteAvatar, - openMoneyRequestModalPage, openPersonalDetailsPage, openPublicProfilePage, extractFirstAndLastNameFromAvailableDetails, diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js index 35e33bb5170a..5d83dcdf285a 100644 --- a/src/pages/home/report/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose.js @@ -21,8 +21,6 @@ import withLocalize, {withLocalizePropTypes} from '../../../components/withLocal import willBlurTextInputOnTapOutside from '../../../libs/willBlurTextInputOnTapOutside'; import canFocusInputOnScreenFocus from '../../../libs/canFocusInputOnScreenFocus'; import CONST from '../../../CONST'; -import Navigation from '../../../libs/Navigation/Navigation'; -import ROUTES from '../../../ROUTES'; import reportActionPropTypes from './reportActionPropTypes'; import * as ReportUtils from '../../../libs/ReportUtils'; import ReportActionComposeFocusManager from '../../../libs/ReportActionComposeFocusManager'; @@ -52,6 +50,7 @@ import * as Welcome from '../../../libs/actions/Welcome'; import Permissions from '../../../libs/Permissions'; import * as TaskUtils from '../../../libs/actions/Task'; import * as Browser from '../../../libs/Browser'; +import * as IOU from '../../../libs/actions/IOU'; import PressableWithFeedback from '../../../components/Pressable/PressableWithFeedback'; const propTypes = { @@ -370,20 +369,20 @@ class ReportActionCompose extends React.Component { [CONST.IOU.MONEY_REQUEST_TYPE.SPLIT]: { icon: Expensicons.Receipt, text: this.props.translate('iou.splitBill'), - onSelected: () => Navigation.navigate(ROUTES.getIouSplitRoute(this.props.reportID)), }, [CONST.IOU.MONEY_REQUEST_TYPE.REQUEST]: { icon: Expensicons.MoneyCircle, text: this.props.translate('iou.requestMoney'), - onSelected: () => Navigation.navigate(ROUTES.getIouRequestRoute(this.props.reportID)), }, [CONST.IOU.MONEY_REQUEST_TYPE.SEND]: { icon: Expensicons.Send, text: this.props.translate('iou.sendMoney'), - onSelected: () => Navigation.navigate(ROUTES.getIOUSendRoute(this.props.reportID)), }, }; - return _.map(ReportUtils.getMoneyRequestOptions(this.props.report, reportParticipants, this.props.betas), (option) => options[option]); + return _.map(ReportUtils.getMoneyRequestOptions(this.props.report, reportParticipants, this.props.betas), (option) => ({ + ...options[option], + onSelected: () => IOU.startMoneyRequest(option, this.props.report.reportID), + })); } /** diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js index 7266d2673c4f..d1e0f3bf9bb4 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js @@ -24,6 +24,7 @@ import * as Welcome from '../../../../libs/actions/Welcome'; import withNavigationFocus from '../../../../components/withNavigationFocus'; import * as TaskUtils from '../../../../libs/actions/Task'; import * as Session from '../../../../libs/actions/Session'; +import * as IOU from '../../../../libs/actions/IOU'; /** * @param {Object} [policy] @@ -196,7 +197,7 @@ class FloatingActionButtonAndPopover extends React.Component { { icon: Expensicons.Send, text: this.props.translate('iou.sendMoney'), - onSelected: () => this.interceptAnonymousUser(() => Navigation.navigate(ROUTES.IOU_SEND)), + onSelected: () => this.interceptAnonymousUser(() => IOU.startMoneyRequest(CONST.IOU.MONEY_REQUEST_TYPE.SEND)), }, ] : []), @@ -205,7 +206,7 @@ class FloatingActionButtonAndPopover extends React.Component { { icon: Expensicons.MoneyCircle, text: this.props.translate('iou.requestMoney'), - onSelected: () => this.interceptAnonymousUser(() => Navigation.navigate(ROUTES.IOU_REQUEST)), + onSelected: () => this.interceptAnonymousUser(() => IOU.startMoneyRequest(CONST.IOU.MONEY_REQUEST_TYPE.REQUEST)), }, ] : []), @@ -214,7 +215,7 @@ class FloatingActionButtonAndPopover extends React.Component { { icon: Expensicons.Receipt, text: this.props.translate('iou.splitBill'), - onSelected: () => this.interceptAnonymousUser(() => Navigation.navigate(ROUTES.IOU_BILL)), + onSelected: () => this.interceptAnonymousUser(() => IOU.startMoneyRequest(CONST.IOU.MONEY_REQUEST_TYPE.SPLIT)), }, ] : []), diff --git a/src/pages/iou/IOUBillPage.js b/src/pages/iou/IOUBillPage.js deleted file mode 100644 index a3a9d4932bd4..000000000000 --- a/src/pages/iou/IOUBillPage.js +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react'; -import MoneyRequestModal from './MoneyRequestModal'; - -function IOUBillPage(props) { - return ( - - ); -} -IOUBillPage.displayName = 'IOUBillPage'; -export default IOUBillPage; diff --git a/src/pages/iou/IOUCurrencySelection.js b/src/pages/iou/IOUCurrencySelection.js index 92a737295da2..20c1a58b3fed 100644 --- a/src/pages/iou/IOUCurrencySelection.js +++ b/src/pages/iou/IOUCurrencySelection.js @@ -5,16 +5,16 @@ import _ from 'underscore'; import lodashGet from 'lodash/get'; import Str from 'expensify-common/lib/str'; import ONYXKEYS from '../../ONYXKEYS'; +import CONST from '../../CONST'; import OptionsSelector from '../../components/OptionsSelector'; import Navigation from '../../libs/Navigation/Navigation'; import ScreenWrapper from '../../components/ScreenWrapper'; import HeaderWithBackButton from '../../components/HeaderWithBackButton'; import compose from '../../libs/compose'; import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize'; -import * as CurrencyUtils from '../../libs/CurrencyUtils'; import {withNetwork} from '../../components/OnyxProvider'; +import * as CurrencyUtils from '../../libs/CurrencyUtils'; import ROUTES from '../../ROUTES'; -import CONST from '../../CONST'; import themeColors from '../../styles/themes/default'; import * as Expensicons from '../../components/Icon/Expensicons'; @@ -38,12 +38,9 @@ const propTypes = { }), ), - /* Onyx Props */ - - /** Holds data related to IOU view state, rather than the underlying IOU data. */ + /** Holds data related to Money Request view state, rather than the underlying Money Request data. */ iou: PropTypes.shape({ - /** Selected Currency Code of the current IOU */ - selectedCurrencyCode: PropTypes.string, + currency: PropTypes.string, }), ...withLocalizePropTypes, @@ -52,7 +49,7 @@ const propTypes = { const defaultProps = { currencyList: {}, iou: { - selectedCurrencyCode: CONST.CURRENCY.USD, + currency: CONST.CURRENCY.USD, }, }; @@ -91,7 +88,7 @@ class IOUCurrencySelection extends Component { } getSelectedCurrencyCode() { - return lodashGet(this.props.route, 'params.currency', this.props.iou.selectedCurrencyCode); + return lodashGet(this.props.route, 'params.currency', this.props.iou.currency); } /** @@ -146,13 +143,15 @@ class IOUCurrencySelection extends Component { render() { const headerMessage = this.state.searchValue.trim() && !this.state.currencyData.length ? this.props.translate('common.noResultsFound') : ''; + const iouType = lodashGet(this.props.route, 'params.iouType', CONST.IOU.MONEY_REQUEST_TYPE.REQUEST); + const reportID = lodashGet(this.props.route, 'params.reportID', ''); return ( {({safeAreaPaddingBottomStyle}) => ( <> Navigation.goBack(ROUTES.getIouRequestRoute(Navigation.getTopmostReportId()))} + onBackButtonPress={() => Navigation.goBack(ROUTES.getMoneyRequestRoute(iouType, reportID))} /> ; -} -IOURequestPage.displayName = 'IOURequestPage'; -export default IOURequestPage; diff --git a/src/pages/iou/IOUSendPage.js b/src/pages/iou/IOUSendPage.js deleted file mode 100644 index e53fd273a2f2..000000000000 --- a/src/pages/iou/IOUSendPage.js +++ /dev/null @@ -1,15 +0,0 @@ -import React from 'react'; -import CONST from '../../CONST'; -import MoneyRequestModal from './MoneyRequestModal'; - -function IOUSendPage(props) { - return ( - - ); -} -IOUSendPage.displayName = 'IOUSendPage'; -export default IOUSendPage; diff --git a/src/pages/iou/MoneyRequestDescriptionPage.js b/src/pages/iou/MoneyRequestDescriptionPage.js index 9c9b44e5a72a..1c9af254a162 100644 --- a/src/pages/iou/MoneyRequestDescriptionPage.js +++ b/src/pages/iou/MoneyRequestDescriptionPage.js @@ -2,6 +2,8 @@ import React, {Component} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import PropTypes from 'prop-types'; +import _ from 'underscore'; +import lodashGet from 'lodash/get'; import TextInput from '../../components/TextInput'; import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize'; import ScreenWrapper from '../../components/ScreenWrapper'; @@ -10,8 +12,10 @@ import Form from '../../components/Form'; import ONYXKEYS from '../../ONYXKEYS'; import styles from '../../styles/styles'; import Navigation from '../../libs/Navigation/Navigation'; +import ROUTES from '../../ROUTES'; import compose from '../../libs/compose'; import * as IOU from '../../libs/actions/IOU'; +import optionPropTypes from '../../components/optionPropTypes'; const propTypes = { ...withLocalizePropTypes, @@ -19,13 +23,19 @@ const propTypes = { /** Onyx Props */ /** Holds data related to Money Request view state, rather than the underlying Money Request data. */ iou: PropTypes.shape({ + id: PropTypes.string, + amount: PropTypes.number, comment: PropTypes.string, + participants: PropTypes.arrayOf(optionPropTypes), }), }; const defaultProps = { iou: { + id: '', + amount: 0, comment: '', + participants: [], }, }; @@ -34,14 +44,36 @@ class MoneyRequestDescriptionPage extends Component { super(props); this.updateComment = this.updateComment.bind(this); + this.navigateBack = this.navigateBack.bind(this); + this.iouType = lodashGet(props.route, 'params.iouType', ''); + this.reportID = lodashGet(props.route, 'params.reportID', ''); } - /** - * Goes back and clears the description from Onyx. - */ - onBackButtonPress() { - IOU.setMoneyRequestDescription(''); - Navigation.goBack(); + componentDidMount() { + const moneyRequestId = `${this.iouType}${this.reportID}`; + const shouldReset = this.props.iou.id !== moneyRequestId; + if (shouldReset) { + IOU.resetMoneyRequestInfo(moneyRequestId); + } + + if (_.isEmpty(this.props.iou.participants) || this.props.iou.amount === 0 || shouldReset) { + Navigation.goBack(ROUTES.getMoneyRequestRoute(this.iouType, this.reportID), true); + } + } + + // eslint-disable-next-line rulesdir/prefer-early-return + componentDidUpdate(prevProps) { + // ID in Onyx could change by initiating a new request in a separate browser tab or completing a request + if (_.isEmpty(this.props.iou.participants) || this.props.iou.amount === 0 || prevProps.iou.id !== this.props.iou.id) { + // The ID is cleared on completing a request. In that case, we will do nothing. + if (this.props.iou.id) { + Navigation.goBack(ROUTES.getMoneyRequestRoute(this.iouType, this.reportID), true); + } + } + } + + navigateBack() { + Navigation.goBack(ROUTES.getMoneyRequestConfirmationRoute(this.iouType, this.reportID)); } /** @@ -52,7 +84,7 @@ class MoneyRequestDescriptionPage extends Component { */ updateComment(value) { IOU.setMoneyRequestDescription(value.moneyRequestComment); - Navigation.goBack(); + this.navigateBack(); } render() { @@ -64,7 +96,7 @@ class MoneyRequestDescriptionPage extends Component { >
(reportParticipants.length ? [Steps.MoneyRequestAmount, Steps.MoneyRequestConfirm] : [Steps.MoneyRequestAmount, Steps.MoneyRequestParticipants, Steps.MoneyRequestConfirm]), - [reportParticipants.length], - ); - - const [previousStepIndex, setPreviousStepIndex] = useState(-1); - const [currentStepIndex, setCurrentStepIndex] = useState(0); - const [selectedOptions, setSelectedOptions] = useState( - ReportUtils.isPolicyExpenseChat(props.report) - ? OptionsListUtils.getPolicyExpenseReportOptions(props.report) - : OptionsListUtils.getParticipantsOptions(props.report, props.allPersonalDetails), - ); - const [amount, setAmount] = useState(0); - - useEffect(() => { - PersonalDetails.openMoneyRequestModalPage(); - IOU.setMoneyRequestDescription(''); - }, []); - - // We update selected currency when PersonalDetails.openMoneyRequestModalPage finishes - // props.currentUserPersonalDetails might be stale data or might not exist if user is signing in - useEffect(() => { - if (_.isUndefined(props.currentUserPersonalDetails.localCurrencyCode)) { - return; - } - IOU.setIOUSelectedCurrency(props.currentUserPersonalDetails.localCurrencyCode); - }, [props.currentUserPersonalDetails.localCurrencyCode]); - - // User came back online, so let's refetch the currency details based on location - useOnNetworkReconnect(PersonalDetails.openMoneyRequestModalPage); - - /** - * Decides our animation type based on whether we're increasing or decreasing - * our step index. - * @returns {String|null} - */ - const direction = useMemo(() => { - // If we're going to the "amount" step from the "confirm" step, push it in and pop it out like we're moving - // forward instead of backwards. - const amountIndex = _.indexOf(steps, Steps.MoneyRequestAmount); - const confirmIndex = _.indexOf(steps, Steps.MoneyRequestConfirm); - if (previousStepIndex === confirmIndex && currentStepIndex === amountIndex) { - return 'in'; - } - if (previousStepIndex === amountIndex && currentStepIndex === confirmIndex) { - return 'out'; - } - - 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, steps]); - - /** - * Retrieve title for current step, based upon current step and type of request - * - * @returns {String} - */ - const titleForStep = useMemo(() => { - if (currentStepIndex === 0) { - const confirmIndex = _.indexOf(steps, Steps.MoneyRequestConfirm); - if (previousStepIndex === confirmIndex) { - return props.translate('iou.amount'); - } - if (props.iouType === CONST.IOU.MONEY_REQUEST_TYPE.SEND) { - return props.translate('iou.sendMoney'); - } - return props.translate(props.hasMultipleParticipants ? 'iou.splitBill' : 'iou.requestMoney'); - } - return props.translate('iou.cash'); - // eslint-disable-next-line react-hooks/exhaustive-deps -- props does not need to be a dependency as it will always exist - }, [currentStepIndex, props.translate, steps]); - - /** - * Navigate to a provided step. - * - * @param {Number} stepIndex - * @type {(function(*): void)|*} - */ - const navigateToStep = useCallback( - (stepIndex) => { - if (stepIndex < 0 || stepIndex > steps.length) { - return; - } - - if (currentStepIndex === stepIndex) { - return; - } - - setPreviousStepIndex(currentStepIndex); - setCurrentStepIndex(stepIndex); - }, - [currentStepIndex, steps.length], - ); - - /** - * Navigate to the previous request step if possible - */ - const navigateToPreviousStep = useCallback(() => { - if (currentStepIndex === 0) { - Navigation.dismissModal(); - return; - } - - if (currentStepIndex <= 0 && previousStepIndex < 0) { - return; - } - - setPreviousStepIndex(currentStepIndex); - setCurrentStepIndex(currentStepIndex - 1); - }, [currentStepIndex, previousStepIndex]); - - /** - * Navigate to the next request step if possible - */ - const navigateToNextStep = useCallback(() => { - if (currentStepIndex >= steps.length - 1) { - return; - } - - // If we're coming from the confirm step, it means we were editing something so go back to the confirm step. - const confirmIndex = _.indexOf(steps, Steps.MoneyRequestConfirm); - if (previousStepIndex === confirmIndex) { - navigateToStep(confirmIndex); - return; - } - - setPreviousStepIndex(currentStepIndex); - setCurrentStepIndex(currentStepIndex + 1); - }, [currentStepIndex, previousStepIndex, navigateToStep, steps]); - - /** - * Checks if user has a GOLD wallet then creates a paid IOU report on the fly - * - * @param {String} paymentMethodType - */ - const sendMoney = useCallback( - (paymentMethodType) => { - const currency = props.iou.selectedCurrencyCode; - const trimmedComment = props.iou.comment.trim(); - const participant = selectedOptions[0]; - - if (paymentMethodType === CONST.IOU.PAYMENT_TYPE.ELSEWHERE) { - IOU.sendMoneyElsewhere(props.report, amount, currency, trimmedComment, props.currentUserPersonalDetails.accountID, participant); - return; - } - - if (paymentMethodType === CONST.IOU.PAYMENT_TYPE.PAYPAL_ME) { - IOU.sendMoneyViaPaypal(props.report, amount, currency, trimmedComment, props.currentUserPersonalDetails.accountID, participant); - return; - } - - if (paymentMethodType === CONST.IOU.PAYMENT_TYPE.EXPENSIFY) { - IOU.sendMoneyWithWallet(props.report, amount, currency, trimmedComment, props.currentUserPersonalDetails.accountID, participant); - } - }, - [amount, props.iou.comment, selectedOptions, props.currentUserPersonalDetails.accountID, props.iou.selectedCurrencyCode, props.report], - ); - - /** - * @param {Array} selectedParticipants - */ - const createTransaction = useCallback( - (selectedParticipants) => { - const reportID = lodashGet(props.route, 'params.reportID', ''); - const trimmedComment = props.iou.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, - props.currentUserPersonalDetails.accountID, - amount, - trimmedComment, - props.iou.selectedCurrencyCode, - 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, - props.currentUserPersonalDetails.accountID, - amount, - trimmedComment, - props.iou.selectedCurrencyCode, - ); - return; - } - - IOU.requestMoney( - props.report, - amount, - props.iou.selectedCurrencyCode, - props.currentUserPersonalDetails.login, - props.currentUserPersonalDetails.accountID, - selectedParticipants[0], - trimmedComment, - ); - }, - [ - amount, - props.iou.comment, - props.currentUserPersonalDetails.login, - props.currentUserPersonalDetails.accountID, - props.hasMultipleParticipants, - props.iou.selectedCurrencyCode, - props.report, - props.route, - ], - ); - - const currentStep = steps[currentStepIndex]; - const moneyRequestStepIndex = _.indexOf(steps, Steps.MoneyRequestConfirm); - const isEditingAmountAfterConfirm = currentStepIndex === 0 && previousStepIndex === _.indexOf(steps, Steps.MoneyRequestConfirm); - const navigateBack = isEditingAmountAfterConfirm ? () => navigateToStep(moneyRequestStepIndex) : navigateToPreviousStep; - const reportID = lodashGet(props, 'route.params.reportID', ''); - const modalHeader = ( - - ); - const amountButtonText = isEditingAmountAfterConfirm ? props.translate('common.save') : props.translate('common.next'); - const enableMaxHeight = DeviceCapabilities.canUseTouchScreen() && currentStep === Steps.MoneyRequestParticipants; - const bankAccountRoute = ReportUtils.getBankAccountRoute(props.report); - - return ( - - {({didScreenTransitionEnd, safeAreaPaddingBottomStyle}) => ( - <> - - {!didScreenTransitionEnd && } - {didScreenTransitionEnd && ( - <> - {currentStep === Steps.MoneyRequestAmount && ( - - {modalHeader} - { - const amountInSmallestCurrencyUnits = CurrencyUtils.convertToSmallestUnit(selectedCurrencyCode, Number.parseFloat(value)); - IOU.setIOUSelectedCurrency(selectedCurrencyCode); - setAmount(amountInSmallestCurrencyUnits); - navigateToNextStep(); - }} - reportID={reportID} - hasMultipleParticipants={props.hasMultipleParticipants} - selectedAmount={CurrencyUtils.convertToWholeUnit(props.iou.selectedCurrencyCode, amount)} - navigation={props.navigation} - route={props.route} - iouType={props.iouType} - buttonText={amountButtonText} - /> - - )} - {currentStep === Steps.MoneyRequestParticipants && ( - - {modalHeader} - - - )} - {currentStep === Steps.MoneyRequestConfirm && ( - - {modalHeader} - { - createTransaction(selectedParticipants); - ReportScrollManager.scrollToBottom(); - }} - onSendMoney={(paymentMethodType) => { - sendMoney(paymentMethodType); - ReportScrollManager.scrollToBottom(); - }} - hasMultipleParticipants={props.hasMultipleParticipants} - participants={_.filter(selectedOptions, (option) => props.currentUserPersonalDetails.accountID !== option.accountID)} - iouAmount={amount} - 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)} - navigateToStep={navigateToStep} - policyID={props.report.policyID} - bankAccountRoute={bankAccountRoute} - /> - - )} - - )} - - - )} - - ); -} - -MoneyRequestModal.displayName = 'MoneyRequestModal'; -MoneyRequestModal.propTypes = propTypes; -MoneyRequestModal.defaultProps = defaultProps; - -export default compose( - withLocalize, - withCurrentUserPersonalDetails, - withOnyx({ - report: { - key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${lodashGet(route, 'params.reportID', '')}`, - }, - iou: { - key: ONYXKEYS.IOU, - }, - allPersonalDetails: { - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - }, - }), -)(MoneyRequestModal); diff --git a/src/pages/iou/SplitBillDetailsPage.js b/src/pages/iou/SplitBillDetailsPage.js index f6b63be4df76..e3ff75c82668 100644 --- a/src/pages/iou/SplitBillDetailsPage.js +++ b/src/pages/iou/SplitBillDetailsPage.js @@ -66,9 +66,11 @@ function getReportID(route) { function SplitBillDetailsPage(props) { const reportAction = props.reportActions[`${props.route.params.reportActionID.toString()}`]; const participantAccountIDs = reportAction.originalMessage.participantAccountIDs || PersonalDetailsUtils.getAccountIDsByLogins(reportAction.originalMessage.participants); - const personalDetails = OptionsListUtils.getPersonalDetailsForAccountIDs(participantAccountIDs, props.personalDetails); - const participants = OptionsListUtils.getParticipantsOptions({participantAccountIDs}, personalDetails); - const payeePersonalDetails = personalDetails[reportAction.actorAccountID]; + const participants = OptionsListUtils.getParticipantsOptions( + _.map(participantAccountIDs, (accountID) => ({accountID, selected: true})), + props.personalDetails, + ); + const payeePersonalDetails = props.personalDetails[reportAction.actorAccountID]; const participantsExcludingPayee = _.filter(participants, (participant) => participant.accountID !== reportAction.actorAccountID); const splitAmount = parseInt(lodashGet(reportAction, 'originalMessage.amount', 0), 10); const splitComment = lodashGet(reportAction, 'originalMessage.comment'); diff --git a/src/pages/iou/steps/MoneyRequestAmountPage.js b/src/pages/iou/steps/MoneyRequestAmountPage.js index 39495c6a5f2c..6cf8fa0baf01 100755 --- a/src/pages/iou/steps/MoneyRequestAmountPage.js +++ b/src/pages/iou/steps/MoneyRequestAmountPage.js @@ -11,40 +11,67 @@ import Navigation from '../../../libs/Navigation/Navigation'; import ROUTES from '../../../ROUTES'; import withLocalize, {withLocalizePropTypes} from '../../../components/withLocalize'; import compose from '../../../libs/compose'; +import * as ReportUtils from '../../../libs/ReportUtils'; +import * as IOUUtils from '../../../libs/IOUUtils'; +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 ScreenWrapper from '../../../components/ScreenWrapper'; +import FullPageNotFoundView from '../../../components/BlockingViews/FullPageNotFoundView'; +import withNavigation from '../../../components/withNavigation'; +import HeaderWithBackButton from '../../../components/HeaderWithBackButton'; +import reportPropTypes from '../../reportPropTypes'; +import * as IOU from '../../../libs/actions/IOU'; +import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '../../../components/withCurrentUserPersonalDetails'; const propTypes = { - /** Whether or not this IOU has multiple participants */ - hasMultipleParticipants: PropTypes.bool.isRequired, - - /** The ID of the report this screen should display */ - reportID: PropTypes.string.isRequired, - - /** Callback to inform parent modal of success */ - onStepComplete: PropTypes.func.isRequired, - - /** Previously selected amount to show if the user comes back to this screen */ - selectedAmount: PropTypes.number.isRequired, - - /** Text to display on the button that "saves" the amount */ - buttonText: PropTypes.string.isRequired, + route: PropTypes.shape({ + params: PropTypes.shape({ + iouType: PropTypes.string, + reportID: PropTypes.string, + }), + }), - /* Onyx Props */ + /** The report on which the request is initiated on */ + report: reportPropTypes, - /** Holds data related to IOU view state, rather than the underlying IOU data. */ + /** Holds data related to Money Request view state, rather than the underlying Money Request data. */ iou: PropTypes.shape({ - /** Selected Currency Code of the current IOU */ - selectedCurrencyCode: PropTypes.string, + 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, + }), + ), }), ...withLocalizePropTypes, + ...withCurrentUserPersonalDetailsPropTypes, }; const defaultProps = { - iou: {}, + route: { + params: { + iouType: '', + reportID: '', + }, + }, + report: {}, + iou: { + id: '', + amount: 0, + currency: CONST.CURRENCY.USD, + participants: [], + }, + ...withCurrentUserPersonalDetailsDefaultProps, }; class MoneyRequestAmountPage extends React.Component { constructor(props) { @@ -57,14 +84,19 @@ class MoneyRequestAmountPage extends React.Component { this.stripSpacesFromAmount = this.stripSpacesFromAmount.bind(this); this.focusTextInput = this.focusTextInput.bind(this); this.navigateToCurrencySelectionPage = this.navigateToCurrencySelectionPage.bind(this); + this.navigateBack = this.navigateBack.bind(this); + this.navigateToNextPage = this.navigateToNextPage.bind(this); this.amountViewID = 'amountView'; this.numPadContainerViewID = 'numPadContainerView'; this.numPadViewID = 'numPadView'; + this.iouType = lodashGet(props.route, 'params.iouType', ''); + this.reportID = lodashGet(props.route, 'params.reportID', ''); + this.isEditing = lodashGet(props.route, 'path', '').includes('amount'); - const selectedAmountAsString = props.selectedAmount ? props.selectedAmount.toString() : ''; + const selectedAmountAsString = props.iou.amount ? CurrencyUtils.convertToWholeUnit(props.iou.currency, props.iou.amount).toString() : ''; this.state = { amount: selectedAmountAsString, - selectedCurrencyCode: _.isUndefined(props.iou.selectedCurrencyCode) ? CONST.CURRENCY.USD : props.iou.selectedCurrencyCode, + selectedCurrencyCode: props.iou.currency, shouldUpdateSelection: true, selection: { start: selectedAmountAsString.length, @@ -74,24 +106,63 @@ class MoneyRequestAmountPage extends React.Component { } componentDidMount() { - this.focusTextInput(); + if (this.isEditing) { + const moneyRequestId = `${this.iouType}${this.reportID}`; + const shouldReset = this.props.iou.id !== moneyRequestId; + if (shouldReset) { + IOU.resetMoneyRequestInfo(moneyRequestId); + } + + if (_.isEmpty(this.props.iou.participants) || this.props.iou.amount === 0 || shouldReset) { + Navigation.goBack(ROUTES.getMoneyRequestRoute(this.iouType, this.reportID), true); + return; + } + } // Focus automatically after navigating back from currency selector this.unsubscribeNavFocus = this.props.navigation.addListener('focus', () => { this.focusTextInput(); - this.getCurrencyFromRouteParams(); }); } componentDidUpdate(prevProps) { - if (prevProps.iou.selectedCurrencyCode === this.props.iou.selectedCurrencyCode) { - return; + if (this.isEditing) { + // ID in Onyx could change by initiating a new request in a separate browser tab or completing a request + if (_.isEmpty(this.props.iou.participants) || this.props.iou.amount === 0 || prevProps.iou.id !== this.props.iou.id) { + // The ID is cleared on completing a request. In that case, we will do nothing. + if (this.props.iou.id) { + Navigation.goBack(ROUTES.getMoneyRequestRoute(this.iouType, this.reportID), true); + } + return; + } + } + + const prevCurrencyParam = lodashGet(prevProps.route.params, 'currency', ''); + const currencyParam = lodashGet(this.props.route.params, 'currency', ''); + if (currencyParam !== '' && prevCurrencyParam !== currencyParam) { + this.setState({selectedCurrencyCode: currencyParam}); + } + + if (prevProps.iou.currency !== this.props.iou.currency) { + this.setState({selectedCurrencyCode: this.props.iou.currency}); } - this.setState({selectedCurrencyCode: this.props.iou.selectedCurrencyCode}); + if (prevProps.iou.amount !== this.props.iou.amount) { + const selectedAmountAsString = this.props.iou.amount ? CurrencyUtils.convertToWholeUnit(this.props.iou.currency, this.props.iou.amount).toString() : ''; + this.setState({ + amount: selectedAmountAsString, + selection: { + start: selectedAmountAsString.length, + end: selectedAmountAsString.length, + }, + }); + } } componentWillUnmount() { + if (!this.unsubscribeNavFocus) { + return; + } this.unsubscribeNavFocus(); } @@ -112,13 +183,6 @@ class MoneyRequestAmountPage extends React.Component { } } - getCurrencyFromRouteParams() { - const selectedCurrencyCode = lodashGet(this.props.route.params, 'currency', ''); - if (selectedCurrencyCode !== '') { - this.setState({selectedCurrencyCode}); - } - } - /** * Returns the new selection object based on the updated amount's length * @@ -152,6 +216,23 @@ class MoneyRequestAmountPage extends React.Component { return {amount: this.stripCommaFromAmount(newAmountWithoutSpaces), selection}; } + /** + * Get page title based on the iou type + * + * @returns {String} + */ + getTitleForStep() { + if (this.isEditing) { + return this.props.translate('iou.amount'); + } + const title = { + [CONST.IOU.MONEY_REQUEST_TYPE.REQUEST]: this.props.translate('iou.requestMoney'), + [CONST.IOU.MONEY_REQUEST_TYPE.SEND]: this.props.translate('iou.sendMoney'), + [CONST.IOU.MONEY_REQUEST_TYPE.SPLIT]: this.props.translate('iou.splitBill'), + }; + return title[this.iouType]; + } + /** * Focus text input */ @@ -306,69 +387,120 @@ class MoneyRequestAmountPage extends React.Component { .value(); } + navigateBack() { + Navigation.goBack(this.isEditing ? ROUTES.getMoneyRequestConfirmationRoute(this.iouType, this.reportID) : null); + } + navigateToCurrencySelectionPage() { // Remove query from the route and encode it. const activeRoute = encodeURIComponent(Navigation.getActiveRoute().replace(/\?.*/, '')); - if (this.props.hasMultipleParticipants) { - return Navigation.navigate(ROUTES.getIouBillCurrencyRoute(this.props.reportID, this.state.selectedCurrencyCode, activeRoute)); + Navigation.navigate(ROUTES.getMoneyRequestCurrencyRoute(this.iouType, this.reportID, this.state.selectedCurrencyCode, activeRoute)); + } + + navigateToNextPage() { + const amountInSmallestCurrencyUnits = CurrencyUtils.convertToSmallestUnit(this.state.selectedCurrencyCode, Number.parseFloat(this.state.amount)); + IOU.setMoneyRequestAmount(amountInSmallestCurrencyUnits); + IOU.setMoneyRequestCurrency(this.state.selectedCurrencyCode); + + if (this.isEditing) { + Navigation.goBack(ROUTES.getMoneyRequestConfirmationRoute(this.iouType, this.reportID)); + return; } - if (this.props.iouType === CONST.IOU.MONEY_REQUEST_TYPE.SEND) { - return Navigation.navigate(ROUTES.getIouSendCurrencyRoute(this.props.reportID, this.state.selectedCurrencyCode, activeRoute)); + + const moneyRequestId = `${this.iouType}${this.reportID}`; + const shouldReset = this.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([]); } - return Navigation.navigate(ROUTES.getIouRequestCurrencyRoute(this.props.reportID, this.state.selectedCurrencyCode, activeRoute)); + + // If a request is initiated on a report, skip the participants selection step and navigate to the confirmation page. + if (this.props.report.reportID) { + // Reinitialize the participants when the money request ID in Onyx does not match the ID from params + if (_.isEmpty(this.props.iou.participants) || shouldReset) { + const currentUserAccountID = this.props.currentUserPersonalDetails.accountID; + const participants = ReportUtils.isPolicyExpenseChat(this.props.report) + ? [{reportID: this.props.report.reportID, isPolicyExpenseChat: true, selected: true}] + : _.chain(this.props.report.participantAccountIDs) + .filter((accountID) => currentUserAccountID !== accountID) + .map((accountID) => ({accountID, selected: true})) + .value(); + IOU.setMoneyRequestParticipants(participants); + } + Navigation.navigate(ROUTES.getMoneyRequestConfirmationRoute(this.iouType, this.reportID)); + return; + } + Navigation.navigate(ROUTES.getMoneyRequestParticipantsRoute(this.iouType)); } render() { const formattedAmount = this.replaceAllDigits(this.state.amount, this.props.toLocaleDigit); + const buttonText = this.isEditing ? this.props.translate('common.save') : this.props.translate('common.next'); return ( - <> - this.onMouseDown(event, [this.amountViewID])} - style={[styles.flex1, styles.flexRow, styles.w100, styles.alignItemsCenter, styles.justifyContentCenter]} + + - (this.textInput = el)} - selectedCurrencyCode={this.state.selectedCurrencyCode} - selection={this.state.selection} - onSelectionChange={(e) => { - if (!this.state.shouldUpdateSelection) { - return; - } - this.setState({selection: e.nativeEvent.selection}); - }} - /> - - this.onMouseDown(event, [this.numPadContainerViewID, this.numPadViewID])} - style={[styles.w100, styles.justifyContentEnd, styles.pageWrapper]} - nativeID={this.numPadContainerViewID} - > - {DeviceCapabilities.canUseTouchScreen() ? ( - - ) : ( - + {({safeAreaPaddingBottomStyle}) => ( + + + this.onMouseDown(event, [this.amountViewID])} + style={[styles.flex1, styles.flexRow, styles.w100, styles.alignItemsCenter, styles.justifyContentCenter]} + > + (this.textInput = el)} + selectedCurrencyCode={this.state.selectedCurrencyCode} + selection={this.state.selection} + onSelectionChange={(e) => { + if (!this.state.shouldUpdateSelection) { + return; + } + this.setState({selection: e.nativeEvent.selection}); + }} + /> + + this.onMouseDown(event, [this.numPadContainerViewID, this.numPadViewID])} + style={[styles.w100, styles.justifyContentEnd, styles.pageWrapper]} + nativeID={this.numPadContainerViewID} + > + {DeviceCapabilities.canUseTouchScreen() ? ( + + ) : ( + + )} + +