From 94b8046ba353921bb901f93566edc9300da3c792 Mon Sep 17 00:00:00 2001 From: Francois Laithier Date: Mon, 29 Jan 2024 17:04:41 -0800 Subject: [PATCH] Revert "Remove MoneyRequestConfirmPage" --- src/ROUTES.ts | 4 + src/SCREENS.ts | 1 + .../AppNavigator/ModalStackNavigators.tsx | 1 + src/libs/Navigation/linkingConfig.ts | 1 + src/libs/Navigation/types.ts | 4 + .../step/IOURequestStepConfirmation.js | 14 +- .../iou/steps/MoneyRequestConfirmPage.js | 473 ++++++++++++++++++ 7 files changed, 496 insertions(+), 2 deletions(-) create mode 100644 src/pages/iou/steps/MoneyRequestConfirmPage.js diff --git a/src/ROUTES.ts b/src/ROUTES.ts index deabdc0ac853..9c4375b84ab6 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -267,6 +267,10 @@ const ROUTES = { route: ':iouType/new/participants/:reportID?', getRoute: (iouType: string, reportID = '') => `${iouType}/new/participants/${reportID}` as const, }, + MONEY_REQUEST_CONFIRMATION: { + route: ':iouType/new/confirmation/:reportID?', + getRoute: (iouType: string, reportID = '') => `${iouType}/new/confirmation/${reportID}` as const, + }, MONEY_REQUEST_DATE: { route: ':iouType/new/date/:reportID?', getRoute: (iouType: string, reportID = '') => `${iouType}/new/date/${reportID}` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 5a8922ee01c3..2bf40caede57 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -141,6 +141,7 @@ const SCREENS = { ROOT: 'Money_Request', AMOUNT: 'Money_Request_Amount', PARTICIPANTS: 'Money_Request_Participants', + CONFIRMATION: 'Money_Request_Confirmation', CURRENCY: 'Money_Request_Currency', DATE: 'Money_Request_Date', DESCRIPTION: 'Money_Request_Description', diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx index 3a843e400409..c9325206e5b2 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx @@ -93,6 +93,7 @@ const MoneyRequestModalStackNavigator = createModalStackNavigator require('../../../pages/iou/MoneyRequestSelectorPage').default as React.ComponentType, [SCREENS.MONEY_REQUEST.AMOUNT]: () => require('../../../pages/iou/steps/NewRequestAmountPage').default as React.ComponentType, [SCREENS.MONEY_REQUEST.PARTICIPANTS]: () => require('../../../pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage').default as React.ComponentType, + [SCREENS.MONEY_REQUEST.CONFIRMATION]: () => require('../../../pages/iou/steps/MoneyRequestConfirmPage').default as React.ComponentType, [SCREENS.MONEY_REQUEST.CURRENCY]: () => require('../../../pages/iou/IOUCurrencySelection').default as React.ComponentType, [SCREENS.MONEY_REQUEST.DATE]: () => require('../../../pages/iou/MoneyRequestDatePage').default as React.ComponentType, [SCREENS.MONEY_REQUEST.DESCRIPTION]: () => require('../../../pages/iou/MoneyRequestDescriptionPage').default as React.ComponentType, diff --git a/src/libs/Navigation/linkingConfig.ts b/src/libs/Navigation/linkingConfig.ts index d4e04d5402e2..5df2bcf0e57b 100644 --- a/src/libs/Navigation/linkingConfig.ts +++ b/src/libs/Navigation/linkingConfig.ts @@ -428,6 +428,7 @@ const linkingConfig: LinkingOptions = { [SCREENS.MONEY_REQUEST.STEP_TAX_AMOUNT]: ROUTES.MONEY_REQUEST_STEP_TAX_AMOUNT.route, [SCREENS.MONEY_REQUEST.STEP_TAX_RATE]: ROUTES.MONEY_REQUEST_STEP_TAX_RATE.route, [SCREENS.MONEY_REQUEST.PARTICIPANTS]: ROUTES.MONEY_REQUEST_PARTICIPANTS.route, + [SCREENS.MONEY_REQUEST.CONFIRMATION]: ROUTES.MONEY_REQUEST_CONFIRMATION.route, [SCREENS.MONEY_REQUEST.DATE]: ROUTES.MONEY_REQUEST_DATE.route, [SCREENS.MONEY_REQUEST.CURRENCY]: ROUTES.MONEY_REQUEST_CURRENCY.route, [SCREENS.MONEY_REQUEST.DESCRIPTION]: ROUTES.MONEY_REQUEST_DESCRIPTION.route, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index b4a77f96cc74..2371c764f42a 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -185,6 +185,10 @@ type MoneyRequestNavigatorParamList = { iouType: string; reportID: string; }; + [SCREENS.MONEY_REQUEST.CONFIRMATION]: { + iouType: string; + reportID: string; + }; [SCREENS.MONEY_REQUEST.CURRENCY]: { iouType: string; reportID: string; diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.js b/src/pages/iou/request/step/IOURequestStepConfirmation.js index f867e57f9a13..6028a735d132 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.js +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.js @@ -8,7 +8,6 @@ import categoryPropTypes from '@components/categoryPropTypes'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Expensicons from '@components/Icon/Expensicons'; import MoneyRequestConfirmationList from '@components/MoneyTemporaryForRefactorRequestConfirmationList'; -import {usePersonalDetails} from '@components/OnyxProvider'; import ScreenWrapper from '@components/ScreenWrapper'; import tagPropTypes from '@components/tagPropTypes'; import transactionPropTypes from '@components/transactionPropTypes'; @@ -24,6 +23,7 @@ import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; +import personalDetailsPropType from '@pages/personalDetailsPropType'; import reportPropTypes from '@pages/reportPropTypes'; import {policyPropTypes} from '@pages/workspace/withPolicy'; import * as IOU from '@userActions/IOU'; @@ -43,6 +43,9 @@ const propTypes = { /** The personal details of the current user */ ...withCurrentUserPersonalDetailsPropTypes, + /** Personal details of all users */ + personalDetails: personalDetailsPropType, + /** The policy of the report */ ...policyPropTypes, @@ -59,6 +62,7 @@ const propTypes = { transaction: transactionPropTypes, }; const defaultProps = { + personalDetails: {}, policy: {}, policyCategories: {}, policyTags: {}, @@ -68,6 +72,7 @@ const defaultProps = { }; function IOURequestStepConfirmation({ currentUserPersonalDetails, + personalDetails, policy, policyTags, policyCategories, @@ -81,7 +86,6 @@ function IOURequestStepConfirmation({ const {translate} = useLocalize(); const {windowWidth} = useWindowDimensions(); const {isOffline} = useNetwork(); - const personalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT; const [receiptFile, setReceiptFile] = useState(); const receiptFilename = lodashGet(transaction, 'filename'); const receiptPath = lodashGet(transaction, 'receipt.source'); @@ -381,6 +385,12 @@ export default compose( withCurrentUserPersonalDetails, withWritableReportOrNotFound, withFullTransactionOrNotFound, + withOnyx({ + personalDetails: { + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + }, + }), + // eslint-disable-next-line rulesdir/no-multiple-onyx-in-file withOnyx({ policy: { key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report ? report.policyID : '0'}`, diff --git a/src/pages/iou/steps/MoneyRequestConfirmPage.js b/src/pages/iou/steps/MoneyRequestConfirmPage.js new file mode 100644 index 000000000000..1738ac78df47 --- /dev/null +++ b/src/pages/iou/steps/MoneyRequestConfirmPage.js @@ -0,0 +1,473 @@ +import lodashGet from 'lodash/get'; +import PropTypes from 'prop-types'; +import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import {View} from 'react-native'; +import {withOnyx} from 'react-native-onyx'; +import _ from 'underscore'; +import categoryPropTypes from '@components/categoryPropTypes'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import * as Expensicons from '@components/Icon/Expensicons'; +import MoneyRequestConfirmationList from '@components/MoneyRequestConfirmationList'; +import {usePersonalDetails} from '@components/OnyxProvider'; +import ScreenWrapper from '@components/ScreenWrapper'; +import tagPropTypes from '@components/tagPropTypes'; +import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '@components/withCurrentUserPersonalDetails'; +import withLocalize from '@components/withLocalize'; +import useInitialValue from '@hooks/useInitialValue'; +import useNetwork from '@hooks/useNetwork'; +import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; +import compose from '@libs/compose'; +import * as FileUtils from '@libs/fileDownload/FileUtils'; +import * as MoneyRequestUtils from '@libs/MoneyRequestUtils'; +import Navigation from '@libs/Navigation/Navigation'; +import * as OptionsListUtils from '@libs/OptionsListUtils'; +import * as ReportUtils from '@libs/ReportUtils'; +import {iouDefaultProps, iouPropTypes} from '@pages/iou/propTypes'; +import reportPropTypes from '@pages/reportPropTypes'; +import {policyDefaultProps, policyPropTypes} from '@pages/workspace/withPolicy'; +import * as IOU from '@userActions/IOU'; +import * as Policy from '@userActions/Policy'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; + +const propTypes = { + /** React Navigation route */ + route: PropTypes.shape({ + /** Params from the route */ + params: PropTypes.shape({ + /** The type of IOU report, i.e. bill, request, send */ + iouType: PropTypes.string, + + /** The report ID of the IOU */ + reportID: PropTypes.string, + }), + }).isRequired, + + report: reportPropTypes, + + /** Holds data related to Money Request view state, rather than the underlying Money Request data. */ + iou: iouPropTypes, + + /** The policy of the current request */ + policy: policyPropTypes, + + policyTags: tagPropTypes, + + policyCategories: PropTypes.objectOf(categoryPropTypes), + + ...withCurrentUserPersonalDetailsPropTypes, +}; + +const defaultProps = { + report: {}, + policyCategories: {}, + policyTags: {}, + iou: iouDefaultProps, + policy: policyDefaultProps, + ...withCurrentUserPersonalDetailsDefaultProps, +}; + +function MoneyRequestConfirmPage(props) { + const styles = useThemeStyles(); + const {isOffline} = useNetwork(); + const {windowWidth} = useWindowDimensions(); + const prevMoneyRequestId = useRef(props.iou.id); + const iouType = useInitialValue(() => lodashGet(props.route, 'params.iouType', '')); + const reportID = useInitialValue(() => lodashGet(props.route, 'params.reportID', '')); + const isDistanceRequest = MoneyRequestUtils.isDistanceRequest(iouType, props.selectedTab); + const isScanRequest = MoneyRequestUtils.isScanRequest(props.selectedTab); + const [receiptFile, setReceiptFile] = useState(); + const personalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT; + + const participants = useMemo( + () => + _.map(props.iou.participants, (participant) => { + const isPolicyExpenseChat = lodashGet(participant, 'isPolicyExpenseChat', false); + return isPolicyExpenseChat ? OptionsListUtils.getPolicyExpenseReportOption(participant) : OptionsListUtils.getParticipantsOption(participant, personalDetails); + }), + [props.iou.participants, personalDetails], + ); + const isPolicyExpenseChat = useMemo(() => ReportUtils.isPolicyExpenseChat(ReportUtils.getRootParentReport(props.report)), [props.report]); + const isManualRequestDM = props.selectedTab === CONST.TAB_REQUEST.MANUAL && iouType === CONST.IOU.TYPE.REQUEST; + + useEffect(() => { + const policyExpenseChat = _.find(participants, (participant) => participant.isPolicyExpenseChat); + if (policyExpenseChat) { + Policy.openDraftWorkspaceRequest(policyExpenseChat.policyID); + } + }, [isOffline, participants, props.iou.billable, props.policy]); + + const defaultBillable = lodashGet(props.policy, 'defaultBillable', false); + useEffect(() => { + IOU.setMoneyRequestBillable(defaultBillable); + }, [defaultBillable, isOffline]); + + useEffect(() => { + if (!props.iou.receiptPath || !props.iou.receiptFilename) { + return; + } + const onSuccess = (file) => { + const receipt = file; + receipt.state = file && isManualRequestDM ? CONST.IOU.RECEIPT_STATE.OPEN : CONST.IOU.RECEIPT_STATE.SCANREADY; + setReceiptFile(receipt); + }; + const onFailure = () => { + Navigation.goBack(ROUTES.MONEY_REQUEST.getRoute(iouType, reportID)); + }; + FileUtils.readFileAsync(props.iou.receiptPath, props.iou.receiptFilename, onSuccess, onFailure); + }, [props.iou.receiptPath, props.iou.receiptFilename, isManualRequestDM, iouType, reportID]); + + useEffect(() => { + // ID in Onyx could change by initiating a new request in a separate browser tab or completing a request + if (!isDistanceRequest && prevMoneyRequestId.current !== props.iou.id) { + // The ID is cleared on completing a request. In that case, we will do nothing. + if (props.iou.id) { + Navigation.goBack(ROUTES.MONEY_REQUEST.getRoute(iouType, reportID), true); + } + return; + } + + // Reset the money request Onyx if the ID in Onyx does not match the ID from params + const moneyRequestId = `${iouType}${reportID}`; + const shouldReset = !isDistanceRequest && props.iou.id !== moneyRequestId && !_.isEmpty(reportID); + if (shouldReset) { + IOU.resetMoneyRequestInfo(moneyRequestId); + } + + if (_.isEmpty(props.iou.participants) || (props.iou.amount === 0 && !props.iou.receiptPath && !isDistanceRequest) || shouldReset || ReportUtils.isArchivedRoom(props.report)) { + Navigation.goBack(ROUTES.MONEY_REQUEST.getRoute(iouType, reportID), true); + } + + return () => { + prevMoneyRequestId.current = props.iou.id; + }; + }, [props.iou.participants, props.iou.amount, props.iou.id, props.iou.receiptPath, isDistanceRequest, props.report, iouType, reportID]); + + const navigateBack = () => { + let fallback; + if (reportID) { + fallback = ROUTES.MONEY_REQUEST.getRoute(iouType, reportID); + } else { + fallback = ROUTES.MONEY_REQUEST_PARTICIPANTS.getRoute(iouType); + } + Navigation.goBack(fallback); + }; + + /** + * @param {Array} selectedParticipants + * @param {String} trimmedComment + * @param {File} [receipt] + */ + const requestMoney = useCallback( + (selectedParticipants, trimmedComment, receipt) => { + IOU.requestMoney( + props.report, + props.iou.amount, + props.iou.currency, + props.iou.created, + props.iou.merchant, + props.currentUserPersonalDetails.login, + props.currentUserPersonalDetails.accountID, + selectedParticipants[0], + trimmedComment, + receipt, + props.iou.category, + props.iou.tag, + props.iou.billable, + props.policy, + props.policyTags, + props.policyCategories, + ); + }, + [ + props.report, + props.iou.amount, + props.iou.currency, + props.iou.created, + props.iou.merchant, + props.currentUserPersonalDetails.login, + props.currentUserPersonalDetails.accountID, + props.iou.category, + props.iou.tag, + props.iou.billable, + props.policy, + props.policyTags, + props.policyCategories, + ], + ); + + /** + * @param {Array} selectedParticipants + * @param {String} trimmedComment + */ + const createDistanceRequest = useCallback( + (selectedParticipants, trimmedComment) => { + IOU.createDistanceRequest( + props.report, + selectedParticipants[0], + trimmedComment, + props.iou.created, + props.iou.transactionID, + props.iou.category, + props.iou.tag, + props.iou.amount, + props.iou.currency, + props.iou.merchant, + props.iou.billable, + props.policy, + props.policyTags, + props.policyCategories, + ); + }, + [ + props.report, + props.iou.created, + props.iou.transactionID, + props.iou.category, + props.iou.tag, + props.iou.amount, + props.iou.currency, + props.iou.merchant, + props.iou.billable, + props.policy, + props.policyTags, + props.policyCategories, + ], + ); + + const createTransaction = useCallback( + (selectedParticipants) => { + const trimmedComment = props.iou.comment.trim(); + + // If we have a receipt let's start the split bill by creating only the action, the transaction, and the group DM if needed + if (iouType === CONST.IOU.TYPE.SPLIT && props.iou.receiptPath) { + const existingSplitChatReportID = CONST.REGEX.NUMBER.test(reportID) ? reportID : ''; + const onSuccess = (receipt) => { + IOU.startSplitBill( + selectedParticipants, + props.currentUserPersonalDetails.login, + props.currentUserPersonalDetails.accountID, + trimmedComment, + receipt, + existingSplitChatReportID, + ); + }; + FileUtils.readFileAsync(props.iou.receiptPath, props.iou.receiptFilename, onSuccess); + return; + } + + // 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 (iouType === CONST.IOU.TYPE.SPLIT && CONST.REGEX.NUMBER.test(reportID)) { + IOU.splitBill( + selectedParticipants, + props.currentUserPersonalDetails.login, + props.currentUserPersonalDetails.accountID, + props.iou.amount, + trimmedComment, + props.iou.currency, + props.iou.category, + props.iou.tag, + reportID, + props.iou.merchant, + ); + return; + } + + // If the request is created from the global create menu, we also navigate the user to the group report + if (iouType === CONST.IOU.TYPE.SPLIT) { + IOU.splitBillAndOpenReport( + selectedParticipants, + props.currentUserPersonalDetails.login, + props.currentUserPersonalDetails.accountID, + props.iou.amount, + trimmedComment, + props.iou.currency, + props.iou.category, + props.iou.tag, + props.iou.merchant, + ); + return; + } + + if (receiptFile) { + requestMoney(selectedParticipants, trimmedComment, receiptFile); + return; + } + + if (isDistanceRequest) { + createDistanceRequest(selectedParticipants, trimmedComment); + return; + } + + requestMoney(selectedParticipants, trimmedComment); + }, + [ + props.iou.amount, + props.iou.comment, + props.currentUserPersonalDetails.login, + props.currentUserPersonalDetails.accountID, + props.iou.currency, + props.iou.category, + props.iou.tag, + props.iou.receiptPath, + props.iou.receiptFilename, + isDistanceRequest, + requestMoney, + createDistanceRequest, + receiptFile, + iouType, + reportID, + props.iou.merchant, + ], + ); + + /** + * 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.currency; + const trimmedComment = props.iou.comment.trim(); + const participant = participants[0]; + + if (paymentMethodType === CONST.IOU.PAYMENT_TYPE.ELSEWHERE) { + IOU.sendMoneyElsewhere(props.report, props.iou.amount, currency, trimmedComment, props.currentUserPersonalDetails.accountID, participant); + return; + } + + if (paymentMethodType === CONST.IOU.PAYMENT_TYPE.EXPENSIFY) { + IOU.sendMoneyWithWallet(props.report, props.iou.amount, currency, trimmedComment, props.currentUserPersonalDetails.accountID, participant); + } + }, + [props.iou.amount, props.iou.comment, participants, props.iou.currency, props.currentUserPersonalDetails.accountID, props.report], + ); + + const headerTitle = () => { + if (isDistanceRequest) { + return props.translate('common.distance'); + } + + if (iouType === CONST.IOU.TYPE.SPLIT) { + return props.translate('iou.split'); + } + + if (iouType === CONST.IOU.TYPE.SEND) { + return props.translate('common.send'); + } + + if (isScanRequest) { + return props.translate('tabSelector.scan'); + } + + return props.translate('tabSelector.manual'); + }; + + return ( + + {({safeAreaPaddingBottomStyle}) => ( + + Navigation.navigate(ROUTES.MONEY_REQUEST_RECEIPT.getRoute(iouType, reportID)), + }, + ]} + /> + { + const newParticipants = _.map(props.iou.participants, (participant) => { + if (participant.accountID === option.accountID) { + return {...participant, selected: !participant.selected}; + } + return participant; + }); + IOU.setMoneyRequestParticipants(newParticipants); + }} + receiptPath={props.iou.receiptPath} + receiptFilename={props.iou.receiptFilename} + iouType={iouType} + reportID={reportID} + isPolicyExpenseChat={isPolicyExpenseChat} + // 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)} + policyID={props.report.policyID} + bankAccountRoute={ReportUtils.getBankAccountRoute(props.report)} + iouMerchant={props.iou.merchant} + iouCreated={props.iou.created} + isScanRequest={isScanRequest} + isDistanceRequest={isDistanceRequest} + shouldShowSmartScanFields={_.isEmpty(props.iou.receiptPath)} + /> + + )} + + ); +} + +MoneyRequestConfirmPage.displayName = 'MoneyRequestConfirmPage'; +MoneyRequestConfirmPage.propTypes = propTypes; +MoneyRequestConfirmPage.defaultProps = defaultProps; + +export default compose( + withCurrentUserPersonalDetails, + withLocalize, + withOnyx({ + iou: { + key: ONYXKEYS.IOU, + }, + }), + // eslint-disable-next-line rulesdir/no-multiple-onyx-in-file + withOnyx({ + report: { + key: ({route, iou}) => { + const reportID = IOU.getIOUReportID(iou, route); + + return `${ONYXKEYS.COLLECTION.REPORT}${reportID}`; + }, + }, + selectedTab: { + key: `${ONYXKEYS.COLLECTION.SELECTED_TAB}${CONST.TAB.RECEIPT_TAB_ID}`, + }, + }), + withOnyx({ + policy: { + key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report ? report.policyID : '0'}`, + }, + policyCategories: { + key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${report ? report.policyID : '0'}`, + }, + policyTags: { + key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${report ? report.policyID : '0'}`, + }, + }), +)(MoneyRequestConfirmPage);