From c495dc673b8326286449fea2bc1d0a9a795ebee3 Mon Sep 17 00:00:00 2001 From: BrtqKr Date: Mon, 25 Mar 2024 16:24:01 +0100 Subject: [PATCH 01/29] migrate step screen wrapper --- ...ScreenWrapper.js => StepScreenWrapper.tsx} | 53 ++++++++----------- 1 file changed, 22 insertions(+), 31 deletions(-) rename src/pages/iou/request/step/{StepScreenWrapper.js => StepScreenWrapper.tsx} (60%) diff --git a/src/pages/iou/request/step/StepScreenWrapper.js b/src/pages/iou/request/step/StepScreenWrapper.tsx similarity index 60% rename from src/pages/iou/request/step/StepScreenWrapper.js rename to src/pages/iou/request/step/StepScreenWrapper.tsx index 3739cbbcc188f..902122fa0dd4c 100644 --- a/src/pages/iou/request/step/StepScreenWrapper.js +++ b/src/pages/iou/request/step/StepScreenWrapper.tsx @@ -1,46 +1,45 @@ -import PropTypes from 'prop-types'; -import React from 'react'; +import type {PropsWithChildren} from 'react'; import {View} from 'react-native'; -import _ from 'underscore'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import useThemeStyles from '@hooks/useThemeStyles'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; +import callOrReturn from '@src/types/utils/callOrReturn'; -const propTypes = { - /** The things to display inside the screenwrapper */ - children: PropTypes.oneOfType([PropTypes.func, PropTypes.node]).isRequired, - +type StepScreenWrapperProps = { /** The title to show in the header (should be translated already) */ - headerTitle: PropTypes.string.isRequired, + headerTitle: string; /** A function triggered when the back button is pressed */ - onBackButtonPress: PropTypes.func.isRequired, + onBackButtonPress: () => void; /** A function triggered when the entry transition is ended. Useful for auto-focusing elements. */ - onEntryTransitionEnd: PropTypes.func, + onEntryTransitionEnd?: () => void; /** Whether or not the wrapper should be shown (sometimes screens can be embedded inside another screen that already is using a wrapper) */ - shouldShowWrapper: PropTypes.bool.isRequired, + shouldShowWrapper: boolean; /** Whether or not to display not found page */ - shouldShowNotFoundPage: PropTypes.bool, + shouldShowNotFoundPage?: boolean; /** An ID used for unit testing */ - testID: PropTypes.string.isRequired, + testID: string; /** Whether or not to include safe area padding */ - includeSafeAreaPaddingBottom: PropTypes.bool, -}; - -const defaultProps = { - onEntryTransitionEnd: () => {}, - includeSafeAreaPaddingBottom: false, - shouldShowNotFoundPage: false, + includeSafeAreaPaddingBottom: boolean; }; -function StepScreenWrapper({testID, headerTitle, onBackButtonPress, onEntryTransitionEnd, children, shouldShowWrapper, shouldShowNotFoundPage, includeSafeAreaPaddingBottom}) { +function StepScreenWrapper({ + testID, + headerTitle, + onBackButtonPress, + onEntryTransitionEnd, + children, + shouldShowWrapper, + shouldShowNotFoundPage, + includeSafeAreaPaddingBottom, +}: PropsWithChildren) { const styles = useThemeStyles(); if (!shouldShowWrapper) { @@ -62,14 +61,8 @@ function StepScreenWrapper({testID, headerTitle, onBackButtonPress, onEntryTrans onBackButtonPress={onBackButtonPress} /> { - // If props.children is a function, call it to provide the insets to the children. - _.isFunction(children) - ? children({ - insets, - safeAreaPaddingBottomStyle, - didScreenTransitionEnd, - }) - : children + // If props.children is a function, call it to provide the insets to the children + callOrReturn(children, {insets, safeAreaPaddingBottomStyle, didScreenTransitionEnd}) } @@ -79,7 +72,5 @@ function StepScreenWrapper({testID, headerTitle, onBackButtonPress, onEntryTrans } StepScreenWrapper.displayName = 'StepScreenWrapper'; -StepScreenWrapper.propTypes = propTypes; -StepScreenWrapper.defaultProps = defaultProps; export default StepScreenWrapper; From 7833b99f229256143fba1fddc489049bca1b1dff Mon Sep 17 00:00:00 2001 From: BrtqKr Date: Mon, 25 Mar 2024 16:35:51 +0100 Subject: [PATCH 02/29] migrate step drag and drop wrapper --- ...er.js => StepScreenDragAndDropWrapper.tsx} | 25 ++++++------------- 1 file changed, 8 insertions(+), 17 deletions(-) rename src/pages/iou/request/step/{StepScreenDragAndDropWrapper.js => StepScreenDragAndDropWrapper.tsx} (79%) diff --git a/src/pages/iou/request/step/StepScreenDragAndDropWrapper.js b/src/pages/iou/request/step/StepScreenDragAndDropWrapper.tsx similarity index 79% rename from src/pages/iou/request/step/StepScreenDragAndDropWrapper.js rename to src/pages/iou/request/step/StepScreenDragAndDropWrapper.tsx index ceb0d5a443516..39ac12b6bdcf6 100644 --- a/src/pages/iou/request/step/StepScreenDragAndDropWrapper.js +++ b/src/pages/iou/request/step/StepScreenDragAndDropWrapper.tsx @@ -1,4 +1,4 @@ -import PropTypes from 'prop-types'; +import type {PropsWithChildren} from 'react'; import React, {useState} from 'react'; import {View} from 'react-native'; import DragAndDropProvider from '@components/DragAndDrop/Provider'; @@ -7,31 +7,24 @@ import ScreenWrapper from '@components/ScreenWrapper'; import useThemeStyles from '@hooks/useThemeStyles'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; -const propTypes = { - /** The things to display inside the screenwrapper */ - children: PropTypes.node.isRequired, - +type StepScreenDragAndDropWrapperProps = { /** The title to show in the header (should be translated already) */ - headerTitle: PropTypes.string.isRequired, + headerTitle: string; /** A function triggered when the back button is pressed */ - onBackButtonPress: PropTypes.func.isRequired, + onBackButtonPress: () => void; /** A function triggered when the entry transition is ended. Useful for auto-focusing elements. */ - onEntryTransitionEnd: PropTypes.func, + onEntryTransitionEnd?: () => void; /** Whether or not the wrapper should be shown (sometimes screens can be embedded inside another screen that already is using a wrapper) */ - shouldShowWrapper: PropTypes.bool.isRequired, + shouldShowWrapper: boolean; /** An ID used for unit testing */ - testID: PropTypes.string.isRequired, -}; - -const defaultProps = { - onEntryTransitionEnd: () => {}, + testID: string; }; -function StepScreenDragAndDropWrapper({testID, headerTitle, onBackButtonPress, onEntryTransitionEnd, children, shouldShowWrapper}) { +function StepScreenDragAndDropWrapper({testID, headerTitle, onBackButtonPress, onEntryTransitionEnd, children, shouldShowWrapper}: PropsWithChildren) { const styles = useThemeStyles(); const [isDraggingOver, setIsDraggingOver] = useState(false); @@ -65,7 +58,5 @@ function StepScreenDragAndDropWrapper({testID, headerTitle, onBackButtonPress, o } StepScreenDragAndDropWrapper.displayName = 'StepScreenDragAndDropWrapper'; -StepScreenDragAndDropWrapper.propTypes = propTypes; -StepScreenDragAndDropWrapper.defaultProps = defaultProps; export default StepScreenDragAndDropWrapper; From a738e500a06dd49c1806dfa35efd1b7d00c77a45 Mon Sep 17 00:00:00 2001 From: BrtqKr Date: Tue, 26 Mar 2024 09:00:43 +0100 Subject: [PATCH 03/29] migrate iouRequest wip --- src/libs/Navigation/types.ts | 8 + .../step/IOURequestStepConfirmation.tsx | 558 ++++++++++++++++++ 2 files changed, 566 insertions(+) create mode 100644 src/pages/iou/request/step/IOURequestStepConfirmation.tsx diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 3f85aec3a560a..a06503d9edf37 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -413,6 +413,14 @@ type MoneyRequestNavigatorParamList = { iouType: string; reportID: string; }; + [SCREENS.MONEY_REQUEST.STEP_CONFIRMATION]: { + action: keyof typeof CONST.IOU.ACTION; + iouType: string; + transactionID: string; + reportID: string; + pageIndex?: string; + backTo?: string; + }; }; type NewTaskNavigatorParamList = { diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx new file mode 100644 index 0000000000000..e1a842bf30f8c --- /dev/null +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -0,0 +1,558 @@ +import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import {View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; +import categoryPropTypes from '@components/categoryPropTypes'; +import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import * as Expensicons from '@components/Icon/Expensicons'; +import MoneyRequestConfirmationList from '@components/MoneyTemporaryForRefactorRequestConfirmationList'; +import ScreenWrapper from '@components/ScreenWrapper'; +import tagPropTypes from '@components/tagPropTypes'; +import transactionPropTypes from '@components/transactionPropTypes'; +import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '@components/withCurrentUserPersonalDetails'; +import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; +import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; +import compose from '@libs/compose'; +import * as DeviceCapabilities from '@libs/DeviceCapabilities'; +import getCurrentPosition from '@libs/getCurrentPosition'; +import * as IOUUtils from '@libs/IOUUtils'; +import Log from '@libs/Log'; +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'; +// import * as Policy from '@userActions/Policy'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import {Policy} from '@src/types/onyx'; +import type {PolicyCategories, PolicyTagList} from '@src/types/onyx'; +import IOURequestStepRoutePropTypes from './IOURequestStepRoutePropTypes'; +import withFullTransactionOrNotFound from './withFullTransactionOrNotFound'; +import withWritableReportOrNotFound from './withWritableReportOrNotFound'; + +type IOURequestStepConfirmationOnyxProps = { + policy: OnyxEntry; + policyCategories: OnyxEntry; + policyTags: OnyxEntry; +}; + +type IOURequestStepConfirmationProps = IOURequestStepConfirmationOnyxProps & { + someProp: string; +}; + +function IOURequestStepConfirmation({ + currentUserPersonalDetails, + personalDetails, + policy, + policyTags, + policyCategories, + report, + route: { + params: {iouType, reportID, transactionID}, + }, + transaction, +}) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const {windowWidth} = useWindowDimensions(); + const {isOffline} = useNetwork(); + const [receiptFile, setReceiptFile] = useState(); + const receiptFilename = lodashGet(transaction, 'filename'); + const receiptPath = lodashGet(transaction, 'receipt.source'); + const receiptType = lodashGet(transaction, 'receipt.type'); + const transactionTaxCode = transaction.taxRate?.keyForList; + const transactionTaxAmount = transaction.taxAmount; + const requestType = TransactionUtils.getRequestType(transaction); + const headerTitle = useMemo(() => { + if (iouType === CONST.IOU.TYPE.SPLIT) { + return translate('iou.split'); + } + if (iouType === CONST.IOU.TYPE.TRACK_EXPENSE) { + return translate('iou.trackExpense'); + } + if (iouType === CONST.IOU.TYPE.SEND) { + return translate('common.send'); + } + return translate(TransactionUtils.getHeaderTitleTranslationKey(transaction)); + }, [iouType, transaction, translate]); + + const participants = useMemo( + () => + _.map(transaction.participants, (participant) => { + const participantAccountID = lodashGet(participant, 'accountID', 0); + return participantAccountID ? OptionsListUtils.getParticipantsOption(participant, personalDetails) : OptionsListUtils.getReportOption(participant); + }), + [transaction.participants, personalDetails], + ); + const isPolicyExpenseChat = useMemo(() => ReportUtils.isPolicyExpenseChat(ReportUtils.getRootParentReport(report)), [report]); + const formHasBeenSubmitted = useRef(false); + + useEffect(() => { + if (!transaction?.originalCurrency) { + return; + } + // If user somehow lands on this page without the currency reset, then reset it here. + IOU.setMoneyRequestCurrency_temporaryForRefactor(transactionID, transaction.originalCurrency, true); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + const policyExpenseChat = _.find(participants, (participant) => participant.isPolicyExpenseChat); + if (policyExpenseChat) { + Policy.openDraftWorkspaceRequest(policyExpenseChat.policyID); + } + }, [isOffline, participants, transaction.billable, policy, transactionID]); + + const defaultBillable = lodashGet(policy, 'defaultBillable', false); + useEffect(() => { + IOU.setMoneyRequestBillable_temporaryForRefactor(transactionID, defaultBillable); + }, [transactionID, defaultBillable]); + + useEffect(() => { + if (!transaction.category) { + return; + } + if (policyCategories?.[transaction.category] && !policyCategories[transaction.category].enabled) { + IOU.setMoneyRequestCategory(transactionID, ''); + } + }, [policyCategories, transaction.category, transactionID]); + const defaultCategory = lodashGet( + _.find(lodashGet(policy, 'customUnits', {}), (customUnit) => customUnit.name === CONST.CUSTOM_UNITS.NAME_DISTANCE), + 'defaultCategory', + '', + ); + useEffect(() => { + if (requestType !== CONST.IOU.REQUEST_TYPE.DISTANCE || !_.isEmpty(transaction.category)) { + return; + } + IOU.setMoneyRequestCategory(transactionID, defaultCategory); + // Prevent resetting to default when unselect category + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [transactionID, requestType, defaultCategory]); + + const navigateBack = useCallback(() => { + // If there is not a report attached to the IOU with a reportID, then the participants were manually selected and the user needs taken + // back to the participants step + if (!transaction.participantsAutoAssigned) { + Navigation.goBack(ROUTES.MONEY_REQUEST_STEP_PARTICIPANTS.getRoute(iouType, transactionID, reportID)); + return; + } + IOUUtils.navigateToStartMoneyRequestStep(requestType, iouType, transactionID, reportID); + }, [transaction, iouType, requestType, transactionID, reportID]); + + const navigateToAddReceipt = useCallback(() => { + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_SCAN.getRoute(CONST.IOU.ACTION.CREATE, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams())); + }, [iouType, transactionID, reportID]); + + // When the component mounts, if there is a receipt, see if the image can be read from the disk. If not, redirect the user to the starting step of the flow. + // This is because until the request is saved, the receipt file is only stored in the browsers memory as a blob:// and if the browser is refreshed, then + // the image ceases to exist. The best way for the user to recover from this is to start over from the start of the request process. + useEffect(() => { + const onSuccess = (file) => { + const receipt = file; + receipt.state = file && requestType === CONST.IOU.REQUEST_TYPE.MANUAL ? CONST.IOU.RECEIPT_STATE.OPEN : CONST.IOU.RECEIPT_STATE.SCANREADY; + setReceiptFile(receipt); + }; + + IOU.navigateToStartStepIfScanFileCannotBeRead(receiptFilename, receiptPath, onSuccess, requestType, iouType, transactionID, reportID, receiptType); + }, [receiptType, receiptPath, receiptFilename, requestType, iouType, transactionID, reportID]); + + /** + * @param {Array} selectedParticipants + * @param {String} trimmedComment + * @param {File} [receiptObj] + */ + const requestMoney = useCallback( + (selectedParticipants, trimmedComment, receiptObj, gpsPoints) => { + IOU.requestMoney( + report, + transaction.amount, + transaction.currency, + transaction.created, + transaction.merchant, + currentUserPersonalDetails.login, + currentUserPersonalDetails.accountID, + selectedParticipants[0], + trimmedComment, + receiptObj, + transaction.category, + transaction.tag, + transactionTaxCode, + transactionTaxAmount, + transaction.billable, + policy, + policyTags, + policyCategories, + gpsPoints, + ); + }, + [report, transaction, transactionTaxCode, transactionTaxAmount, currentUserPersonalDetails.login, currentUserPersonalDetails.accountID, policy, policyTags, policyCategories], + ); + + /** + * @param {Array} selectedParticipants + * @param {String} trimmedComment + * @param {File} [receiptObj] + */ + const trackExpense = useCallback( + (selectedParticipants, trimmedComment, receiptObj, gpsPoints) => { + IOU.trackExpense( + report, + transaction.amount, + transaction.currency, + transaction.created, + transaction.merchant, + currentUserPersonalDetails.login, + currentUserPersonalDetails.accountID, + selectedParticipants[0], + trimmedComment, + receiptObj, + transaction.category, + transaction.tag, + transactionTaxCode, + transactionTaxAmount, + transaction.billable, + policy, + policyTags, + policyCategories, + gpsPoints, + ); + }, + [ + report, + transaction.amount, + transaction.currency, + transaction.created, + transaction.merchant, + transaction.category, + transaction.tag, + transaction.billable, + currentUserPersonalDetails.login, + currentUserPersonalDetails.accountID, + transactionTaxCode, + transactionTaxAmount, + policy, + policyTags, + policyCategories, + ], + ); + + /** + * @param {Array} selectedParticipants + * @param {String} trimmedComment + */ + const createDistanceRequest = useCallback( + (selectedParticipants, trimmedComment) => { + IOU.createDistanceRequest( + report, + selectedParticipants[0], + trimmedComment, + transaction.created, + transaction.category, + transaction.tag, + transaction.amount, + transaction.currency, + transaction.merchant, + transaction.billable, + TransactionUtils.getValidWaypoints(transaction.comment.waypoints, true), + policy, + policyTags, + policyCategories, + ); + }, + [policy, policyCategories, policyTags, report, transaction], + ); + + const createTransaction = useCallback( + (selectedParticipants) => { + const trimmedComment = lodashGet(transaction, 'comment.comment', '').trim(); + + // Don't let the form be submitted multiple times while the navigator is waiting to take the user to a different page + if (formHasBeenSubmitted.current) { + return; + } + + formHasBeenSubmitted.current = true; + + // 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 && receiptFile) { + IOU.startSplitBill( + selectedParticipants, + currentUserPersonalDetails.login, + currentUserPersonalDetails.accountID, + trimmedComment, + transaction.category, + transaction.tag, + receiptFile, + report.reportID, + transaction.billable, + ); + 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 && !transaction.isFromGlobalCreate) { + IOU.splitBill( + selectedParticipants, + currentUserPersonalDetails.login, + currentUserPersonalDetails.accountID, + transaction.amount, + trimmedComment, + transaction.currency, + transaction.merchant, + transaction.created, + transaction.category, + transaction.tag, + report.reportID, + transaction.billable, + transaction.iouRequestType, + ); + 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, + currentUserPersonalDetails.login, + currentUserPersonalDetails.accountID, + transaction.amount, + trimmedComment, + transaction.currency, + transaction.merchant, + transaction.created, + transaction.category, + transaction.tag, + transaction.billable, + transaction.iouRequestType, + ); + return; + } + + if (iouType === CONST.IOU.TYPE.TRACK_EXPENSE) { + if (receiptFile) { + // If the transaction amount is zero, then the money is being requested through the "Scan" flow and the GPS coordinates need to be included. + if (transaction.amount === 0) { + getCurrentPosition( + (successData) => { + trackExpense(selectedParticipants, trimmedComment, receiptFile, { + lat: successData.coords.latitude, + long: successData.coords.longitude, + }); + }, + (errorData) => { + Log.info('[IOURequestStepConfirmation] getCurrentPosition failed', false, errorData); + // When there is an error, the money can still be requested, it just won't include the GPS coordinates + trackExpense(selectedParticipants, trimmedComment, receiptFile); + }, + { + // It's OK to get a cached location that is up to an hour old because the only accuracy needed is the country the user is in + maximumAge: 1000 * 60 * 60, + + // 15 seconds, don't wait too long because the server can always fall back to using the IP address + timeout: 15000, + }, + ); + return; + } + + // Otherwise, the money is being requested through the "Manual" flow with an attached image and the GPS coordinates are not needed. + trackExpense(selectedParticipants, trimmedComment, receiptFile); + return; + } + trackExpense(selectedParticipants, trimmedComment, receiptFile); + return; + } + + if (receiptFile) { + // If the transaction amount is zero, then the money is being requested through the "Scan" flow and the GPS coordinates need to be included. + if (transaction.amount === 0) { + getCurrentPosition( + (successData) => { + requestMoney(selectedParticipants, trimmedComment, receiptFile, { + lat: successData.coords.latitude, + long: successData.coords.longitude, + }); + }, + (errorData) => { + Log.info('[IOURequestStepConfirmation] getCurrentPosition failed', false, errorData); + // When there is an error, the money can still be requested, it just won't include the GPS coordinates + requestMoney(selectedParticipants, trimmedComment, receiptFile); + }, + { + // It's OK to get a cached location that is up to an hour old because the only accuracy needed is the country the user is in + maximumAge: 1000 * 60 * 60, + + // 15 seconds, don't wait too long because the server can always fall back to using the IP address + timeout: 15000, + }, + ); + return; + } + + // Otherwise, the money is being requested through the "Manual" flow with an attached image and the GPS coordinates are not needed. + requestMoney(selectedParticipants, trimmedComment, receiptFile); + return; + } + + if (requestType === CONST.IOU.REQUEST_TYPE.DISTANCE) { + createDistanceRequest(selectedParticipants, trimmedComment); + return; + } + + requestMoney(selectedParticipants, trimmedComment); + }, + [ + transaction, + iouType, + receiptFile, + requestType, + requestMoney, + currentUserPersonalDetails.login, + currentUserPersonalDetails.accountID, + report.reportID, + trackExpense, + createDistanceRequest, + ], + ); + + /** + * 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 = transaction.currency; + + const trimmedComment = transaction.comment?.comment ? transaction.comment.comment.trim() : ''; + + const participant = participants[0]; + + if (paymentMethodType === CONST.IOU.PAYMENT_TYPE.ELSEWHERE) { + IOU.sendMoneyElsewhere(report, transaction.amount, currency, trimmedComment, currentUserPersonalDetails.accountID, participant); + return; + } + + if (paymentMethodType === CONST.IOU.PAYMENT_TYPE.EXPENSIFY) { + IOU.sendMoneyWithWallet(report, transaction.amount, currency, trimmedComment, currentUserPersonalDetails.accountID, participant); + } + }, + [transaction.amount, transaction.comment, transaction.currency, participants, currentUserPersonalDetails.accountID, report], + ); + + const addNewParticipant = (option) => { + const newParticipants = _.map(transaction.participants, (participant) => { + if (participant.accountID === option.accountID) { + return {...participant, selected: !participant.selected}; + } + return participant; + }); + IOU.setMoneyRequestParticipants_temporaryForRefactor(transactionID, newParticipants); + }; + + /** + * @param billable + */ + const setBillable = (billable) => { + IOU.setMoneyRequestBillable_temporaryForRefactor(transactionID, billable); + }; + + // This loading indicator is shown because the transaction originalCurrency is being updated later than the component mounts. + // To prevent the component from rendering with the wrong currency, we show a loading indicator until the correct currency is set. + const isLoading = !!transaction?.originalCurrency; + + return ( + + {({safeAreaPaddingBottomStyle}) => ( + + + {isLoading && } + + + + + )} + + ); +} + +IOURequestStepConfirmation.displayName = 'IOURequestStepConfirmation'; + +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'}`, + }, + policyCategories: { + key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${report ? report.policyID : '0'}`, + }, + policyTags: { + key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${report ? report.policyID : '0'}`, + }, + }), +)(IOURequestStepConfirmation); From 860c8891ad83dc1e1d75999e163eedda96fa6d2d Mon Sep 17 00:00:00 2001 From: BrtqKr Date: Tue, 26 Mar 2024 11:31:22 +0100 Subject: [PATCH 04/29] apply prop types, general types changes wip --- src/libs/TransactionUtils.ts | 6 +- .../step/IOURequestStepConfirmation.tsx | 63 ++++++++++--------- 2 files changed, 36 insertions(+), 33 deletions(-) diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index bc94c8fee8fc5..32356d56898af 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -3,6 +3,7 @@ import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; +import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import type {RecentWaypoint, Report, TaxRate, TaxRates, Transaction, TransactionViolation} from '@src/types/onyx'; import type {Comment, Receipt, TransactionChanges, TransactionPendingFieldsKey, Waypoint, WaypointCollection} from '@src/types/onyx/Transaction'; @@ -416,12 +417,13 @@ function getCreated(transaction: OnyxEntry, dateFormat: string = CO /** * Returns the translation key to use for the header title */ -function getHeaderTitleTranslationKey(transaction: Transaction): string { - const headerTitles = { +function getHeaderTitleTranslationKey(transaction: Transaction): TranslationPaths { + const headerTitles: Record = { [CONST.IOU.REQUEST_TYPE.DISTANCE]: 'tabSelector.distance', [CONST.IOU.REQUEST_TYPE.MANUAL]: 'tabSelector.manual', [CONST.IOU.REQUEST_TYPE.SCAN]: 'tabSelector.scan', }; + return headerTitles[getRequestType(transaction)]; } diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index e1a842bf30f8c..8ceb38a2c518d 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -1,52 +1,54 @@ +import type {StackScreenProps} from '@react-navigation/stack'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; -import categoryPropTypes from '@components/categoryPropTypes'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Expensicons from '@components/Icon/Expensicons'; import MoneyRequestConfirmationList from '@components/MoneyTemporaryForRefactorRequestConfirmationList'; import ScreenWrapper from '@components/ScreenWrapper'; -import tagPropTypes from '@components/tagPropTypes'; -import transactionPropTypes from '@components/transactionPropTypes'; -import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '@components/withCurrentUserPersonalDetails'; +import type {CurrentUserPersonalDetails} from '@components/withCurrentUserPersonalDetails'; +import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; +import {openDraftWorkspaceRequest} from '@libs/actions/Policy'; import compose from '@libs/compose'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import getCurrentPosition from '@libs/getCurrentPosition'; import * as IOUUtils from '@libs/IOUUtils'; import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; +import type {MoneyRequestNavigatorParamList} from '@libs/Navigation/types'; 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'; -// import * as Policy from '@userActions/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import {Policy} from '@src/types/onyx'; -import type {PolicyCategories, PolicyTagList} from '@src/types/onyx'; -import IOURequestStepRoutePropTypes from './IOURequestStepRoutePropTypes'; +import type SCREENS from '@src/SCREENS'; +import type {PersonalDetailsList, Policy, PolicyCategories, PolicyTagList, Report, Transaction} from '@src/types/onyx'; import withFullTransactionOrNotFound from './withFullTransactionOrNotFound'; import withWritableReportOrNotFound from './withWritableReportOrNotFound'; +type IOURequestStepConfirmationStackProps = StackScreenProps; + type IOURequestStepConfirmationOnyxProps = { policy: OnyxEntry; policyCategories: OnyxEntry; policyTags: OnyxEntry; }; -type IOURequestStepConfirmationProps = IOURequestStepConfirmationOnyxProps & { - someProp: string; -}; +type IOURequestStepConfirmationProps = IOURequestStepConfirmationOnyxProps & + IOURequestStepConfirmationStackProps & { + currentUserPersonalDetails: CurrentUserPersonalDetails; + personalDetails: PersonalDetailsList; + report: Report; + transaction: Transaction; + }; function IOURequestStepConfirmation({ currentUserPersonalDetails, @@ -59,15 +61,15 @@ function IOURequestStepConfirmation({ params: {iouType, reportID, transactionID}, }, transaction, -}) { +}: IOURequestStepConfirmationProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const {windowWidth} = useWindowDimensions(); const {isOffline} = useNetwork(); const [receiptFile, setReceiptFile] = useState(); - const receiptFilename = lodashGet(transaction, 'filename'); - const receiptPath = lodashGet(transaction, 'receipt.source'); - const receiptType = lodashGet(transaction, 'receipt.type'); + const receiptFilename = transaction.filename; + const receiptPath = transaction.receipt?.source; + const receiptType = transaction.receipt?.type; const transactionTaxCode = transaction.taxRate?.keyForList; const transactionTaxAmount = transaction.taxAmount; const requestType = TransactionUtils.getRequestType(transaction); @@ -86,9 +88,9 @@ function IOURequestStepConfirmation({ const participants = useMemo( () => - _.map(transaction.participants, (participant) => { - const participantAccountID = lodashGet(participant, 'accountID', 0); - return participantAccountID ? OptionsListUtils.getParticipantsOption(participant, personalDetails) : OptionsListUtils.getReportOption(participant); + transaction.participants?.map((participant) => { + const participantAccountID = participant.accountID ?? 0; + return participant && participantAccountID ? OptionsListUtils.getParticipantsOption(participant, personalDetails) : OptionsListUtils.getReportOption(participant); }), [transaction.participants, personalDetails], ); @@ -105,13 +107,13 @@ function IOURequestStepConfirmation({ }, []); useEffect(() => { - const policyExpenseChat = _.find(participants, (participant) => participant.isPolicyExpenseChat); - if (policyExpenseChat) { - Policy.openDraftWorkspaceRequest(policyExpenseChat.policyID); + const policyExpenseChat = participants?.find((participant) => participant.isPolicyExpenseChat); + if (policyExpenseChat?.policyID) { + openDraftWorkspaceRequest(policyExpenseChat.policyID); } }, [isOffline, participants, transaction.billable, policy, transactionID]); - const defaultBillable = lodashGet(policy, 'defaultBillable', false); + const defaultBillable = !!policy?.defaultBillable; useEffect(() => { IOU.setMoneyRequestBillable_temporaryForRefactor(transactionID, defaultBillable); }, [transactionID, defaultBillable]); @@ -124,11 +126,10 @@ function IOURequestStepConfirmation({ IOU.setMoneyRequestCategory(transactionID, ''); } }, [policyCategories, transaction.category, transactionID]); - const defaultCategory = lodashGet( - _.find(lodashGet(policy, 'customUnits', {}), (customUnit) => customUnit.name === CONST.CUSTOM_UNITS.NAME_DISTANCE), - 'defaultCategory', - '', - ); + + const policyDistance = Object.values(policy?.customUnits ?? {}).find((customUnit) => customUnit.name === CONST.CUSTOM_UNITS.NAME_DISTANCE); + const defaultCategory = policyDistance?.defaultCategory ?? ''; + useEffect(() => { if (requestType !== CONST.IOU.REQUEST_TYPE.DISTANCE || !_.isEmpty(transaction.category)) { return; @@ -436,7 +437,7 @@ function IOURequestStepConfirmation({ const trimmedComment = transaction.comment?.comment ? transaction.comment.comment.trim() : ''; - const participant = participants[0]; + const participant = participants?.[0]; if (paymentMethodType === CONST.IOU.PAYMENT_TYPE.ELSEWHERE) { IOU.sendMoneyElsewhere(report, transaction.amount, currency, trimmedComment, currentUserPersonalDetails.accountID, participant); From cb63fd64232791cbc2dd2f93e0e80279c6cff2df Mon Sep 17 00:00:00 2001 From: BrtqKr Date: Tue, 26 Mar 2024 12:43:14 +0100 Subject: [PATCH 05/29] receipts types wip --- src/libs/Navigation/types.ts | 2 +- .../step/IOURequestStepConfirmation.tsx | 29 ++++++++++--------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index a06503d9edf37..4846251cac4a8 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -415,7 +415,7 @@ type MoneyRequestNavigatorParamList = { }; [SCREENS.MONEY_REQUEST.STEP_CONFIRMATION]: { action: keyof typeof CONST.IOU.ACTION; - iouType: string; + iouType: ValueOf; transactionID: string; reportID: string; pageIndex?: string; diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index 8ceb38a2c518d..9b1139757c11f 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -1,27 +1,28 @@ -import type {StackScreenProps} from '@react-navigation/stack'; -import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; -import {View} from 'react-native'; -import type {OnyxEntry} from 'react-native-onyx'; -import {withOnyx} from 'react-native-onyx'; +import type { StackScreenProps } from '@react-navigation/stack'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { View } from 'react-native'; +import type { OnyxEntry } from 'react-native-onyx'; +import { withOnyx } from 'react-native-onyx'; +import { ValueOf } from 'type-fest'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Expensicons from '@components/Icon/Expensicons'; import MoneyRequestConfirmationList from '@components/MoneyTemporaryForRefactorRequestConfirmationList'; import ScreenWrapper from '@components/ScreenWrapper'; -import type {CurrentUserPersonalDetails} from '@components/withCurrentUserPersonalDetails'; +import type { CurrentUserPersonalDetails } from '@components/withCurrentUserPersonalDetails'; import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; -import {openDraftWorkspaceRequest} from '@libs/actions/Policy'; +import { openDraftWorkspaceRequest } from '@libs/actions/Policy'; import compose from '@libs/compose'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import getCurrentPosition from '@libs/getCurrentPosition'; import * as IOUUtils from '@libs/IOUUtils'; import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; -import type {MoneyRequestNavigatorParamList} from '@libs/Navigation/types'; +import type { MoneyRequestNavigatorParamList } from '@libs/Navigation/types'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; @@ -30,9 +31,11 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; -import type {PersonalDetailsList, Policy, PolicyCategories, PolicyTagList, Report, Transaction} from '@src/types/onyx'; +import type { PersonalDetailsList, Policy, PolicyCategories, PolicyTagList, Report, Transaction } from '@src/types/onyx'; import withFullTransactionOrNotFound from './withFullTransactionOrNotFound'; import withWritableReportOrNotFound from './withWritableReportOrNotFound'; +import { Receipt } from '@src/types/onyx/Transaction'; + type IOURequestStepConfirmationStackProps = StackScreenProps; @@ -131,7 +134,7 @@ function IOURequestStepConfirmation({ const defaultCategory = policyDistance?.defaultCategory ?? ''; useEffect(() => { - if (requestType !== CONST.IOU.REQUEST_TYPE.DISTANCE || !_.isEmpty(transaction.category)) { + if (requestType !== CONST.IOU.REQUEST_TYPE.DISTANCE || !!transaction.category) { return; } IOU.setMoneyRequestCategory(transactionID, defaultCategory); @@ -157,8 +160,8 @@ function IOURequestStepConfirmation({ // This is because until the request is saved, the receipt file is only stored in the browsers memory as a blob:// and if the browser is refreshed, then // the image ceases to exist. The best way for the user to recover from this is to start over from the start of the request process. useEffect(() => { - const onSuccess = (file) => { - const receipt = file; + const onSuccess = (file: File) => { + const receipt: Receipt = file; receipt.state = file && requestType === CONST.IOU.REQUEST_TYPE.MANUAL ? CONST.IOU.RECEIPT_STATE.OPEN : CONST.IOU.RECEIPT_STATE.SCANREADY; setReceiptFile(receipt); }; @@ -556,4 +559,4 @@ export default compose( key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${report ? report.policyID : '0'}`, }, }), -)(IOURequestStepConfirmation); +)(IOURequestStepConfirmation); \ No newline at end of file From eb5bfcfb4607cc139f887cbefeb9f9afb4284d6d Mon Sep 17 00:00:00 2001 From: BrtqKr Date: Tue, 26 Mar 2024 15:51:24 +0100 Subject: [PATCH 06/29] callback params cleanups --- src/libs/actions/IOU.ts | 20 +++++---- .../step/IOURequestStepConfirmation.tsx | 41 ++++++++++--------- 2 files changed, 34 insertions(+), 27 deletions(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 5632268ef6cae..868b9348cb01d 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -134,6 +134,11 @@ type OutstandingChildRequest = { hasOutstandingChildRequest?: boolean; }; +type GpsPoint = { + lat: number; + long: number; +}; + let betas: OnyxTypes.Beta[] = []; Onyx.connect({ key: ONYXKEYS.BETAS, @@ -2056,8 +2061,8 @@ function requestMoney( amount: number, currency: string, created: string, - merchant: string, - payeeEmail: string, + merchant: string | undefined, + payeeEmail: string | undefined, payeeAccountID: number, participant: Participant, comment: string, @@ -2070,7 +2075,7 @@ function requestMoney( policy?: OnyxEntry, policyTagList?: OnyxEntry, policyCategories?: OnyxEntry, - gpsPoints = undefined, + gpsPoints?: GpsPoint, ) { // If the report is iou or expense report, we should get the linked chat report to be passed to the getMoneyRequestInformation function const isMoneyRequestReport = ReportUtils.isMoneyRequestReport(report); @@ -5124,14 +5129,14 @@ function unholdRequest(transactionID: string, reportID: string) { } // eslint-disable-next-line rulesdir/no-negated-variables function navigateToStartStepIfScanFileCannotBeRead( - receiptFilename: string, - receiptPath: string, + receiptFilename: string | undefined, + receiptPath: ReceiptSource | undefined, onSuccess: (file: File) => void, requestType: ValueOf, iouType: ValueOf, transactionID: string, reportID: string, - receiptType: string, + receiptType: string | undefined, ) { if (!receiptFilename || !receiptPath) { return; @@ -5145,7 +5150,7 @@ function navigateToStartStepIfScanFileCannotBeRead( } IOUUtils.navigateToStartMoneyRequestStep(requestType, iouType, transactionID, reportID); }; - FileUtils.readFileAsync(receiptPath, receiptFilename, onSuccess, onFailure, receiptType); + FileUtils.readFileAsync(receiptPath.toString(), receiptFilename, onSuccess, onFailure, receiptType); } /** Save the preferred payment method for a policy */ @@ -5153,6 +5158,7 @@ function savePreferredPaymentMethod(policyID: string, paymentMethod: PaymentMeth Onyx.merge(`${ONYXKEYS.NVP_LAST_PAYMENT_METHOD}`, {[policyID]: paymentMethod}); } +export type {GpsPoint}; export { setMoneyRequestParticipants, createDistanceRequest, diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index 9b1139757c11f..516bd936674a8 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -1,28 +1,27 @@ -import type { StackScreenProps } from '@react-navigation/stack'; -import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { View } from 'react-native'; -import type { OnyxEntry } from 'react-native-onyx'; -import { withOnyx } from 'react-native-onyx'; -import { ValueOf } from 'type-fest'; +import type {StackScreenProps} from '@react-navigation/stack'; +import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import {View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Expensicons from '@components/Icon/Expensicons'; import MoneyRequestConfirmationList from '@components/MoneyTemporaryForRefactorRequestConfirmationList'; import ScreenWrapper from '@components/ScreenWrapper'; -import type { CurrentUserPersonalDetails } from '@components/withCurrentUserPersonalDetails'; +import type {CurrentUserPersonalDetails} from '@components/withCurrentUserPersonalDetails'; import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; -import { openDraftWorkspaceRequest } from '@libs/actions/Policy'; +import {openDraftWorkspaceRequest} from '@libs/actions/Policy'; import compose from '@libs/compose'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import getCurrentPosition from '@libs/getCurrentPosition'; import * as IOUUtils from '@libs/IOUUtils'; import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; -import type { MoneyRequestNavigatorParamList } from '@libs/Navigation/types'; +import type {MoneyRequestNavigatorParamList} from '@libs/Navigation/types'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; @@ -31,11 +30,11 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; -import type { PersonalDetailsList, Policy, PolicyCategories, PolicyTagList, Report, Transaction } from '@src/types/onyx'; +import type {PersonalDetailsList, Policy, PolicyCategories, PolicyTagList, Report, Transaction} from '@src/types/onyx'; +import type {Participant} from '@src/types/onyx/IOU'; +import type {Receipt} from '@src/types/onyx/Transaction'; import withFullTransactionOrNotFound from './withFullTransactionOrNotFound'; import withWritableReportOrNotFound from './withWritableReportOrNotFound'; -import { Receipt } from '@src/types/onyx/Transaction'; - type IOURequestStepConfirmationStackProps = StackScreenProps; @@ -69,13 +68,15 @@ function IOURequestStepConfirmation({ const {translate} = useLocalize(); const {windowWidth} = useWindowDimensions(); const {isOffline} = useNetwork(); - const [receiptFile, setReceiptFile] = useState(); + const [receiptFile, setReceiptFile] = useState(); + const receiptFilename = transaction.filename; const receiptPath = transaction.receipt?.source; const receiptType = transaction.receipt?.type; const transactionTaxCode = transaction.taxRate?.keyForList; const transactionTaxAmount = transaction.taxAmount; const requestType = TransactionUtils.getRequestType(transaction); + const headerTitle = useMemo(() => { if (iouType === CONST.IOU.TYPE.SPLIT) { return translate('iou.split'); @@ -175,7 +176,7 @@ function IOURequestStepConfirmation({ * @param {File} [receiptObj] */ const requestMoney = useCallback( - (selectedParticipants, trimmedComment, receiptObj, gpsPoints) => { + (selectedParticipants: Participant[], trimmedComment: string, receiptObj: Receipt, gpsPoints: IOU.GpsPoint) => { IOU.requestMoney( report, transaction.amount, @@ -207,7 +208,7 @@ function IOURequestStepConfirmation({ * @param {File} [receiptObj] */ const trackExpense = useCallback( - (selectedParticipants, trimmedComment, receiptObj, gpsPoints) => { + (selectedParticipants: Participant[], trimmedComment: string, receiptObj: Receipt, gpsPoints?: IOU.GpsPoint) => { IOU.trackExpense( report, transaction.amount, @@ -254,7 +255,7 @@ function IOURequestStepConfirmation({ * @param {String} trimmedComment */ const createDistanceRequest = useCallback( - (selectedParticipants, trimmedComment) => { + (selectedParticipants: Participant[], trimmedComment: string) => { IOU.createDistanceRequest( report, selectedParticipants[0], @@ -276,8 +277,8 @@ function IOURequestStepConfirmation({ ); const createTransaction = useCallback( - (selectedParticipants) => { - const trimmedComment = lodashGet(transaction, 'comment.comment', '').trim(); + (selectedParticipants: Participant[]) => { + const trimmedComment = (transaction.comment.comment ?? '').trim(); // Don't let the form be submitted multiple times while the navigator is waiting to take the user to a different page if (formHasBeenSubmitted.current) { @@ -455,7 +456,7 @@ function IOURequestStepConfirmation({ ); const addNewParticipant = (option) => { - const newParticipants = _.map(transaction.participants, (participant) => { + const newParticipants = transaction.participants?.map((participant) => { if (participant.accountID === option.accountID) { return {...participant, selected: !participant.selected}; } @@ -559,4 +560,4 @@ export default compose( key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${report ? report.policyID : '0'}`, }, }), -)(IOURequestStepConfirmation); \ No newline at end of file +)(IOURequestStepConfirmation); From 6e3d1d6966f423ff9343b790aae18323b6a402f5 Mon Sep 17 00:00:00 2001 From: BrtqKr Date: Wed, 27 Mar 2024 13:53:41 +0100 Subject: [PATCH 07/29] type compose, type function params --- src/libs/TransactionUtils.ts | 2 +- src/libs/actions/IOU.ts | 4 ++-- .../iou/request/step/IOURequestStepConfirmation.tsx | 13 ++++++------- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index 32356d56898af..4022418494001 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -505,7 +505,7 @@ function getWaypointIndex(key: string): number { /** * Filters the waypoints which are valid and returns those */ -function getValidWaypoints(waypoints: WaypointCollection, reArrangeIndexes = false): WaypointCollection { +function getValidWaypoints(waypoints: WaypointCollection | undefined = {}, reArrangeIndexes = false): WaypointCollection { const sortedIndexes = Object.keys(waypoints) .map(getWaypointIndex) .sort((a, b) => a - b); diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 868b9348cb01d..d142aae5dcd2b 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -2160,7 +2160,7 @@ function trackExpense( currency: string, created: string, merchant: string, - payeeEmail: string, + payeeEmail: string | undefined, payeeAccountID: number, participant: Participant, comment: string, @@ -2173,7 +2173,7 @@ function trackExpense( policy?: OnyxEntry, policyTagList?: OnyxEntry, policyCategories?: OnyxEntry, - gpsPoints = undefined, + gpsPoints?: GpsPoint, ) { const currentCreated = DateUtils.enrichMoneyRequestTimestamp(created); const { diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index 516bd936674a8..4b334f5c19d46 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -3,6 +3,7 @@ import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; +import type {ValueOf} from 'type-fest'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -42,12 +43,12 @@ type IOURequestStepConfirmationOnyxProps = { policy: OnyxEntry; policyCategories: OnyxEntry; policyTags: OnyxEntry; + personalDetails: OnyxEntry; }; type IOURequestStepConfirmationProps = IOURequestStepConfirmationOnyxProps & IOURequestStepConfirmationStackProps & { currentUserPersonalDetails: CurrentUserPersonalDetails; - personalDetails: PersonalDetailsList; report: Report; transaction: Transaction; }; @@ -436,7 +437,7 @@ function IOURequestStepConfirmation({ * @param {String} paymentMethodType */ const sendMoney = useCallback( - (paymentMethodType) => { + (paymentMethodType: ValueOf) => { const currency = transaction.currency; const trimmedComment = transaction.comment?.comment ? transaction.comment.comment.trim() : ''; @@ -468,7 +469,7 @@ function IOURequestStepConfirmation({ /** * @param billable */ - const setBillable = (billable) => { + const setBillable = (billable: boolean) => { IOU.setMoneyRequestBillable_temporaryForRefactor(transactionID, billable); }; @@ -543,13 +544,11 @@ export default compose( withCurrentUserPersonalDetails, withWritableReportOrNotFound, withFullTransactionOrNotFound, - withOnyx({ + // eslint-disable-next-line rulesdir/no-multiple-onyx-in-file + 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'}`, }, From cafcdab921958ff2cbdeb9bca6f99b8866665313 Mon Sep 17 00:00:00 2001 From: BrtqKr Date: Wed, 27 Mar 2024 17:06:57 +0100 Subject: [PATCH 08/29] cleanup optional params, participants wip --- .../withCurrentUserPersonalDetails.tsx | 2 +- src/libs/OptionsListUtils.ts | 2 +- src/libs/actions/IOU.ts | 4 +- .../step/IOURequestStepConfirmation.tsx | 109 ++++++++++-------- 4 files changed, 68 insertions(+), 49 deletions(-) diff --git a/src/components/withCurrentUserPersonalDetails.tsx b/src/components/withCurrentUserPersonalDetails.tsx index 8431ededcb565..03b9cd1acac4b 100644 --- a/src/components/withCurrentUserPersonalDetails.tsx +++ b/src/components/withCurrentUserPersonalDetails.tsx @@ -39,7 +39,7 @@ export default function ) /** * Get the participant option for a report. */ -function getParticipantsOption(participant: ReportUtils.OptionData, personalDetails: OnyxEntry): Participant { +function getParticipantsOption(participant: ReportUtils.OptionData | Participant, personalDetails: OnyxEntry): Participant { const detail = getPersonalDetailsForAccountIDs([participant.accountID ?? -1], personalDetails)[participant.accountID ?? -1]; // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const login = detail?.login || participant.login || ''; diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index d142aae5dcd2b..d36446d79b861 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -387,7 +387,7 @@ function setMoneyRequestBillable_temporaryForRefactor(transactionID: string, bil } // eslint-disable-next-line @typescript-eslint/naming-convention -function setMoneyRequestParticipants_temporaryForRefactor(transactionID: string, participants: Participant[]) { +function setMoneyRequestParticipants_temporaryForRefactor(transactionID: string, participants?: Participant[] = []) { Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {participants}); } @@ -2061,7 +2061,7 @@ function requestMoney( amount: number, currency: string, created: string, - merchant: string | undefined, + merchant: string, payeeEmail: string | undefined, payeeAccountID: number, participant: Participant, diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index 4b334f5c19d46..f74dfbcea977b 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -177,7 +177,10 @@ function IOURequestStepConfirmation({ * @param {File} [receiptObj] */ const requestMoney = useCallback( - (selectedParticipants: Participant[], trimmedComment: string, receiptObj: Receipt, gpsPoints: IOU.GpsPoint) => { + (selectedParticipants: Participant[], trimmedComment: string, receiptObj?: Receipt, gpsPoints?: IOU.GpsPoint) => { + if (!receiptObj) { + return; + } IOU.requestMoney( report, transaction.amount, @@ -209,7 +212,10 @@ function IOURequestStepConfirmation({ * @param {File} [receiptObj] */ const trackExpense = useCallback( - (selectedParticipants: Participant[], trimmedComment: string, receiptObj: Receipt, gpsPoints?: IOU.GpsPoint) => { + (selectedParticipants: Participant[], trimmedComment: string, receiptObj?: Receipt, gpsPoints?: IOU.GpsPoint) => { + if (!receiptObj) { + return; + } IOU.trackExpense( report, transaction.amount, @@ -290,57 +296,63 @@ function IOURequestStepConfirmation({ // 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 && receiptFile) { - IOU.startSplitBill( - selectedParticipants, - currentUserPersonalDetails.login, - currentUserPersonalDetails.accountID, - trimmedComment, - transaction.category, - transaction.tag, - receiptFile, - report.reportID, - transaction.billable, - ); + if (currentUserPersonalDetails.login && transaction.category && transaction.tag) { + IOU.startSplitBill( + selectedParticipants, + currentUserPersonalDetails.login, + currentUserPersonalDetails.accountID, + trimmedComment, + transaction.category, + transaction.tag, + receiptFile, + report.reportID, + transaction.billable, + ); + } 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 && !transaction.isFromGlobalCreate) { - IOU.splitBill( - selectedParticipants, - currentUserPersonalDetails.login, - currentUserPersonalDetails.accountID, - transaction.amount, - trimmedComment, - transaction.currency, - transaction.merchant, - transaction.created, - transaction.category, - transaction.tag, - report.reportID, - transaction.billable, - transaction.iouRequestType, - ); + if (currentUserPersonalDetails.login && transaction.category && transaction.tag) { + IOU.splitBill( + selectedParticipants, + currentUserPersonalDetails.login, + currentUserPersonalDetails.accountID, + transaction.amount, + trimmedComment, + transaction.currency, + transaction.merchant, + transaction.created, + transaction.category, + transaction.tag, + report.reportID, + transaction.billable, + transaction.iouRequestType, + ); + } 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, - currentUserPersonalDetails.login, - currentUserPersonalDetails.accountID, - transaction.amount, - trimmedComment, - transaction.currency, - transaction.merchant, - transaction.created, - transaction.category, - transaction.tag, - transaction.billable, - transaction.iouRequestType, - ); + if (currentUserPersonalDetails.login && transaction.category && transaction.tag) { + IOU.splitBillAndOpenReport( + selectedParticipants, + currentUserPersonalDetails.login, + currentUserPersonalDetails.accountID, + transaction.amount, + trimmedComment, + transaction.currency, + transaction.merchant, + transaction.created, + transaction.category, + transaction.tag, + !!transaction.billable, + transaction.iouRequestType, + ); + } return; } @@ -444,7 +456,7 @@ function IOURequestStepConfirmation({ const participant = participants?.[0]; - if (paymentMethodType === CONST.IOU.PAYMENT_TYPE.ELSEWHERE) { + if (paymentMethodType === CONST.IOU.PAYMENT_TYPE.ELSEWHERE && participant) { IOU.sendMoneyElsewhere(report, transaction.amount, currency, trimmedComment, currentUserPersonalDetails.accountID, participant); return; } @@ -456,7 +468,7 @@ function IOURequestStepConfirmation({ [transaction.amount, transaction.comment, transaction.currency, participants, currentUserPersonalDetails.accountID, report], ); - const addNewParticipant = (option) => { + const addNewParticipant = (option: Participant) => { const newParticipants = transaction.participants?.map((participant) => { if (participant.accountID === option.accountID) { return {...participant, selected: !participant.selected}; @@ -505,7 +517,7 @@ function IOURequestStepConfirmation({ hasMultipleParticipants={iouType === CONST.IOU.TYPE.SPLIT} selectedParticipants={participants} iouAmount={transaction.amount} - iouComment={lodashGet(transaction, 'comment.comment', '')} + iouComment={transaction.comment.comment ?? ''} iouCurrencyCode={transaction.currency} iouIsBillable={transaction.billable} onToggleBillable={setBillable} @@ -530,6 +542,13 @@ function IOURequestStepConfirmation({ iouCreated={transaction.created} isDistanceRequest={requestType === CONST.IOU.REQUEST_TYPE.DISTANCE} shouldShowSmartScanFields={requestType !== CONST.IOU.REQUEST_TYPE.SCAN} + // reportActionID={undefined} + // hasSmartScanFailed={undefined} + // isEditingSplitBill={undefined} + // isReadOnly={undefined} + // isScanRequest={undefined} + // listStyles={undefined} + // payeePersonalDetails={undefined} /> From c04e0a4728ed8ffcbdec40d7339ece3132b5860b Mon Sep 17 00:00:00 2001 From: BrtqKr Date: Fri, 29 Mar 2024 12:25:46 +0100 Subject: [PATCH 09/29] make function params named, modify types, add transaction checks --- src/libs/TransactionUtils.ts | 19 +- src/libs/actions/IOU.ts | 110 +++++--- .../step/IOURequestStepConfirmation.js | 78 +++--- .../step/IOURequestStepConfirmation.tsx | 258 ++++++++---------- .../iou/steps/MoneyRequestConfirmPage.js | 58 ++-- tests/actions/IOUTest.ts | 30 +- 6 files changed, 281 insertions(+), 272 deletions(-) diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index 4022418494001..7c5f96b1aa501 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -48,19 +48,22 @@ function isDistanceRequest(transaction: OnyxEntry): boolean { function isScanRequest(transaction: Transaction): boolean { // This is used during the request creation flow before the transaction has been saved to the server if (lodashHas(transaction, 'iouRequestType')) { - return transaction.iouRequestType === CONST.IOU.REQUEST_TYPE.SCAN; + return transaction?.iouRequestType === CONST.IOU.REQUEST_TYPE.SCAN; } return Boolean(transaction?.receipt?.source); } -function getRequestType(transaction: Transaction): ValueOf { - if (isDistanceRequest(transaction)) { - return CONST.IOU.REQUEST_TYPE.DISTANCE; - } - if (isScanRequest(transaction)) { - return CONST.IOU.REQUEST_TYPE.SCAN; +function getRequestType(transaction: OnyxEntry): ValueOf { + if (transaction) { + if (isDistanceRequest(transaction)) { + return CONST.IOU.REQUEST_TYPE.DISTANCE; + } + if (isScanRequest(transaction)) { + return CONST.IOU.REQUEST_TYPE.SCAN; + } } + return CONST.IOU.REQUEST_TYPE.MANUAL; } @@ -417,7 +420,7 @@ function getCreated(transaction: OnyxEntry, dateFormat: string = CO /** * Returns the translation key to use for the header title */ -function getHeaderTitleTranslationKey(transaction: Transaction): TranslationPaths { +function getHeaderTitleTranslationKey(transaction: OnyxEntry): TranslationPaths { const headerTitles: Record = { [CONST.IOU.REQUEST_TYPE.DISTANCE]: 'tabSelector.distance', [CONST.IOU.REQUEST_TYPE.MANUAL]: 'tabSelector.manual', diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index d36446d79b861..780ffcf5f65e5 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -387,7 +387,7 @@ function setMoneyRequestBillable_temporaryForRefactor(transactionID: string, bil } // eslint-disable-next-line @typescript-eslint/naming-convention -function setMoneyRequestParticipants_temporaryForRefactor(transactionID: string, participants?: Participant[] = []) { +function setMoneyRequestParticipants_temporaryForRefactor(transactionID: string, participants: Participant[] | undefined = []) { Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {participants}); } @@ -2615,25 +2615,41 @@ function createSplitsAndOnyxData( }; } +type SplitBillActionsParams = { + participants: Participant[]; + currentUserLogin: string; + currentUserAccountID: number; + amount: number; + comment: string; + currency: string; + merchant: string; + created: string; + category?: string; + tag?: string; + billable?: boolean; + iouRequestType?: IOURequestType; + existingSplitChatReportID?: string; +}; + /** * @param amount - always in smallest currency unit * @param existingSplitChatReportID - Either a group DM or a workspace chat */ -function splitBill( - participants: Participant[], - currentUserLogin: string, - currentUserAccountID: number, - amount: number, - comment: string, - currency: string, - merchant: string, - created: string, - category: string, - tag: string, - existingSplitChatReportID = '', +function splitBill({ + participants, + currentUserLogin, + currentUserAccountID, + amount, + comment, + currency, + merchant, + created, + category = '', + tag = '', billable = false, - iouRequestType: IOURequestType = CONST.IOU.REQUEST_TYPE.MANUAL, -) { + iouRequestType = CONST.IOU.REQUEST_TYPE.MANUAL, + existingSplitChatReportID = '', +}: SplitBillActionsParams) { const currentCreated = DateUtils.enrichMoneyRequestTimestamp(created); const {splitData, splits, onyxData} = createSplitsAndOnyxData( participants, @@ -2678,20 +2694,20 @@ function splitBill( /** * @param amount - always in the smallest currency unit */ -function splitBillAndOpenReport( - participants: Participant[], - currentUserLogin: string, - currentUserAccountID: number, - amount: number, - comment: string, - currency: string, - merchant: string, - created: string, - category: string, - tag: string, - billable: boolean, - iouRequestType: IOURequestType = CONST.IOU.REQUEST_TYPE.MANUAL, -) { +function splitBillAndOpenReport({ + participants, + currentUserLogin, + currentUserAccountID, + amount, + comment, + currency, + merchant, + created, + category = '', + tag = '', + billable = false, + iouRequestType = CONST.IOU.REQUEST_TYPE.MANUAL, +}: SplitBillActionsParams) { const currentCreated = DateUtils.enrichMoneyRequestTimestamp(created); const {splitData, splits, onyxData} = createSplitsAndOnyxData( participants, @@ -2733,22 +2749,34 @@ function splitBillAndOpenReport( Report.notifyNewAction(splitData.chatReportID, currentUserAccountID); } +type StartSplitBilActionParams = { + participants: Participant[]; + currentUserLogin: string; + currentUserAccountID: number; + comment: string; + receipt: Receipt; + existingSplitChatReportID?: string; + billable?: boolean; + category: string | undefined; + tag: string | undefined; +}; + /** Used exclusively for starting a split bill request that contains a receipt, the split request will be completed once the receipt is scanned * or user enters details manually. * * @param existingSplitChatReportID - Either a group DM or a workspace chat */ -function startSplitBill( - participants: Participant[], - currentUserLogin: string, - currentUserAccountID: number, - comment: string, - category: string, - tag: string, - receipt: Receipt, +function startSplitBill({ + participants, + currentUserLogin, + currentUserAccountID, + comment, + receipt, existingSplitChatReportID = '', billable = false, -) { + category = '', + tag = '', +}: StartSplitBilActionParams) { const currentUserEmailForIOUSplit = PhoneNumber.addSMSDomainIfPhoneNumber(currentUserLogin); const participantAccountIDs = participants.map((participant) => Number(participant.accountID)); const existingSplitChatReport = @@ -3008,7 +3036,7 @@ function startSplitBill( splits: JSON.stringify(splits), receipt, comment, - category, + category: category ?? '', tag, isFromGroupDM: !existingSplitChatReport, billable, @@ -4465,7 +4493,7 @@ function getPayMoneyRequestParams(chatReport: OnyxTypes.Report, iouReport: OnyxT * @param managerID - Account ID of the person sending the money * @param recipient - The user receiving the money */ -function sendMoneyElsewhere(report: OnyxTypes.Report, amount: number, currency: string, comment: string, managerID: number, recipient: Participant) { +function sendMoneyElsewhere(report: OnyxTypes.Report, amount: number, currency: string, comment: string, managerID: number, recipient: Participant | ReportUtils.OptionData) { const {params, optimisticData, successData, failureData} = getSendMoneyParams(report, amount, currency, comment, CONST.IOU.PAYMENT_TYPE.ELSEWHERE, managerID, recipient); API.write(WRITE_COMMANDS.SEND_MONEY_ELSEWHERE, params, {optimisticData, successData, failureData}); @@ -4479,7 +4507,7 @@ function sendMoneyElsewhere(report: OnyxTypes.Report, amount: number, currency: * @param managerID - Account ID of the person sending the money * @param recipient - The user receiving the money */ -function sendMoneyWithWallet(report: OnyxTypes.Report, amount: number, currency: string, comment: string, managerID: number, recipient: Participant) { +function sendMoneyWithWallet(report: OnyxTypes.Report, amount: number, currency: string, comment: string, managerID: number, recipient: Participant | ReportUtils.OptionData) { const {params, optimisticData, successData, failureData} = getSendMoneyParams(report, amount, currency, comment, CONST.IOU.PAYMENT_TYPE.EXPENSIFY, managerID, recipient); API.write(WRITE_COMMANDS.SEND_MONEY_WITH_WALLET, params, {optimisticData, successData, failureData}); diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.js b/src/pages/iou/request/step/IOURequestStepConfirmation.js index 435121a760289..46694de541253 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.js +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.js @@ -309,57 +309,57 @@ function IOURequestStepConfirmation({ // 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 && receiptFile) { - IOU.startSplitBill( - selectedParticipants, - currentUserPersonalDetails.login, - currentUserPersonalDetails.accountID, - trimmedComment, - transaction.category, - transaction.tag, + IOU.startSplitBill({ + participants: selectedParticipants, + currentUserLogin: currentUserPersonalDetails.login, + currentUserAccountID: currentUserPersonalDetails.accountID, + comment: trimmedComment, receiptFile, - report.reportID, - transaction.billable, - ); + existingSplitChatReportID: report.reportID, + billable: transaction.billable, + category: transaction.category, + tag: transaction.tag, + }); 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 && !transaction.isFromGlobalCreate) { - IOU.splitBill( - selectedParticipants, - currentUserPersonalDetails.login, - currentUserPersonalDetails.accountID, - transaction.amount, - trimmedComment, - transaction.currency, - transaction.merchant, - transaction.created, - transaction.category, - transaction.tag, - report.reportID, - transaction.billable, - transaction.iouRequestType, - ); + IOU.splitBill({ + participants: selectedParticipants, + currentUserLogin: currentUserPersonalDetails.login, + currentUserAccountID: currentUserPersonalDetails.accountID, + amount: transaction.amount, + comment: trimmedComment, + currency: transaction.currency, + merchant: transaction.merchant, + created: transaction.created, + category: transaction.category, + tag: transaction.tag, + existingSplitChatReportID: report.reportID, + billable: transaction.billable, + iouRequestType: transaction.iouRequestType, + }); 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, - currentUserPersonalDetails.login, - currentUserPersonalDetails.accountID, - transaction.amount, - trimmedComment, - transaction.currency, - transaction.merchant, - transaction.created, - transaction.category, - transaction.tag, - transaction.billable, - transaction.iouRequestType, - ); + IOU.splitBillAndOpenReport({ + participants: selectedParticipants, + currentUserLogin: currentUserPersonalDetails.login, + currentUserAccountID: currentUserPersonalDetails.accountID, + amount: transaction.amount, + comment: trimmedComment, + currency: transaction.currency, + merchant: transaction.merchant, + created: transaction.created, + category: transaction.category, + tag: transaction.tag, + billable: transaction.billable, + iouRequestType: transaction.iouRequestType, + }); return; } diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index f74dfbcea977b..c62f6dd64685f 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -1,3 +1,4 @@ +/* eslint-disable rulesdir/no-negated-variables */ import type {StackScreenProps} from '@react-navigation/stack'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; @@ -9,14 +10,12 @@ import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Expensicons from '@components/Icon/Expensicons'; import MoneyRequestConfirmationList from '@components/MoneyTemporaryForRefactorRequestConfirmationList'; import ScreenWrapper from '@components/ScreenWrapper'; -import type {CurrentUserPersonalDetails} from '@components/withCurrentUserPersonalDetails'; -import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; +import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import {openDraftWorkspaceRequest} from '@libs/actions/Policy'; -import compose from '@libs/compose'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import getCurrentPosition from '@libs/getCurrentPosition'; import * as IOUUtils from '@libs/IOUUtils'; @@ -31,11 +30,12 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; -import type {PersonalDetailsList, Policy, PolicyCategories, PolicyTagList, Report, Transaction} from '@src/types/onyx'; +import type {PersonalDetailsList, Policy, PolicyCategories, PolicyTagList, Transaction} from '@src/types/onyx'; import type {Participant} from '@src/types/onyx/IOU'; import type {Receipt} from '@src/types/onyx/Transaction'; import withFullTransactionOrNotFound from './withFullTransactionOrNotFound'; import withWritableReportOrNotFound from './withWritableReportOrNotFound'; +import type {WithWritableReportOrNotFoundProps} from './withWritableReportOrNotFound'; type IOURequestStepConfirmationStackProps = StackScreenProps; @@ -47,14 +47,12 @@ type IOURequestStepConfirmationOnyxProps = { }; type IOURequestStepConfirmationProps = IOURequestStepConfirmationOnyxProps & - IOURequestStepConfirmationStackProps & { - currentUserPersonalDetails: CurrentUserPersonalDetails; - report: Report; - transaction: Transaction; + IOURequestStepConfirmationStackProps & + WithWritableReportOrNotFoundProps & { + transaction: OnyxEntry; }; function IOURequestStepConfirmation({ - currentUserPersonalDetails, personalDetails, policy, policyTags, @@ -65,17 +63,19 @@ function IOURequestStepConfirmation({ }, transaction, }: IOURequestStepConfirmationProps) { + const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const styles = useThemeStyles(); const {translate} = useLocalize(); const {windowWidth} = useWindowDimensions(); const {isOffline} = useNetwork(); const [receiptFile, setReceiptFile] = useState(); - const receiptFilename = transaction.filename; - const receiptPath = transaction.receipt?.source; - const receiptType = transaction.receipt?.type; - const transactionTaxCode = transaction.taxRate?.keyForList; - const transactionTaxAmount = transaction.taxAmount; + const receiptFilename = transaction?.filename; + const receiptPath = transaction?.receipt?.source; + const receiptType = transaction?.receipt?.type; + const transactionTaxCode = transaction?.taxRate?.keyForList; + const transactionTaxAmount = transaction?.taxAmount; + const requestType = TransactionUtils.getRequestType(transaction); const headerTitle = useMemo(() => { @@ -93,11 +93,11 @@ function IOURequestStepConfirmation({ const participants = useMemo( () => - transaction.participants?.map((participant) => { + transaction?.participants?.map((participant) => { const participantAccountID = participant.accountID ?? 0; return participant && participantAccountID ? OptionsListUtils.getParticipantsOption(participant, personalDetails) : OptionsListUtils.getReportOption(participant); }), - [transaction.participants, personalDetails], + [transaction?.participants, personalDetails], ); const isPolicyExpenseChat = useMemo(() => ReportUtils.isPolicyExpenseChat(ReportUtils.getRootParentReport(report)), [report]); const formHasBeenSubmitted = useRef(false); @@ -116,7 +116,7 @@ function IOURequestStepConfirmation({ if (policyExpenseChat?.policyID) { openDraftWorkspaceRequest(policyExpenseChat.policyID); } - }, [isOffline, participants, transaction.billable, policy, transactionID]); + }, [isOffline, participants, transaction?.billable, policy, transactionID]); const defaultBillable = !!policy?.defaultBillable; useEffect(() => { @@ -124,19 +124,19 @@ function IOURequestStepConfirmation({ }, [transactionID, defaultBillable]); useEffect(() => { - if (!transaction.category) { + if (!transaction?.category) { return; } if (policyCategories?.[transaction.category] && !policyCategories[transaction.category].enabled) { IOU.setMoneyRequestCategory(transactionID, ''); } - }, [policyCategories, transaction.category, transactionID]); + }, [policyCategories, transaction?.category, transactionID]); const policyDistance = Object.values(policy?.customUnits ?? {}).find((customUnit) => customUnit.name === CONST.CUSTOM_UNITS.NAME_DISTANCE); const defaultCategory = policyDistance?.defaultCategory ?? ''; useEffect(() => { - if (requestType !== CONST.IOU.REQUEST_TYPE.DISTANCE || !!transaction.category) { + if (requestType !== CONST.IOU.REQUEST_TYPE.DISTANCE || !!transaction?.category) { return; } IOU.setMoneyRequestCategory(transactionID, defaultCategory); @@ -147,7 +147,7 @@ function IOURequestStepConfirmation({ const navigateBack = useCallback(() => { // If there is not a report attached to the IOU with a reportID, then the participants were manually selected and the user needs taken // back to the participants step - if (!transaction.participantsAutoAssigned) { + if (!transaction?.participantsAutoAssigned) { Navigation.goBack(ROUTES.MONEY_REQUEST_STEP_PARTICIPANTS.getRoute(iouType, transactionID, reportID)); return; } @@ -178,7 +178,7 @@ function IOURequestStepConfirmation({ */ const requestMoney = useCallback( (selectedParticipants: Participant[], trimmedComment: string, receiptObj?: Receipt, gpsPoints?: IOU.GpsPoint) => { - if (!receiptObj) { + if (!receiptObj || !report || !transaction) { return; } IOU.requestMoney( @@ -206,14 +206,9 @@ function IOURequestStepConfirmation({ [report, transaction, transactionTaxCode, transactionTaxAmount, currentUserPersonalDetails.login, currentUserPersonalDetails.accountID, policy, policyTags, policyCategories], ); - /** - * @param {Array} selectedParticipants - * @param {String} trimmedComment - * @param {File} [receiptObj] - */ const trackExpense = useCallback( (selectedParticipants: Participant[], trimmedComment: string, receiptObj?: Receipt, gpsPoints?: IOU.GpsPoint) => { - if (!receiptObj) { + if (!receiptObj || !report || !transaction) { return; } IOU.trackExpense( @@ -238,31 +233,14 @@ function IOURequestStepConfirmation({ gpsPoints, ); }, - [ - report, - transaction.amount, - transaction.currency, - transaction.created, - transaction.merchant, - transaction.category, - transaction.tag, - transaction.billable, - currentUserPersonalDetails.login, - currentUserPersonalDetails.accountID, - transactionTaxCode, - transactionTaxAmount, - policy, - policyTags, - policyCategories, - ], + [report, transaction, currentUserPersonalDetails.login, currentUserPersonalDetails.accountID, transactionTaxCode, transactionTaxAmount, policy, policyTags, policyCategories], ); - /** - * @param {Array} selectedParticipants - * @param {String} trimmedComment - */ const createDistanceRequest = useCallback( (selectedParticipants: Participant[], trimmedComment: string) => { + if (!report || !transaction) { + return; + } IOU.createDistanceRequest( report, selectedParticipants[0], @@ -285,7 +263,7 @@ function IOURequestStepConfirmation({ const createTransaction = useCallback( (selectedParticipants: Participant[]) => { - const trimmedComment = (transaction.comment.comment ?? '').trim(); + const trimmedComment = (transaction?.comment.comment ?? '').trim(); // Don't let the form be submitted multiple times while the navigator is waiting to take the user to a different page if (formHasBeenSubmitted.current) { @@ -296,68 +274,68 @@ function IOURequestStepConfirmation({ // 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 && receiptFile) { - if (currentUserPersonalDetails.login && transaction.category && transaction.tag) { - IOU.startSplitBill( - selectedParticipants, - currentUserPersonalDetails.login, - currentUserPersonalDetails.accountID, - trimmedComment, - transaction.category, - transaction.tag, - receiptFile, - report.reportID, - transaction.billable, - ); + if (currentUserPersonalDetails.login && !!transaction) { + IOU.startSplitBill({ + participants: selectedParticipants, + currentUserLogin: currentUserPersonalDetails.login, + currentUserAccountID: currentUserPersonalDetails.accountID, + comment: trimmedComment, + receipt: receiptFile, + existingSplitChatReportID: report?.reportID, + billable: transaction.billable, + category: transaction.category, + tag: transaction.tag, + }); } 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 && !transaction.isFromGlobalCreate) { - if (currentUserPersonalDetails.login && transaction.category && transaction.tag) { - IOU.splitBill( - selectedParticipants, - currentUserPersonalDetails.login, - currentUserPersonalDetails.accountID, - transaction.amount, - trimmedComment, - transaction.currency, - transaction.merchant, - transaction.created, - transaction.category, - transaction.tag, - report.reportID, - transaction.billable, - transaction.iouRequestType, - ); + if (iouType === CONST.IOU.TYPE.SPLIT && !transaction?.isFromGlobalCreate) { + if (currentUserPersonalDetails.login && !!transaction) { + IOU.splitBill({ + participants: selectedParticipants, + currentUserLogin: currentUserPersonalDetails.login, + currentUserAccountID: currentUserPersonalDetails.accountID, + amount: transaction.amount, + comment: trimmedComment, + currency: transaction.currency, + merchant: transaction.merchant, + created: transaction.created, + category: transaction.category, + tag: transaction.tag, + existingSplitChatReportID: report?.reportID, + billable: transaction.billable, + iouRequestType: transaction.iouRequestType, + }); } 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) { - if (currentUserPersonalDetails.login && transaction.category && transaction.tag) { - IOU.splitBillAndOpenReport( - selectedParticipants, - currentUserPersonalDetails.login, - currentUserPersonalDetails.accountID, - transaction.amount, - trimmedComment, - transaction.currency, - transaction.merchant, - transaction.created, - transaction.category, - transaction.tag, - !!transaction.billable, - transaction.iouRequestType, - ); + if (currentUserPersonalDetails.login && !!transaction) { + IOU.splitBillAndOpenReport({ + participants: selectedParticipants, + currentUserLogin: currentUserPersonalDetails.login, + currentUserAccountID: currentUserPersonalDetails.accountID, + amount: transaction.amount, + comment: trimmedComment, + currency: transaction.currency, + merchant: transaction.merchant, + created: transaction.created, + category: transaction.category, + tag: transaction.tag, + billable: !!transaction.billable, + iouRequestType: transaction.iouRequestType, + }); } return; } if (iouType === CONST.IOU.TYPE.TRACK_EXPENSE) { - if (receiptFile) { + if (receiptFile && transaction) { // If the transaction amount is zero, then the money is being requested through the "Scan" flow and the GPS coordinates need to be included. if (transaction.amount === 0) { getCurrentPosition( @@ -391,7 +369,7 @@ function IOURequestStepConfirmation({ return; } - if (receiptFile) { + if (receiptFile && !!transaction) { // If the transaction amount is zero, then the money is being requested through the "Scan" flow and the GPS coordinates need to be included. if (transaction.amount === 0) { getCurrentPosition( @@ -437,7 +415,7 @@ function IOURequestStepConfirmation({ requestMoney, currentUserPersonalDetails.login, currentUserPersonalDetails.accountID, - report.reportID, + report?.reportID, trackExpense, createDistanceRequest, ], @@ -450,13 +428,17 @@ function IOURequestStepConfirmation({ */ const sendMoney = useCallback( (paymentMethodType: ValueOf) => { - const currency = transaction.currency; + const currency = transaction?.currency; - const trimmedComment = transaction.comment?.comment ? transaction.comment.comment.trim() : ''; + const trimmedComment = transaction?.comment?.comment ? transaction.comment.comment.trim() : ''; const participant = participants?.[0]; - if (paymentMethodType === CONST.IOU.PAYMENT_TYPE.ELSEWHERE && participant) { + if (!participant || !report || !transaction?.amount || !currency) { + return; + } + + if (paymentMethodType === CONST.IOU.PAYMENT_TYPE.ELSEWHERE) { IOU.sendMoneyElsewhere(report, transaction.amount, currency, trimmedComment, currentUserPersonalDetails.accountID, participant); return; } @@ -465,11 +447,11 @@ function IOURequestStepConfirmation({ IOU.sendMoneyWithWallet(report, transaction.amount, currency, trimmedComment, currentUserPersonalDetails.accountID, participant); } }, - [transaction.amount, transaction.comment, transaction.currency, participants, currentUserPersonalDetails.accountID, report], + [transaction?.amount, transaction?.comment, transaction?.currency, participants, currentUserPersonalDetails.accountID, report], ); const addNewParticipant = (option: Participant) => { - const newParticipants = transaction.participants?.map((participant) => { + const newParticipants = transaction?.participants?.map((participant) => { if (participant.accountID === option.accountID) { return {...participant, selected: !participant.selected}; } @@ -478,9 +460,6 @@ function IOURequestStepConfirmation({ IOU.setMoneyRequestParticipants_temporaryForRefactor(transactionID, newParticipants); }; - /** - * @param billable - */ const setBillable = (billable: boolean) => { IOU.setMoneyRequestBillable_temporaryForRefactor(transactionID, billable); }; @@ -516,12 +495,12 @@ function IOURequestStepConfirmation({ transaction={transaction} hasMultipleParticipants={iouType === CONST.IOU.TYPE.SPLIT} selectedParticipants={participants} - iouAmount={transaction.amount} - iouComment={transaction.comment.comment ?? ''} - iouCurrencyCode={transaction.currency} - iouIsBillable={transaction.billable} + iouAmount={transaction?.amount} + iouComment={transaction?.comment.comment ?? ''} + iouCurrencyCode={transaction?.currency} + iouIsBillable={transaction?.billable} onToggleBillable={setBillable} - iouCategory={transaction.category} + iouCategory={transaction?.category} onConfirm={createTransaction} onSendMoney={sendMoney} onSelectParticipant={addNewParticipant} @@ -535,20 +514,20 @@ function IOURequestStepConfirmation({ // 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={!transaction.isFromGlobalCreate} - policyID={report.policyID} + canModifyParticipants={!transaction?.isFromGlobalCreate} + policyID={report?.policyID} bankAccountRoute={ReportUtils.getBankAccountRoute(report)} - iouMerchant={transaction.merchant} - iouCreated={transaction.created} + iouMerchant={transaction?.merchant} + iouCreated={transaction?.created} isDistanceRequest={requestType === CONST.IOU.REQUEST_TYPE.DISTANCE} shouldShowSmartScanFields={requestType !== CONST.IOU.REQUEST_TYPE.SCAN} - // reportActionID={undefined} - // hasSmartScanFailed={undefined} - // isEditingSplitBill={undefined} - // isReadOnly={undefined} - // isScanRequest={undefined} - // listStyles={undefined} - // payeePersonalDetails={undefined} + reportActionID={undefined} + hasSmartScanFailed={undefined} + isEditingSplitBill={undefined} + isReadOnly={undefined} + isScanRequest={undefined} + listStyles={undefined} + payeePersonalDetails={undefined} /> @@ -559,23 +538,20 @@ function IOURequestStepConfirmation({ IOURequestStepConfirmation.displayName = 'IOURequestStepConfirmation'; -export default compose( - withCurrentUserPersonalDetails, - withWritableReportOrNotFound, - withFullTransactionOrNotFound, - // eslint-disable-next-line rulesdir/no-multiple-onyx-in-file - withOnyx({ - personalDetails: { - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - }, - 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'}`, - }, - }), -)(IOURequestStepConfirmation); +const ComponentWithWritableReportOrNotFound = withWritableReportOrNotFound(IOURequestStepConfirmation); +const ComponentWithFullTransactionOrNotFound = withFullTransactionOrNotFound(ComponentWithWritableReportOrNotFound); +export default withOnyx({ + personalDetails: { + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + }, + 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'}`, + }, + // @ts-expect-error TODO: Remove this once withFullTransactionOrNotFound (https://github.com/Expensify/App/issues/36123) is migrated to TypeScript. +})(ComponentWithFullTransactionOrNotFound); diff --git a/src/pages/iou/steps/MoneyRequestConfirmPage.js b/src/pages/iou/steps/MoneyRequestConfirmPage.js index 1738ac78df479..fac3d51a1fd6d 100644 --- a/src/pages/iou/steps/MoneyRequestConfirmPage.js +++ b/src/pages/iou/steps/MoneyRequestConfirmPage.js @@ -245,14 +245,14 @@ function MoneyRequestConfirmPage(props) { 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, + IOU.startSplitBill({ + participants: selectedParticipants, + currentUserLogin: props.currentUserPersonalDetails.login, + currentUserAccountID: props.currentUserPersonalDetails.accountID, + comment: trimmedComment, receipt, existingSplitChatReportID, - ); + }); }; FileUtils.readFileAsync(props.iou.receiptPath, props.iou.receiptFilename, onSuccess); return; @@ -261,34 +261,34 @@ function MoneyRequestConfirmPage(props) { // 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, - ); + IOU.splitBill({ + participants: selectedParticipants, + currentUserLogin: props.currentUserPersonalDetails.login, + currentUserAccountID: props.currentUserPersonalDetails.accountID, + amount: props.iou.amount, + comment: trimmedComment, + currency: props.iou.currency, + merchant: props.iou.merchant, + category: props.iou.category, + tag: props.iou.tag, + existingSplitChatReportID: reportID, + }); 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, - ); + IOU.splitBillAndOpenReport({ + participants: selectedParticipants, + currentUserLogin: props.currentUserPersonalDetails.login, + currentUserAccountID: props.currentUserPersonalDetails.accountID, + amount: props.iou.amount, + comment: trimmedComment, + currency: props.iou.currency, + merchant: props.iou.merchant, + category: props.iou.category, + tag: props.iou.tag, + }); return; } diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index 3298cd51c9f13..b6ecde9adb8b2 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -1041,20 +1041,22 @@ describe('actions/IOU', () => { fetch.pause(); IOU.splitBill( // TODO: Migrate after the backend accepts accountIDs - [ - [CARLOS_EMAIL, String(CARLOS_ACCOUNT_ID)], - [JULES_EMAIL, String(JULES_ACCOUNT_ID)], - [VIT_EMAIL, String(VIT_ACCOUNT_ID)], - ].map(([email, accountID]) => ({login: email, accountID: Number(accountID)})), - RORY_EMAIL, - RORY_ACCOUNT_ID, - amount, - comment, - CONST.CURRENCY.USD, - merchant, - '', - '', - '', + { + participants: [ + [CARLOS_EMAIL, String(CARLOS_ACCOUNT_ID)], + [JULES_EMAIL, String(JULES_ACCOUNT_ID)], + [VIT_EMAIL, String(VIT_ACCOUNT_ID)], + ].map(([email, accountID]) => ({login: email, accountID: Number(accountID)})), + currentUserLogin: RORY_EMAIL, + currentUserAccountID: RORY_ACCOUNT_ID, + amount, + comment, + currency: CONST.CURRENCY.USD, + merchant, + created: '', + tag: '', + existingSplitChatReportID: '', + }, ); return waitForBatchedUpdates(); }) From 7658a253922bab285f4075969d5ff2dfd7599a6a Mon Sep 17 00:00:00 2001 From: BrtqKr Date: Sat, 30 Mar 2024 01:41:32 +0100 Subject: [PATCH 10/29] cleanup --- src/libs/actions/IOU.ts | 2 +- .../step/IOURequestStepConfirmation.tsx | 35 +++++++++---------- 2 files changed, 17 insertions(+), 20 deletions(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 780ffcf5f65e5..300f7de1c2bba 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -4052,7 +4052,7 @@ function getSendMoneyParams( comment: string, paymentMethodType: PaymentMethodType, managerID: number, - recipient: Participant, + recipient: Participant | ReportUtils.OptionData, ): SendMoneyParamsData { const recipientEmail = PhoneNumber.addSMSDomainIfPhoneNumber(recipient.login ?? ''); const recipientAccountID = Number(recipient.accountID); diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index c62f6dd64685f..dc7e6a615ae8a 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -1,10 +1,9 @@ -/* eslint-disable rulesdir/no-negated-variables */ -import type {StackScreenProps} from '@react-navigation/stack'; -import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; -import {View} from 'react-native'; -import type {OnyxEntry} from 'react-native-onyx'; -import {withOnyx} from 'react-native-onyx'; -import type {ValueOf} from 'type-fest'; +import type { StackScreenProps } from '@react-navigation/stack'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { View } from 'react-native'; +import type { OnyxEntry } from 'react-native-onyx'; +import { withOnyx } from 'react-native-onyx'; +import type { ValueOf } from 'type-fest'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -15,13 +14,13 @@ import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; -import {openDraftWorkspaceRequest} from '@libs/actions/Policy'; +import { openDraftWorkspaceRequest } from '@libs/actions/Policy'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import getCurrentPosition from '@libs/getCurrentPosition'; import * as IOUUtils from '@libs/IOUUtils'; import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; -import type {MoneyRequestNavigatorParamList} from '@libs/Navigation/types'; +import type { MoneyRequestNavigatorParamList } from '@libs/Navigation/types'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; @@ -30,12 +29,13 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; -import type {PersonalDetailsList, Policy, PolicyCategories, PolicyTagList, Transaction} from '@src/types/onyx'; -import type {Participant} from '@src/types/onyx/IOU'; -import type {Receipt} from '@src/types/onyx/Transaction'; +import type { PersonalDetailsList, Policy, PolicyCategories, PolicyTagList, Transaction } from '@src/types/onyx'; +import type { Participant } from '@src/types/onyx/IOU'; +import type { Receipt } from '@src/types/onyx/Transaction'; import withFullTransactionOrNotFound from './withFullTransactionOrNotFound'; import withWritableReportOrNotFound from './withWritableReportOrNotFound'; -import type {WithWritableReportOrNotFoundProps} from './withWritableReportOrNotFound'; +import type { WithWritableReportOrNotFoundProps } from './withWritableReportOrNotFound'; + type IOURequestStepConfirmationStackProps = StackScreenProps; @@ -171,11 +171,6 @@ function IOURequestStepConfirmation({ IOU.navigateToStartStepIfScanFileCannotBeRead(receiptFilename, receiptPath, onSuccess, requestType, iouType, transactionID, reportID, receiptType); }, [receiptType, receiptPath, receiptFilename, requestType, iouType, transactionID, reportID]); - /** - * @param {Array} selectedParticipants - * @param {String} trimmedComment - * @param {File} [receiptObj] - */ const requestMoney = useCallback( (selectedParticipants: Participant[], trimmedComment: string, receiptObj?: Receipt, gpsPoints?: IOU.GpsPoint) => { if (!receiptObj || !report || !transaction) { @@ -538,7 +533,9 @@ function IOURequestStepConfirmation({ IOURequestStepConfirmation.displayName = 'IOURequestStepConfirmation'; +/* eslint-disable rulesdir/no-negated-variables */ const ComponentWithWritableReportOrNotFound = withWritableReportOrNotFound(IOURequestStepConfirmation); +/* eslint-disable rulesdir/no-negated-variables */ const ComponentWithFullTransactionOrNotFound = withFullTransactionOrNotFound(ComponentWithWritableReportOrNotFound); export default withOnyx({ personalDetails: { @@ -554,4 +551,4 @@ export default withOnyx `${ONYXKEYS.COLLECTION.POLICY_TAGS}${report ? report.policyID : '0'}`, }, // @ts-expect-error TODO: Remove this once withFullTransactionOrNotFound (https://github.com/Expensify/App/issues/36123) is migrated to TypeScript. -})(ComponentWithFullTransactionOrNotFound); +})(ComponentWithFullTransactionOrNotFound); \ No newline at end of file From 0e457d85320f61a39a8d373095ef4662b5d03ded Mon Sep 17 00:00:00 2001 From: BrtqKr Date: Sat, 30 Mar 2024 03:53:58 +0100 Subject: [PATCH 11/29] migrate iouRequestStepCategory, adjust route types --- src/components/CategoryPicker.tsx | 4 +- src/libs/Navigation/types.ts | 4 +- src/libs/OptionsListUtils.ts | 10 +- src/libs/ReportUtils.ts | 12 +- src/libs/SidebarUtils.ts | 12 +- src/libs/actions/IOU.ts | 6 +- .../request/step/IOURequestStepCategory.tsx | 171 ++++++++++++++++++ .../step/IOURequestStepConfirmation.tsx | 27 ++- 8 files changed, 208 insertions(+), 38 deletions(-) create mode 100644 src/pages/iou/request/step/IOURequestStepCategory.tsx diff --git a/src/components/CategoryPicker.tsx b/src/components/CategoryPicker.tsx index 3033bf118e8fa..9fc570e210063 100644 --- a/src/components/CategoryPicker.tsx +++ b/src/components/CategoryPicker.tsx @@ -21,7 +21,7 @@ type CategoryPickerProps = CategoryPickerOnyxProps & { /** It's used by withOnyx HOC */ // eslint-disable-next-line react/no-unused-prop-types policyID: string; - selectedCategory: string; + selectedCategory: string | undefined; onSubmit: (item: ListItem) => void; }; @@ -38,7 +38,7 @@ function CategoryPicker({selectedCategory, policyCategories, policyRecentlyUsedC { name: selectedCategory, enabled: true, - accountID: null, + accountID: undefined, isSelected: true, }, ]; diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 4846251cac4a8..44c427eb32a2c 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -356,8 +356,8 @@ type MoneyRequestNavigatorParamList = { action: ValueOf; iouType: ValueOf; transactionID: string; - reportID: string; - backTo: string; + reportActionID: string; + backTo: Routes; }; [SCREENS.MONEY_REQUEST.STEP_TAX_AMOUNT]: { iouType: string; diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index cd8e93b1bf4f7..870f24044cceb 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -611,7 +611,7 @@ function createOption( ): ReportUtils.OptionData { const result: ReportUtils.OptionData = { text: undefined, - alternateText: null, + alternateText: undefined, pendingAction: undefined, allReportErrors: undefined, brickRoadIndicator: null, @@ -621,12 +621,12 @@ function createOption( subtitle: null, participantsList: undefined, accountID: 0, - login: null, + login: undefined, reportID: '', - phoneNumber: null, + phoneNumber: undefined, hasDraftComment: false, - keyForList: null, - searchText: null, + keyForList: undefined, + searchText: undefined, isDefaultRoom: false, isPinned: false, isWaitingOnBankAccount: false, diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index e7a6af8c6f3a2..6dea32275a087 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -363,7 +363,7 @@ type CustomIcon = { type OptionData = { text?: string; - alternateText?: string | null; + alternateText?: string; allReportErrors?: Errors; brickRoadIndicator?: ValueOf | '' | null; tooltipText?: string | null; @@ -371,16 +371,16 @@ type OptionData = { boldStyle?: boolean; customIcon?: CustomIcon; subtitle?: string | null; - login?: string | null; - accountID?: number | null; + login?: string; + accountID?: number; pronouns?: string; status?: Status | null; - phoneNumber?: string | null; + phoneNumber?: string; isUnread?: boolean | null; isUnreadWithMention?: boolean | null; hasDraftComment?: boolean | null; - keyForList?: string | null; - searchText?: string | null; + keyForList?: string; + searchText?: string; isIOUReportOwner?: boolean | null; isArchivedRoom?: boolean | null; shouldShowSubscript?: boolean | null; diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 63b907a42e25c..43dfc11ab191a 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -211,20 +211,20 @@ function getOptionData({ const result: ReportUtils.OptionData = { text: '', - alternateText: null, + alternateText: undefined, allReportErrors: OptionsListUtils.getAllReportErrors(report, reportActions), brickRoadIndicator: null, tooltipText: null, subtitle: null, - login: null, - accountID: null, + login: undefined, + accountID: undefined, reportID: '', - phoneNumber: null, + phoneNumber: undefined, isUnread: null, isUnreadWithMention: null, hasDraftComment: false, - keyForList: null, - searchText: null, + keyForList: undefined, + searchText: undefined, isPinned: false, hasOutstandingChildRequest: false, isIOUReportOwner: null, diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 300f7de1c2bba..38eceda023528 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -2005,7 +2005,7 @@ function updateMoneyRequestDistance( function updateMoneyRequestCategory( transactionID: string, transactionThreadReportID: string, - category: string, + category: string | undefined, policy: OnyxEntry, policyTagList: OnyxEntry, policyCategories: OnyxEntry, @@ -4052,7 +4052,7 @@ function getSendMoneyParams( comment: string, paymentMethodType: PaymentMethodType, managerID: number, - recipient: Participant | ReportUtils.OptionData, + recipient: Participant, ): SendMoneyParamsData { const recipientEmail = PhoneNumber.addSMSDomainIfPhoneNumber(recipient.login ?? ''); const recipientAccountID = Number(recipient.accountID); @@ -4493,7 +4493,7 @@ function getPayMoneyRequestParams(chatReport: OnyxTypes.Report, iouReport: OnyxT * @param managerID - Account ID of the person sending the money * @param recipient - The user receiving the money */ -function sendMoneyElsewhere(report: OnyxTypes.Report, amount: number, currency: string, comment: string, managerID: number, recipient: Participant | ReportUtils.OptionData) { +function sendMoneyElsewhere(report: OnyxTypes.Report, amount: number, currency: string, comment: string, managerID: number, recipient: Participant) { const {params, optimisticData, successData, failureData} = getSendMoneyParams(report, amount, currency, comment, CONST.IOU.PAYMENT_TYPE.ELSEWHERE, managerID, recipient); API.write(WRITE_COMMANDS.SEND_MONEY_ELSEWHERE, params, {optimisticData, successData, failureData}); diff --git a/src/pages/iou/request/step/IOURequestStepCategory.tsx b/src/pages/iou/request/step/IOURequestStepCategory.tsx new file mode 100644 index 0000000000000..ee14b5bcef8e9 --- /dev/null +++ b/src/pages/iou/request/step/IOURequestStepCategory.tsx @@ -0,0 +1,171 @@ +import type {StackScreenProps} from '@react-navigation/stack'; +import lodashIsEmpty from 'lodash/isEmpty'; +import React from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; +import CategoryPicker from '@components/CategoryPicker'; +import type {ListItem} from '@components/SelectionList/types'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import Navigation from '@libs/Navigation/Navigation'; +import type {MoneyRequestNavigatorParamList} from '@libs/Navigation/types'; +import * as OptionsListUtils from '@libs/OptionsListUtils'; +import * as ReportUtils from '@libs/ReportUtils'; +import * as TransactionUtils from '@libs/TransactionUtils'; +import * as IOU from '@userActions/IOU'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type SCREENS from '@src/SCREENS'; +import type {Policy, PolicyCategories, PolicyTagList, ReportActions, Session, Transaction} from '@src/types/onyx'; +import StepScreenWrapper from './StepScreenWrapper'; +import withFullTransactionOrNotFound from './withFullTransactionOrNotFound'; +import type {WithWritableReportOrNotFoundProps} from './withWritableReportOrNotFound'; +import withWritableReportOrNotFound from './withWritableReportOrNotFound'; + +type IOURequestStepCategoryStackProps = {stackProps: StackScreenProps}; + +type IOURequestStepCategoryOnyxProps = { + /** The draft transaction that holds data to be persisted on the current transaction */ + splitDraftTransaction: OnyxEntry; + /** The policy of the report */ + policy: OnyxEntry; + /** Collection of categories attached to a policy */ + policyCategories: OnyxEntry; + /** Collection of tags attached to a policy */ + policyTags: OnyxEntry; + /** The actions from the parent report */ + reportActions: OnyxEntry; + /** Session info for the currently logged in user. */ + session: OnyxEntry; +}; + +type IOURequestStepCategoryProps = IOURequestStepCategoryStackProps & + IOURequestStepCategoryOnyxProps & + WithWritableReportOrNotFoundProps & { + /** Holds data related to Money Request view state, rather than the underlying Money Request data. */ + transaction: OnyxEntry; + }; + +function IOURequestStepCategory({ + report, + stackProps: { + route: { + params: {transactionID, backTo, action, iouType, reportActionID}, + }, + }, + transaction, + splitDraftTransaction, + policy, + policyTags, + policyCategories, + reportActions, + session, +}: IOURequestStepCategoryProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const isEditing = action === CONST.IOU.ACTION.EDIT; + const isEditingSplitBill = isEditing && iouType === CONST.IOU.TYPE.SPLIT; + const transactionCategory = ReportUtils.getTransactionDetails(isEditingSplitBill && !lodashIsEmpty(splitDraftTransaction) ? splitDraftTransaction : transaction)?.category; + + const reportAction = reportActions?.[report?.parentReportActionID ?? reportActionID] ?? null; + const shouldShowCategory = ReportUtils.isGroupPolicy(report) && (transactionCategory ?? OptionsListUtils.hasEnabledOptions(Object.values(policyCategories ?? {}))); + const isSplitBill = iouType === CONST.IOU.TYPE.SPLIT; + const canEditSplitBill = isSplitBill && reportAction && session?.accountID === reportAction.actorAccountID && TransactionUtils.areRequiredFieldsEmpty(transaction); + // eslint-disable-next-line rulesdir/no-negated-variables + const shouldShowNotFoundPage = !shouldShowCategory || (isEditing && (isSplitBill ? !canEditSplitBill : !ReportUtils.canEditMoneyRequest(reportAction))); + + const navigateBack = () => { + Navigation.goBack(backTo); + }; + + const updateCategory = (category: ListItem) => { + const categorySearchText = category.searchText ?? ''; + const isSelectedCategory = categorySearchText === transactionCategory; + const updatedCategory = isSelectedCategory ? '' : categorySearchText; + + if (transaction) { + // In the split flow, when editing we use SPLIT_TRANSACTION_DRAFT to save draft value + if (isEditingSplitBill) { + IOU.setDraftSplitTransaction(transaction.transactionID, {category: updatedCategory}); + navigateBack(); + return; + } + + if (isEditing && report) { + IOU.updateMoneyRequestCategory(transaction.transactionID, report.reportID, updatedCategory, policy, policyTags, policyCategories); + navigateBack(); + return; + } + } + + IOU.setMoneyRequestCategory(transactionID, updatedCategory); + + navigateBack(); + }; + + return ( + + {translate('iou.categorySelection')} + + + ); +} + +IOURequestStepCategory.displayName = 'IOURequestStepCategory'; + +/* eslint-disable rulesdir/no-negated-variables */ +const ComponentWithWritableReportOrNotFound = withWritableReportOrNotFound(IOURequestStepCategory); +/* eslint-disable rulesdir/no-negated-variables */ +const ComponentWithFullTransactionOrNotFound = withFullTransactionOrNotFound(ComponentWithWritableReportOrNotFound); +export default withOnyx({ + splitDraftTransaction: { + key: ({route}) => { + const transactionID = route?.params.transactionID ?? 0; + return `${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${transactionID}`; + }, + }, + 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'}`, + }, + reportActions: { + key: ({ + report, + route: { + params: {action, iouType}, + }, + }) => { + let reportID = '0'; + if (action === CONST.IOU.ACTION.EDIT && report) { + if (iouType === CONST.IOU.TYPE.SPLIT) { + reportID = report.reportID; + } else if (report.parentReportID) { + reportID = report.parentReportID; + } + } + return `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`; + }, + canEvict: false, + }, + session: { + key: ONYXKEYS.SESSION, + }, + // @ts-expect-error TODO: Remove this once withFullTransactionOrNotFound (https://github.com/Expensify/App/issues/36123) is migrated to TypeScript. +})(ComponentWithFullTransactionOrNotFound); diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index dc7e6a615ae8a..3bde56792580f 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -1,9 +1,9 @@ -import type { StackScreenProps } from '@react-navigation/stack'; -import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { View } from 'react-native'; -import type { OnyxEntry } from 'react-native-onyx'; -import { withOnyx } from 'react-native-onyx'; -import type { ValueOf } from 'type-fest'; +import type {StackScreenProps} from '@react-navigation/stack'; +import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import {View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; +import type {ValueOf} from 'type-fest'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -14,13 +14,13 @@ import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; -import { openDraftWorkspaceRequest } from '@libs/actions/Policy'; +import {openDraftWorkspaceRequest} from '@libs/actions/Policy'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import getCurrentPosition from '@libs/getCurrentPosition'; import * as IOUUtils from '@libs/IOUUtils'; import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; -import type { MoneyRequestNavigatorParamList } from '@libs/Navigation/types'; +import type {MoneyRequestNavigatorParamList} from '@libs/Navigation/types'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; @@ -29,13 +29,12 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; -import type { PersonalDetailsList, Policy, PolicyCategories, PolicyTagList, Transaction } from '@src/types/onyx'; -import type { Participant } from '@src/types/onyx/IOU'; -import type { Receipt } from '@src/types/onyx/Transaction'; +import type {PersonalDetailsList, Policy, PolicyCategories, PolicyTagList, Transaction} from '@src/types/onyx'; +import type {Participant} from '@src/types/onyx/IOU'; +import type {Receipt} from '@src/types/onyx/Transaction'; import withFullTransactionOrNotFound from './withFullTransactionOrNotFound'; import withWritableReportOrNotFound from './withWritableReportOrNotFound'; -import type { WithWritableReportOrNotFoundProps } from './withWritableReportOrNotFound'; - +import type {WithWritableReportOrNotFoundProps} from './withWritableReportOrNotFound'; type IOURequestStepConfirmationStackProps = StackScreenProps; @@ -551,4 +550,4 @@ export default withOnyx `${ONYXKEYS.COLLECTION.POLICY_TAGS}${report ? report.policyID : '0'}`, }, // @ts-expect-error TODO: Remove this once withFullTransactionOrNotFound (https://github.com/Expensify/App/issues/36123) is migrated to TypeScript. -})(ComponentWithFullTransactionOrNotFound); \ No newline at end of file +})(ComponentWithFullTransactionOrNotFound); From 6a947ccbf1cabb5ea1e6fcdffbdc03c15256e556 Mon Sep 17 00:00:00 2001 From: BrtqKr Date: Tue, 2 Apr 2024 10:19:47 +0200 Subject: [PATCH 12/29] cleanup --- .../request/step/IOURequestStepCategory.js | 191 ------ .../request/step/IOURequestStepCategory.tsx | 6 +- .../step/IOURequestStepConfirmation.js | 585 ------------------ .../step/IOURequestStepConfirmation.tsx | 6 +- 4 files changed, 6 insertions(+), 782 deletions(-) delete mode 100644 src/pages/iou/request/step/IOURequestStepCategory.js delete mode 100644 src/pages/iou/request/step/IOURequestStepConfirmation.js diff --git a/src/pages/iou/request/step/IOURequestStepCategory.js b/src/pages/iou/request/step/IOURequestStepCategory.js deleted file mode 100644 index 4f0c77480c04d..0000000000000 --- a/src/pages/iou/request/step/IOURequestStepCategory.js +++ /dev/null @@ -1,191 +0,0 @@ -import lodashGet from 'lodash/get'; -import lodashIsEmpty from 'lodash/isEmpty'; -import PropTypes from 'prop-types'; -import React from 'react'; -import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; -import CategoryPicker from '@components/CategoryPicker'; -import categoryPropTypes from '@components/categoryPropTypes'; -import tagPropTypes from '@components/tagPropTypes'; -import Text from '@components/Text'; -import transactionPropTypes from '@components/transactionPropTypes'; -import useLocalize from '@hooks/useLocalize'; -import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; -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 reportActionPropTypes from '@pages/home/report/reportActionPropTypes'; -import reportPropTypes from '@pages/reportPropTypes'; -import * as IOU from '@userActions/IOU'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import {policyPropTypes} from '@src/pages/workspace/withPolicy'; -import IOURequestStepRoutePropTypes from './IOURequestStepRoutePropTypes'; -import StepScreenWrapper from './StepScreenWrapper'; -import withFullTransactionOrNotFound from './withFullTransactionOrNotFound'; -import withWritableReportOrNotFound from './withWritableReportOrNotFound'; - -const propTypes = { - /** Navigation route context info provided by react navigation */ - route: IOURequestStepRoutePropTypes.isRequired, - - /* Onyx Props */ - /** Holds data related to Money Request view state, rather than the underlying Money Request data. */ - transaction: transactionPropTypes, - - /** The draft transaction that holds data to be persisted on the current transaction */ - splitDraftTransaction: transactionPropTypes, - - /** The report attached to the transaction */ - report: reportPropTypes, - - /** The policy of the report */ - policy: policyPropTypes.policy, - - /** Collection of categories attached to a policy */ - policyCategories: PropTypes.objectOf(categoryPropTypes), - - /** Collection of tags attached to a policy */ - policyTags: tagPropTypes, - - /** The actions from the parent report */ - reportActions: PropTypes.shape(reportActionPropTypes), - - /** Session info for the currently logged in user. */ - session: PropTypes.shape({ - /** Currently logged in user accountID */ - accountID: PropTypes.number, - - /** Currently logged in user email */ - email: PropTypes.string, - }).isRequired, -}; - -const defaultProps = { - report: {}, - transaction: {}, - splitDraftTransaction: {}, - policy: null, - policyTags: null, - policyCategories: null, - reportActions: {}, -}; - -function IOURequestStepCategory({ - report, - route: { - params: {transactionID, backTo, action, iouType, reportActionID}, - }, - transaction, - splitDraftTransaction, - policy, - policyTags, - policyCategories, - session, - reportActions, -}) { - const styles = useThemeStyles(); - const {translate} = useLocalize(); - const isEditing = action === CONST.IOU.ACTION.EDIT; - const isEditingSplitBill = isEditing && iouType === CONST.IOU.TYPE.SPLIT; - const {category: transactionCategory} = ReportUtils.getTransactionDetails(isEditingSplitBill && !lodashIsEmpty(splitDraftTransaction) ? splitDraftTransaction : transaction); - - const reportAction = reportActions[report.parentReportActionID || reportActionID]; - const shouldShowCategory = ReportUtils.isGroupPolicy(report) && (transactionCategory || OptionsListUtils.hasEnabledOptions(_.values(policyCategories))); - const isSplitBill = iouType === CONST.IOU.TYPE.SPLIT; - const canEditSplitBill = isSplitBill && reportAction && session.accountID === reportAction.actorAccountID && TransactionUtils.areRequiredFieldsEmpty(transaction); - // eslint-disable-next-line rulesdir/no-negated-variables - const shouldShowNotFoundPage = !shouldShowCategory || (isEditing && (isSplitBill ? !canEditSplitBill : !ReportUtils.canEditMoneyRequest(reportAction))); - - const navigateBack = () => { - Navigation.goBack(backTo); - }; - - /** - * @param {Object} category - * @param {String} category.searchText - */ - const updateCategory = (category) => { - const isSelectedCategory = category.searchText === transactionCategory; - const updatedCategory = isSelectedCategory ? '' : category.searchText; - - // In the split flow, when editing we use SPLIT_TRANSACTION_DRAFT to save draft value - if (isEditingSplitBill) { - IOU.setDraftSplitTransaction(transaction.transactionID, {category: updatedCategory}); - navigateBack(); - return; - } - - if (isEditing) { - IOU.updateMoneyRequestCategory(transaction.transactionID, report.reportID, updatedCategory, policy, policyTags, policyCategories); - navigateBack(); - return; - } - - IOU.setMoneyRequestCategory(transactionID, updatedCategory); - navigateBack(); - }; - - return ( - - {translate('iou.categorySelection')} - - - ); -} - -IOURequestStepCategory.displayName = 'IOURequestStepCategory'; -IOURequestStepCategory.propTypes = propTypes; -IOURequestStepCategory.defaultProps = defaultProps; - -export default compose( - withWritableReportOrNotFound, - withFullTransactionOrNotFound, - withOnyx({ - splitDraftTransaction: { - key: ({route}) => { - const transactionID = lodashGet(route, 'params.transactionID', 0); - return `${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${transactionID}`; - }, - }, - 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'}`, - }, - reportActions: { - key: ({ - report, - route: { - params: {action, iouType}, - }, - }) => { - let reportID = '0'; - if (action === CONST.IOU.ACTION.EDIT) { - reportID = iouType === CONST.IOU.TYPE.SPLIT ? report.reportID : report.parentReportID; - } - return `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`; - }, - canEvict: false, - }, - session: { - key: ONYXKEYS.SESSION, - }, - }), -)(IOURequestStepCategory); diff --git a/src/pages/iou/request/step/IOURequestStepCategory.tsx b/src/pages/iou/request/step/IOURequestStepCategory.tsx index ee14b5bcef8e9..f386381ea7651 100644 --- a/src/pages/iou/request/step/IOURequestStepCategory.tsx +++ b/src/pages/iou/request/step/IOURequestStepCategory.tsx @@ -126,9 +126,9 @@ function IOURequestStepCategory({ IOURequestStepCategory.displayName = 'IOURequestStepCategory'; /* eslint-disable rulesdir/no-negated-variables */ -const ComponentWithWritableReportOrNotFound = withWritableReportOrNotFound(IOURequestStepCategory); +const IOURequestStepCategoryWithWritableReportOrNotFound = withWritableReportOrNotFound(IOURequestStepCategory); /* eslint-disable rulesdir/no-negated-variables */ -const ComponentWithFullTransactionOrNotFound = withFullTransactionOrNotFound(ComponentWithWritableReportOrNotFound); +const IOURequestStepCategoryWithFullTransactionOrNotFound = withFullTransactionOrNotFound(IOURequestStepCategoryWithWritableReportOrNotFound); export default withOnyx({ splitDraftTransaction: { key: ({route}) => { @@ -168,4 +168,4 @@ export default withOnyx { - if (iouType === CONST.IOU.TYPE.SPLIT) { - return translate('iou.split'); - } - if (iouType === CONST.IOU.TYPE.TRACK_EXPENSE) { - return translate('iou.trackExpense'); - } - if (iouType === CONST.IOU.TYPE.SEND) { - return translate('common.send'); - } - return translate(TransactionUtils.getHeaderTitleTranslationKey(transaction)); - }, [iouType, transaction, translate]); - - const participants = useMemo( - () => - _.map(transaction.participants, (participant) => { - const participantAccountID = lodashGet(participant, 'accountID', 0); - return participantAccountID ? OptionsListUtils.getParticipantsOption(participant, personalDetails) : OptionsListUtils.getReportOption(participant); - }), - [transaction.participants, personalDetails], - ); - const isPolicyExpenseChat = useMemo(() => ReportUtils.isPolicyExpenseChat(ReportUtils.getRootParentReport(report)), [report]); - const formHasBeenSubmitted = useRef(false); - - useEffect(() => { - if (!transaction || !transaction.originalCurrency) { - return; - } - // If user somehow lands on this page without the currency reset, then reset it here. - IOU.setMoneyRequestCurrency_temporaryForRefactor(transactionID, transaction.originalCurrency, true); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - useEffect(() => { - const policyExpenseChat = _.find(participants, (participant) => participant.isPolicyExpenseChat); - if (policyExpenseChat) { - Policy.openDraftWorkspaceRequest(policyExpenseChat.policyID); - } - }, [isOffline, participants, transaction.billable, policy, transactionID]); - - const defaultBillable = lodashGet(policy, 'defaultBillable', false); - useEffect(() => { - IOU.setMoneyRequestBillable_temporaryForRefactor(transactionID, defaultBillable); - }, [transactionID, defaultBillable]); - - useEffect(() => { - if (!transaction.category) { - return; - } - if (policyCategories && policyCategories[transaction.category] && !policyCategories[transaction.category].enabled) { - IOU.setMoneyRequestCategory(transactionID, ''); - } - }, [policyCategories, transaction.category, transactionID]); - const defaultCategory = lodashGet( - _.find(lodashGet(policy, 'customUnits', {}), (customUnit) => customUnit.name === CONST.CUSTOM_UNITS.NAME_DISTANCE), - 'defaultCategory', - '', - ); - useEffect(() => { - if (requestType !== CONST.IOU.REQUEST_TYPE.DISTANCE || !_.isEmpty(transaction.category)) { - return; - } - IOU.setMoneyRequestCategory(transactionID, defaultCategory); - // Prevent resetting to default when unselect category - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [transactionID, requestType, defaultCategory]); - - const navigateBack = useCallback(() => { - // If there is not a report attached to the IOU with a reportID, then the participants were manually selected and the user needs taken - // back to the participants step - if (!transaction.participantsAutoAssigned) { - Navigation.goBack(ROUTES.MONEY_REQUEST_STEP_PARTICIPANTS.getRoute(iouType, transactionID, reportID)); - return; - } - IOUUtils.navigateToStartMoneyRequestStep(requestType, iouType, transactionID, reportID); - }, [transaction, iouType, requestType, transactionID, reportID]); - - const navigateToAddReceipt = useCallback(() => { - Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_SCAN.getRoute(CONST.IOU.ACTION.CREATE, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams())); - }, [iouType, transactionID, reportID]); - - // When the component mounts, if there is a receipt, see if the image can be read from the disk. If not, redirect the user to the starting step of the flow. - // This is because until the request is saved, the receipt file is only stored in the browsers memory as a blob:// and if the browser is refreshed, then - // the image ceases to exist. The best way for the user to recover from this is to start over from the start of the request process. - useEffect(() => { - const onSuccess = (file) => { - const receipt = file; - receipt.state = file && requestType === CONST.IOU.REQUEST_TYPE.MANUAL ? CONST.IOU.RECEIPT_STATE.OPEN : CONST.IOU.RECEIPT_STATE.SCANREADY; - setReceiptFile(receipt); - }; - - IOU.navigateToStartStepIfScanFileCannotBeRead(receiptFilename, receiptPath, onSuccess, requestType, iouType, transactionID, reportID, receiptType); - }, [receiptType, receiptPath, receiptFilename, requestType, iouType, transactionID, reportID]); - - /** - * @param {Array} selectedParticipants - * @param {String} trimmedComment - * @param {File} [receiptObj] - */ - const requestMoney = useCallback( - (selectedParticipants, trimmedComment, receiptObj, gpsPoints) => { - IOU.requestMoney( - report, - transaction.amount, - transaction.currency, - transaction.created, - transaction.merchant, - currentUserPersonalDetails.login, - currentUserPersonalDetails.accountID, - selectedParticipants[0], - trimmedComment, - receiptObj, - transaction.category, - transaction.tag, - transactionTaxCode, - transactionTaxAmount, - transaction.billable, - policy, - policyTags, - policyCategories, - gpsPoints, - ); - }, - [report, transaction, transactionTaxCode, transactionTaxAmount, currentUserPersonalDetails.login, currentUserPersonalDetails.accountID, policy, policyTags, policyCategories], - ); - - /** - * @param {Array} selectedParticipants - * @param {String} trimmedComment - * @param {File} [receiptObj] - */ - const trackExpense = useCallback( - (selectedParticipants, trimmedComment, receiptObj, gpsPoints) => { - IOU.trackExpense( - report, - transaction.amount, - transaction.currency, - transaction.created, - transaction.merchant, - currentUserPersonalDetails.login, - currentUserPersonalDetails.accountID, - selectedParticipants[0], - trimmedComment, - receiptObj, - transaction.category, - transaction.tag, - transactionTaxCode, - transactionTaxAmount, - transaction.billable, - policy, - policyTags, - policyCategories, - gpsPoints, - ); - }, - [ - report, - transaction.amount, - transaction.currency, - transaction.created, - transaction.merchant, - transaction.category, - transaction.tag, - transaction.billable, - currentUserPersonalDetails.login, - currentUserPersonalDetails.accountID, - transactionTaxCode, - transactionTaxAmount, - policy, - policyTags, - policyCategories, - ], - ); - - /** - * @param {Array} selectedParticipants - * @param {String} trimmedComment - */ - const createDistanceRequest = useCallback( - (selectedParticipants, trimmedComment) => { - IOU.createDistanceRequest( - report, - selectedParticipants[0], - trimmedComment, - transaction.created, - transaction.category, - transaction.tag, - transaction.amount, - transaction.currency, - transaction.merchant, - transaction.billable, - TransactionUtils.getValidWaypoints(transaction.comment.waypoints, true), - policy, - policyTags, - policyCategories, - ); - }, - [policy, policyCategories, policyTags, report, transaction], - ); - - const createTransaction = useCallback( - (selectedParticipants) => { - const trimmedComment = lodashGet(transaction, 'comment.comment', '').trim(); - - // Don't let the form be submitted multiple times while the navigator is waiting to take the user to a different page - if (formHasBeenSubmitted.current) { - return; - } - - formHasBeenSubmitted.current = true; - - // 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 && receiptFile) { - IOU.startSplitBill({ - participants: selectedParticipants, - currentUserLogin: currentUserPersonalDetails.login, - currentUserAccountID: currentUserPersonalDetails.accountID, - comment: trimmedComment, - receiptFile, - existingSplitChatReportID: report.reportID, - billable: transaction.billable, - category: transaction.category, - tag: transaction.tag, - }); - 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 && !transaction.isFromGlobalCreate) { - IOU.splitBill({ - participants: selectedParticipants, - currentUserLogin: currentUserPersonalDetails.login, - currentUserAccountID: currentUserPersonalDetails.accountID, - amount: transaction.amount, - comment: trimmedComment, - currency: transaction.currency, - merchant: transaction.merchant, - created: transaction.created, - category: transaction.category, - tag: transaction.tag, - existingSplitChatReportID: report.reportID, - billable: transaction.billable, - iouRequestType: transaction.iouRequestType, - }); - 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({ - participants: selectedParticipants, - currentUserLogin: currentUserPersonalDetails.login, - currentUserAccountID: currentUserPersonalDetails.accountID, - amount: transaction.amount, - comment: trimmedComment, - currency: transaction.currency, - merchant: transaction.merchant, - created: transaction.created, - category: transaction.category, - tag: transaction.tag, - billable: transaction.billable, - iouRequestType: transaction.iouRequestType, - }); - return; - } - - if (iouType === CONST.IOU.TYPE.TRACK_EXPENSE) { - if (receiptFile) { - // If the transaction amount is zero, then the money is being requested through the "Scan" flow and the GPS coordinates need to be included. - if (transaction.amount === 0) { - getCurrentPosition( - (successData) => { - trackExpense(selectedParticipants, trimmedComment, receiptFile, { - lat: successData.coords.latitude, - long: successData.coords.longitude, - }); - }, - (errorData) => { - Log.info('[IOURequestStepConfirmation] getCurrentPosition failed', false, errorData); - // When there is an error, the money can still be requested, it just won't include the GPS coordinates - trackExpense(selectedParticipants, trimmedComment, receiptFile); - }, - { - // It's OK to get a cached location that is up to an hour old because the only accuracy needed is the country the user is in - maximumAge: 1000 * 60 * 60, - - // 15 seconds, don't wait too long because the server can always fall back to using the IP address - timeout: 15000, - }, - ); - return; - } - - // Otherwise, the money is being requested through the "Manual" flow with an attached image and the GPS coordinates are not needed. - trackExpense(selectedParticipants, trimmedComment, receiptFile); - return; - } - trackExpense(selectedParticipants, trimmedComment, receiptFile); - return; - } - - if (receiptFile) { - // If the transaction amount is zero, then the money is being requested through the "Scan" flow and the GPS coordinates need to be included. - if (transaction.amount === 0) { - getCurrentPosition( - (successData) => { - requestMoney(selectedParticipants, trimmedComment, receiptFile, { - lat: successData.coords.latitude, - long: successData.coords.longitude, - }); - }, - (errorData) => { - Log.info('[IOURequestStepConfirmation] getCurrentPosition failed', false, errorData); - // When there is an error, the money can still be requested, it just won't include the GPS coordinates - requestMoney(selectedParticipants, trimmedComment, receiptFile); - }, - { - // It's OK to get a cached location that is up to an hour old because the only accuracy needed is the country the user is in - maximumAge: 1000 * 60 * 60, - - // 15 seconds, don't wait too long because the server can always fall back to using the IP address - timeout: 15000, - }, - ); - return; - } - - // Otherwise, the money is being requested through the "Manual" flow with an attached image and the GPS coordinates are not needed. - requestMoney(selectedParticipants, trimmedComment, receiptFile); - return; - } - - if (requestType === CONST.IOU.REQUEST_TYPE.DISTANCE) { - createDistanceRequest(selectedParticipants, trimmedComment); - return; - } - - requestMoney(selectedParticipants, trimmedComment); - }, - [ - transaction, - iouType, - receiptFile, - requestType, - requestMoney, - currentUserPersonalDetails.login, - currentUserPersonalDetails.accountID, - report.reportID, - trackExpense, - createDistanceRequest, - ], - ); - - /** - * 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 = transaction.currency; - - const trimmedComment = transaction.comment && transaction.comment.comment ? transaction.comment.comment.trim() : ''; - - const participant = participants[0]; - - if (paymentMethodType === CONST.IOU.PAYMENT_TYPE.ELSEWHERE) { - IOU.sendMoneyElsewhere(report, transaction.amount, currency, trimmedComment, currentUserPersonalDetails.accountID, participant); - return; - } - - if (paymentMethodType === CONST.IOU.PAYMENT_TYPE.EXPENSIFY) { - IOU.sendMoneyWithWallet(report, transaction.amount, currency, trimmedComment, currentUserPersonalDetails.accountID, participant); - } - }, - [transaction.amount, transaction.comment, transaction.currency, participants, currentUserPersonalDetails.accountID, report], - ); - - const addNewParticipant = (option) => { - const newParticipants = _.map(transaction.participants, (participant) => { - if (participant.accountID === option.accountID) { - return {...participant, selected: !participant.selected}; - } - return participant; - }); - IOU.setMoneyRequestParticipants_temporaryForRefactor(transactionID, newParticipants); - }; - - /** - * @param {Boolean} billable - */ - const setBillable = (billable) => { - IOU.setMoneyRequestBillable_temporaryForRefactor(transactionID, billable); - }; - - // This loading indicator is shown because the transaction originalCurrency is being updated later than the component mounts. - // To prevent the component from rendering with the wrong currency, we show a loading indicator until the correct currency is set. - const isLoading = !!(transaction && transaction.originalCurrency); - - return ( - - {({safeAreaPaddingBottomStyle}) => ( - - - {isLoading && } - - - - - )} - - ); -} - -IOURequestStepConfirmation.propTypes = propTypes; -IOURequestStepConfirmation.defaultProps = defaultProps; -IOURequestStepConfirmation.displayName = 'IOURequestStepConfirmation'; - -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'}`, - }, - policyCategories: { - key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${report ? report.policyID : '0'}`, - }, - policyTags: { - key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${report ? report.policyID : '0'}`, - }, - }), -)(IOURequestStepConfirmation); diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index 3bde56792580f..90d4bc1c64bef 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -533,9 +533,9 @@ function IOURequestStepConfirmation({ IOURequestStepConfirmation.displayName = 'IOURequestStepConfirmation'; /* eslint-disable rulesdir/no-negated-variables */ -const ComponentWithWritableReportOrNotFound = withWritableReportOrNotFound(IOURequestStepConfirmation); +const IOURequestStepConfirmationWithWritableReportOrNotFound = withWritableReportOrNotFound(IOURequestStepConfirmation); /* eslint-disable rulesdir/no-negated-variables */ -const ComponentWithFullTransactionOrNotFound = withFullTransactionOrNotFound(ComponentWithWritableReportOrNotFound); +const IOURequestStepConfirmationWithFullTransactionOrNotFound = withFullTransactionOrNotFound(IOURequestStepConfirmationWithWritableReportOrNotFound); export default withOnyx({ personalDetails: { key: ONYXKEYS.PERSONAL_DETAILS_LIST, @@ -550,4 +550,4 @@ export default withOnyx `${ONYXKEYS.COLLECTION.POLICY_TAGS}${report ? report.policyID : '0'}`, }, // @ts-expect-error TODO: Remove this once withFullTransactionOrNotFound (https://github.com/Expensify/App/issues/36123) is migrated to TypeScript. -})(ComponentWithFullTransactionOrNotFound); +})(IOURequestStepConfirmationWithFullTransactionOrNotFound); From 6845b0d607f179f623bea30436f7ae15d908ea12 Mon Sep 17 00:00:00 2001 From: BrtqKr Date: Tue, 2 Apr 2024 11:10:05 +0200 Subject: [PATCH 13/29] cleanup --- src/libs/actions/IOU.ts | 10 +++++----- .../iou/request/step/IOURequestStepConfirmation.tsx | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 38eceda023528..e7f7da94799c0 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -134,7 +134,7 @@ type OutstandingChildRequest = { hasOutstandingChildRequest?: boolean; }; -type GpsPoint = { +type GPSPoint = { lat: number; long: number; }; @@ -2075,7 +2075,7 @@ function requestMoney( policy?: OnyxEntry, policyTagList?: OnyxEntry, policyCategories?: OnyxEntry, - gpsPoints?: GpsPoint, + gpsPoints?: GPSPoint, ) { // If the report is iou or expense report, we should get the linked chat report to be passed to the getMoneyRequestInformation function const isMoneyRequestReport = ReportUtils.isMoneyRequestReport(report); @@ -2173,7 +2173,7 @@ function trackExpense( policy?: OnyxEntry, policyTagList?: OnyxEntry, policyCategories?: OnyxEntry, - gpsPoints?: GpsPoint, + gpsPoints?: GPSPoint, ) { const currentCreated = DateUtils.enrichMoneyRequestTimestamp(created); const { @@ -3036,7 +3036,7 @@ function startSplitBill({ splits: JSON.stringify(splits), receipt, comment, - category: category ?? '', + category, tag, isFromGroupDM: !existingSplitChatReport, billable, @@ -5186,7 +5186,7 @@ function savePreferredPaymentMethod(policyID: string, paymentMethod: PaymentMeth Onyx.merge(`${ONYXKEYS.NVP_LAST_PAYMENT_METHOD}`, {[policyID]: paymentMethod}); } -export type {GpsPoint}; +export type {GPSPoint as GpsPoint}; export { setMoneyRequestParticipants, createDistanceRequest, diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index 90d4bc1c64bef..a42a6d1e0be07 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -94,7 +94,7 @@ function IOURequestStepConfirmation({ () => transaction?.participants?.map((participant) => { const participantAccountID = participant.accountID ?? 0; - return participant && participantAccountID ? OptionsListUtils.getParticipantsOption(participant, personalDetails) : OptionsListUtils.getReportOption(participant); + return participantAccountID ? OptionsListUtils.getParticipantsOption(participant, personalDetails) : OptionsListUtils.getReportOption(participant); }), [transaction?.participants, personalDetails], ); From 9c8afd31a25f060e21978372400774122bed27f8 Mon Sep 17 00:00:00 2001 From: BrtqKr Date: Tue, 2 Apr 2024 12:55:51 +0200 Subject: [PATCH 14/29] review fixes --- .../withCurrentUserPersonalDetails.tsx | 2 +- src/libs/Navigation/types.ts | 8 +- .../request/step/IOURequestStepCategory.tsx | 35 +- .../step/IOURequestStepConfirmation.tsx | 23 +- .../step/withWritableReportOrNotFound.tsx | 12 +- .../iou/steps/MoneyRequestConfirmPage.js | 473 ------------------ 6 files changed, 37 insertions(+), 516 deletions(-) delete mode 100644 src/pages/iou/steps/MoneyRequestConfirmPage.js diff --git a/src/components/withCurrentUserPersonalDetails.tsx b/src/components/withCurrentUserPersonalDetails.tsx index 03b9cd1acac4b..8431ededcb565 100644 --- a/src/components/withCurrentUserPersonalDetails.tsx +++ b/src/components/withCurrentUserPersonalDetails.tsx @@ -39,7 +39,7 @@ export default function ; - iouType: string; - transactionID: string; - reportID: string; - }; [SCREENS.MONEY_REQUEST.CURRENCY]: { iouType: string; reportID: string; @@ -435,7 +429,7 @@ type MoneyRequestNavigatorParamList = { reportID: string; }; [SCREENS.MONEY_REQUEST.STEP_CONFIRMATION]: { - action: keyof typeof CONST.IOU.ACTION; + action: ValueOf; iouType: ValueOf; transactionID: string; reportID: string; diff --git a/src/pages/iou/request/step/IOURequestStepCategory.tsx b/src/pages/iou/request/step/IOURequestStepCategory.tsx index f386381ea7651..529be126ce115 100644 --- a/src/pages/iou/request/step/IOURequestStepCategory.tsx +++ b/src/pages/iou/request/step/IOURequestStepCategory.tsx @@ -1,4 +1,3 @@ -import type {StackScreenProps} from '@react-navigation/stack'; import lodashIsEmpty from 'lodash/isEmpty'; import React from 'react'; import type {OnyxEntry} from 'react-native-onyx'; @@ -9,7 +8,6 @@ import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; -import type {MoneyRequestNavigatorParamList} from '@libs/Navigation/types'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; @@ -23,36 +21,36 @@ import withFullTransactionOrNotFound from './withFullTransactionOrNotFound'; import type {WithWritableReportOrNotFoundProps} from './withWritableReportOrNotFound'; import withWritableReportOrNotFound from './withWritableReportOrNotFound'; -type IOURequestStepCategoryStackProps = {stackProps: StackScreenProps}; - type IOURequestStepCategoryOnyxProps = { /** The draft transaction that holds data to be persisted on the current transaction */ splitDraftTransaction: OnyxEntry; + /** The policy of the report */ policy: OnyxEntry; + /** Collection of categories attached to a policy */ policyCategories: OnyxEntry; + /** Collection of tags attached to a policy */ policyTags: OnyxEntry; + /** The actions from the parent report */ reportActions: OnyxEntry; + /** Session info for the currently logged in user. */ session: OnyxEntry; }; -type IOURequestStepCategoryProps = IOURequestStepCategoryStackProps & - IOURequestStepCategoryOnyxProps & - WithWritableReportOrNotFoundProps & { +type IOURequestStepCategoryProps = IOURequestStepCategoryOnyxProps & + WithWritableReportOrNotFoundProps & { /** Holds data related to Money Request view state, rather than the underlying Money Request data. */ transaction: OnyxEntry; }; function IOURequestStepCategory({ report, - stackProps: { - route: { - params: {transactionID, backTo, action, iouType, reportActionID}, - }, + route: { + params: {transactionID, backTo, action, iouType, reportActionID}, }, transaction, splitDraftTransaction, @@ -125,11 +123,7 @@ function IOURequestStepCategory({ IOURequestStepCategory.displayName = 'IOURequestStepCategory'; -/* eslint-disable rulesdir/no-negated-variables */ -const IOURequestStepCategoryWithWritableReportOrNotFound = withWritableReportOrNotFound(IOURequestStepCategory); -/* eslint-disable rulesdir/no-negated-variables */ -const IOURequestStepCategoryWithFullTransactionOrNotFound = withFullTransactionOrNotFound(IOURequestStepCategoryWithWritableReportOrNotFound); -export default withOnyx({ +const IOURequestStepCategoryWithOnyx = withOnyx({ splitDraftTransaction: { key: ({route}) => { const transactionID = route?.params.transactionID ?? 0; @@ -167,5 +161,10 @@ export default withOnyx; - type IOURequestStepConfirmationOnyxProps = { policy: OnyxEntry; policyCategories: OnyxEntry; @@ -47,8 +42,7 @@ type IOURequestStepConfirmationOnyxProps = { }; type IOURequestStepConfirmationProps = IOURequestStepConfirmationOnyxProps & - IOURequestStepConfirmationStackProps & - WithWritableReportOrNotFoundProps & { + WithWritableReportOrNotFoundProps & { transaction: OnyxEntry; }; @@ -533,11 +527,7 @@ function IOURequestStepConfirmation({ IOURequestStepConfirmation.displayName = 'IOURequestStepConfirmation'; -/* eslint-disable rulesdir/no-negated-variables */ -const IOURequestStepConfirmationWithWritableReportOrNotFound = withWritableReportOrNotFound(IOURequestStepConfirmation); -/* eslint-disable rulesdir/no-negated-variables */ -const IOURequestStepConfirmationWithFullTransactionOrNotFound = withFullTransactionOrNotFound(IOURequestStepConfirmationWithWritableReportOrNotFound); -export default withOnyx({ +const IOURequestStepConfirmationWithOnyx = withOnyx({ personalDetails: { key: ONYXKEYS.PERSONAL_DETAILS_LIST, }, @@ -550,5 +540,10 @@ export default withOnyx `${ONYXKEYS.COLLECTION.POLICY_TAGS}${report ? report.policyID : '0'}`, }, - // @ts-expect-error TODO: Remove this once withFullTransactionOrNotFound (https://github.com/Expensify/App/issues/36123) is migrated to TypeScript. -})(IOURequestStepConfirmationWithFullTransactionOrNotFound); +})(IOURequestStepConfirmation); +/* eslint-disable rulesdir/no-negated-variables */ +const IOURequestStepConfirmationWithFullTransactionOrNotFound = withFullTransactionOrNotFound(IOURequestStepConfirmationWithOnyx); +/* eslint-disable rulesdir/no-negated-variables */ +// @ts-expect-error TODO: Remove this once withFullTransactionOrNotFound (https://github.com/Expensify/App/issues/36123) is migrated to TypeScript. +const IOURequestStepConfirmationWithWritableReportOrNotFound = withWritableReportOrNotFound(IOURequestStepConfirmationWithFullTransactionOrNotFound); +export default IOURequestStepConfirmationWithWritableReportOrNotFound; diff --git a/src/pages/iou/request/step/withWritableReportOrNotFound.tsx b/src/pages/iou/request/step/withWritableReportOrNotFound.tsx index d5d27d8268b11..d2ece8c9cbf50 100644 --- a/src/pages/iou/request/step/withWritableReportOrNotFound.tsx +++ b/src/pages/iou/request/step/withWritableReportOrNotFound.tsx @@ -17,11 +17,17 @@ type WithWritableReportOrNotFoundOnyxProps = { report: OnyxEntry; }; -type Route = RouteProp; +type MoneyRequestRouteName = + | typeof SCREENS.MONEY_REQUEST.STEP_WAYPOINT + | typeof SCREENS.MONEY_REQUEST.STEP_DESCRIPTION + | typeof SCREENS.MONEY_REQUEST.STEP_CATEGORY + | typeof SCREENS.MONEY_REQUEST.STEP_CONFIRMATION; -type WithWritableReportOrNotFoundProps = WithWritableReportOrNotFoundOnyxProps & {route: Route}; +type Route = RouteProp; -export default function ( +type WithWritableReportOrNotFoundProps = WithWritableReportOrNotFoundOnyxProps & {route: Route}; + +export default function , TRef>( WrappedComponent: ComponentType>, ): React.ComponentType, keyof WithWritableReportOrNotFoundOnyxProps>> { // eslint-disable-next-line rulesdir/no-negated-variables diff --git a/src/pages/iou/steps/MoneyRequestConfirmPage.js b/src/pages/iou/steps/MoneyRequestConfirmPage.js deleted file mode 100644 index fac3d51a1fd6d..0000000000000 --- a/src/pages/iou/steps/MoneyRequestConfirmPage.js +++ /dev/null @@ -1,473 +0,0 @@ -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({ - participants: selectedParticipants, - currentUserLogin: props.currentUserPersonalDetails.login, - currentUserAccountID: props.currentUserPersonalDetails.accountID, - comment: 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({ - participants: selectedParticipants, - currentUserLogin: props.currentUserPersonalDetails.login, - currentUserAccountID: props.currentUserPersonalDetails.accountID, - amount: props.iou.amount, - comment: trimmedComment, - currency: props.iou.currency, - merchant: props.iou.merchant, - category: props.iou.category, - tag: props.iou.tag, - existingSplitChatReportID: reportID, - }); - 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({ - participants: selectedParticipants, - currentUserLogin: props.currentUserPersonalDetails.login, - currentUserAccountID: props.currentUserPersonalDetails.accountID, - amount: props.iou.amount, - comment: trimmedComment, - currency: props.iou.currency, - merchant: props.iou.merchant, - category: props.iou.category, - tag: props.iou.tag, - }); - 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); From 236eec33f6e6c3f822a8906f5f54d5e716d660de Mon Sep 17 00:00:00 2001 From: BrtqKr Date: Tue, 2 Apr 2024 13:17:23 +0200 Subject: [PATCH 15/29] lint fix --- src/pages/iou/request/step/withWritableReportOrNotFound.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pages/iou/request/step/withWritableReportOrNotFound.tsx b/src/pages/iou/request/step/withWritableReportOrNotFound.tsx index d2ece8c9cbf50..f72810258adbf 100644 --- a/src/pages/iou/request/step/withWritableReportOrNotFound.tsx +++ b/src/pages/iou/request/step/withWritableReportOrNotFound.tsx @@ -53,7 +53,10 @@ export default function , WithWritableReportOrNotFoundOnyxProps>({ report: { - key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${route.params?.reportID ?? '0'}`, + key: ({route}) => { + const reportId = 'reportID' in route.params ? route.params.reportID : '0'; + return `${ONYXKEYS.COLLECTION.REPORT}${reportId}`; + }, }, })(forwardRef(WithWritableReportOrNotFound)); } From 058dafa95ed38e9f06c5070fa2a9d139cfc7ec67 Mon Sep 17 00:00:00 2001 From: BrtqKr Date: Tue, 2 Apr 2024 16:33:17 +0200 Subject: [PATCH 16/29] fix stepscreenwrapper --- src/pages/iou/request/step/StepScreenWrapper.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/pages/iou/request/step/StepScreenWrapper.tsx b/src/pages/iou/request/step/StepScreenWrapper.tsx index 902122fa0dd4c..3f4d5017d71e9 100644 --- a/src/pages/iou/request/step/StepScreenWrapper.tsx +++ b/src/pages/iou/request/step/StepScreenWrapper.tsx @@ -1,3 +1,4 @@ +import React, {forwardRef} from 'react'; import type {PropsWithChildren} from 'react'; import {View} from 'react-native'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; @@ -27,7 +28,7 @@ type StepScreenWrapperProps = { testID: string; /** Whether or not to include safe area padding */ - includeSafeAreaPaddingBottom: boolean; + includeSafeAreaPaddingBottom?: boolean; }; function StepScreenWrapper({ @@ -62,7 +63,7 @@ function StepScreenWrapper({ /> { // If props.children is a function, call it to provide the insets to the children - callOrReturn(children, {insets, safeAreaPaddingBottomStyle, didScreenTransitionEnd}) + callOrReturn(children, [insets, safeAreaPaddingBottomStyle, didScreenTransitionEnd]) } @@ -71,6 +72,4 @@ function StepScreenWrapper({ ); } -StepScreenWrapper.displayName = 'StepScreenWrapper'; - -export default StepScreenWrapper; +export default forwardRef(StepScreenWrapper); From 091da01454c155e82955be982a05b5054dab5774 Mon Sep 17 00:00:00 2001 From: BrtqKr Date: Tue, 2 Apr 2024 17:03:13 +0200 Subject: [PATCH 17/29] fix step category --- src/pages/iou/request/step/IOURequestStepCategory.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pages/iou/request/step/IOURequestStepCategory.tsx b/src/pages/iou/request/step/IOURequestStepCategory.tsx index 529be126ce115..272db69e7d17f 100644 --- a/src/pages/iou/request/step/IOURequestStepCategory.tsx +++ b/src/pages/iou/request/step/IOURequestStepCategory.tsx @@ -67,7 +67,9 @@ function IOURequestStepCategory({ const transactionCategory = ReportUtils.getTransactionDetails(isEditingSplitBill && !lodashIsEmpty(splitDraftTransaction) ? splitDraftTransaction : transaction)?.category; const reportAction = reportActions?.[report?.parentReportActionID ?? reportActionID] ?? null; - const shouldShowCategory = ReportUtils.isGroupPolicy(report) && (transactionCategory ?? OptionsListUtils.hasEnabledOptions(Object.values(policyCategories ?? {}))); + + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const shouldShowCategory = ReportUtils.isGroupPolicy(report) && (transactionCategory || OptionsListUtils.hasEnabledOptions(Object.values(policyCategories ?? {}))); const isSplitBill = iouType === CONST.IOU.TYPE.SPLIT; const canEditSplitBill = isSplitBill && reportAction && session?.accountID === reportAction.actorAccountID && TransactionUtils.areRequiredFieldsEmpty(transaction); // eslint-disable-next-line rulesdir/no-negated-variables From 5550a8171b2da7a238b00f9ba25cb37dabca6a93 Mon Sep 17 00:00:00 2001 From: BrtqKr Date: Wed, 3 Apr 2024 13:14:39 +0200 Subject: [PATCH 18/29] fix request money flow --- src/libs/API/parameters/RequestMoneyParams.ts | 2 +- src/libs/TransactionUtils.ts | 20 ++++++++++--------- src/libs/actions/IOU.ts | 2 +- .../step/IOURequestStepConfirmation.tsx | 3 ++- 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/libs/API/parameters/RequestMoneyParams.ts b/src/libs/API/parameters/RequestMoneyParams.ts index b55f9fd7a2a9a..ce8fb99c3f25e 100644 --- a/src/libs/API/parameters/RequestMoneyParams.ts +++ b/src/libs/API/parameters/RequestMoneyParams.ts @@ -17,7 +17,7 @@ type RequestMoneyParams = { createdChatReportActionID: string; createdIOUReportActionID: string; reportPreviewReportActionID: string; - receipt: Receipt; + receipt?: Receipt; receiptState?: ValueOf; category?: string; tag?: string; diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index 9b4617b3754f5..6f07fb9fd6181 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -45,7 +45,7 @@ function isDistanceRequest(transaction: OnyxEntry): boolean { return type === CONST.TRANSACTION.TYPE.CUSTOM_UNIT && customUnitName === CONST.CUSTOM_UNITS.NAME_DISTANCE; } -function isScanRequest(transaction: Transaction): boolean { +function isScanRequest(transaction: OnyxEntry): boolean { // This is used during the request creation flow before the transaction has been saved to the server if (lodashHas(transaction, 'iouRequestType')) { return transaction?.iouRequestType === CONST.IOU.REQUEST_TYPE.SCAN; @@ -55,13 +55,11 @@ function isScanRequest(transaction: Transaction): boolean { } function getRequestType(transaction: OnyxEntry): ValueOf { - if (transaction) { - if (isDistanceRequest(transaction)) { - return CONST.IOU.REQUEST_TYPE.DISTANCE; - } - if (isScanRequest(transaction)) { - return CONST.IOU.REQUEST_TYPE.SCAN; - } + if (isDistanceRequest(transaction)) { + return CONST.IOU.REQUEST_TYPE.DISTANCE; + } + if (isScanRequest(transaction)) { + return CONST.IOU.REQUEST_TYPE.SCAN; } return CONST.IOU.REQUEST_TYPE.MANUAL; @@ -508,7 +506,11 @@ function getWaypointIndex(key: string): number { /** * Filters the waypoints which are valid and returns those */ -function getValidWaypoints(waypoints: WaypointCollection | undefined = {}, reArrangeIndexes = false): WaypointCollection { +function getValidWaypoints(waypoints: WaypointCollection | undefined, reArrangeIndexes = false): WaypointCollection { + if (!waypoints) { + return {}; + } + const sortedIndexes = Object.keys(waypoints) .map(getWaypointIndex) .sort((a, b) => a - b); diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index e7b380598da49..faab87ed628ec 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -2078,7 +2078,7 @@ function requestMoney( payeeAccountID: number, participant: Participant, comment: string, - receipt: Receipt, + receipt: Receipt | undefined, category?: string, tag?: string, taxCode = '', diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index d60bae6440249..ba92fdf0ae006 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -167,9 +167,10 @@ function IOURequestStepConfirmation({ const requestMoney = useCallback( (selectedParticipants: Participant[], trimmedComment: string, receiptObj?: Receipt, gpsPoints?: IOU.GpsPoint) => { - if (!receiptObj || !report || !transaction) { + if (!report || !transaction) { return; } + IOU.requestMoney( report, transaction.amount, From 7d23a770a2273e8431c854eefe631063a2a4155f Mon Sep 17 00:00:00 2001 From: BrtqKr Date: Wed, 3 Apr 2024 18:02:40 +0200 Subject: [PATCH 19/29] fix forwardref --- src/pages/iou/request/step/StepScreenWrapper.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/iou/request/step/StepScreenWrapper.tsx b/src/pages/iou/request/step/StepScreenWrapper.tsx index 3f4d5017d71e9..c7ed6862ee113 100644 --- a/src/pages/iou/request/step/StepScreenWrapper.tsx +++ b/src/pages/iou/request/step/StepScreenWrapper.tsx @@ -63,7 +63,7 @@ function StepScreenWrapper({ /> { // If props.children is a function, call it to provide the insets to the children - callOrReturn(children, [insets, safeAreaPaddingBottomStyle, didScreenTransitionEnd]) + callOrReturn(children, {insets, safeAreaPaddingBottomStyle, didScreenTransitionEnd}) } From 431470bf30ff67f4ea1ee66a513e485435e84f5e Mon Sep 17 00:00:00 2001 From: BrtqKr Date: Thu, 4 Apr 2024 16:00:24 +0200 Subject: [PATCH 20/29] review fixes --- src/pages/iou/request/step/IOURequestStepCategory.tsx | 3 ++- src/pages/iou/request/step/IOURequestStepConfirmation.tsx | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/pages/iou/request/step/IOURequestStepCategory.tsx b/src/pages/iou/request/step/IOURequestStepCategory.tsx index 272db69e7d17f..3a343244a0be7 100644 --- a/src/pages/iou/request/step/IOURequestStepCategory.tsx +++ b/src/pages/iou/request/step/IOURequestStepCategory.tsx @@ -66,7 +66,8 @@ function IOURequestStepCategory({ const isEditingSplitBill = isEditing && iouType === CONST.IOU.TYPE.SPLIT; const transactionCategory = ReportUtils.getTransactionDetails(isEditingSplitBill && !lodashIsEmpty(splitDraftTransaction) ? splitDraftTransaction : transaction)?.category; - const reportAction = reportActions?.[report?.parentReportActionID ?? reportActionID] ?? null; + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const reportAction = reportActions?.[report?.parentReportActionID || reportActionID] ?? null; // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const shouldShowCategory = ReportUtils.isGroupPolicy(report) && (transactionCategory || OptionsListUtils.hasEnabledOptions(Object.values(policyCategories ?? {}))); diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index b8f77304ffbc5..d00a8a532d73f 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -36,13 +36,19 @@ import withWritableReportOrNotFound from './withWritableReportOrNotFound'; import type {WithWritableReportOrNotFoundProps} from './withWritableReportOrNotFound'; type IOURequestStepConfirmationOnyxProps = { + /** The policy of the report */ policy: OnyxEntry; + + /** The category configuration of the report's policy */ policyCategories: OnyxEntry; + + /** The tag configuration of the report's policy */ policyTags: OnyxEntry; }; type IOURequestStepConfirmationProps = IOURequestStepConfirmationOnyxProps & WithWritableReportOrNotFoundProps & { + /** The transaction object being modified in Onyx */ transaction: OnyxEntry; }; From 5c54efe88e320b8ce5cbfa914eec282f1197007e Mon Sep 17 00:00:00 2001 From: BrtqKr Date: Thu, 4 Apr 2024 16:19:15 +0200 Subject: [PATCH 21/29] fix null/undef --- src/components/TagPicker/index.tsx | 4 ++-- src/libs/OptionsListUtils.ts | 2 +- src/libs/ReportUtils.ts | 2 +- src/libs/SidebarUtils.ts | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/TagPicker/index.tsx b/src/components/TagPicker/index.tsx index 54ad016173b75..fc9c0ca6ad1ad 100644 --- a/src/components/TagPicker/index.tsx +++ b/src/components/TagPicker/index.tsx @@ -15,7 +15,7 @@ import type {PolicyTag, PolicyTagList, PolicyTags, RecentlyUsedTags} from '@src/ type SelectedTagOption = { name: string; enabled: boolean; - accountID: number | null; + accountID: number | undefined; }; type TagPickerOnyxProps = { @@ -76,7 +76,7 @@ function TagPicker({selectedTag, tagListName, policyTags, tagListIndex, policyRe { name: selectedTag, enabled: true, - accountID: null, + accountID: undefined, }, ]; }, [selectedTag]); diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 71b0b81e3aeaa..7c9c264f42434 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -648,7 +648,7 @@ function createOption( icons: undefined, tooltipText: null, ownerAccountID: undefined, - subtitle: null, + subtitle: undefined, participantsList: undefined, accountID: 0, login: undefined, diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 20bd636b409b0..070c1906ea89c 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -383,7 +383,7 @@ type OptionData = { alternateTextMaxLines?: number; boldStyle?: boolean; customIcon?: CustomIcon; - subtitle?: string | null; + subtitle?: string; login?: string; accountID?: number; pronouns?: string; diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 6abe75ecaf70b..10c019298edab 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -207,7 +207,7 @@ function getOptionData({ allReportErrors: OptionsListUtils.getAllReportErrors(report, reportActions), brickRoadIndicator: null, tooltipText: null, - subtitle: null, + subtitle: undefined, login: undefined, accountID: undefined, reportID: '', From cfc2dfd8f7cba8e1b40788c5e919f522ae62a8f1 Mon Sep 17 00:00:00 2001 From: BrtqKr Date: Thu, 4 Apr 2024 16:25:06 +0200 Subject: [PATCH 22/29] fix null/undef --- src/pages/RoomInvitePage.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/RoomInvitePage.tsx b/src/pages/RoomInvitePage.tsx index 49e53381e0407..9792a35e9154f 100644 --- a/src/pages/RoomInvitePage.tsx +++ b/src/pages/RoomInvitePage.tsx @@ -70,7 +70,7 @@ function RoomInvitePage({betas, report, policies}: RoomInvitePageProps) { }); const newSelectedOptions: ReportUtils.OptionData[] = []; selectedOptions.forEach((option) => { - newSelectedOptions.push(option.login && option.login in detailsMap ? {...detailsMap[option.login], isSelected: true} : option); + newSelectedOptions.push(option.login && option.login in detailsMap ? {...detailsMap[option.login], accountID: detailsMap[option.login] ?? undefined, isSelected: true} : option); }); setUserToInvite(inviteOptions.userToInvite); @@ -134,7 +134,7 @@ function RoomInvitePage({betas, report, policies}: RoomInvitePageProps) { if (isOptionInList) { newSelectedOptions = selectedOptions.filter((selectedOption) => selectedOption.login !== option.login); } else { - newSelectedOptions = [...selectedOptions, {...option, isSelected: true}]; + newSelectedOptions = [...selectedOptions, {...option, accountID: option.accountID ?? undefined, isSelected: true}]; } setSelectedOptions(newSelectedOptions); From 77ec664cc061ca52c96d547e8c219acf0fd76e0d Mon Sep 17 00:00:00 2001 From: BrtqKr Date: Thu, 4 Apr 2024 16:29:29 +0200 Subject: [PATCH 23/29] fix null/undef --- src/pages/RoomInvitePage.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pages/RoomInvitePage.tsx b/src/pages/RoomInvitePage.tsx index 9792a35e9154f..28d6f5db27807 100644 --- a/src/pages/RoomInvitePage.tsx +++ b/src/pages/RoomInvitePage.tsx @@ -70,7 +70,9 @@ function RoomInvitePage({betas, report, policies}: RoomInvitePageProps) { }); const newSelectedOptions: ReportUtils.OptionData[] = []; selectedOptions.forEach((option) => { - newSelectedOptions.push(option.login && option.login in detailsMap ? {...detailsMap[option.login], accountID: detailsMap[option.login] ?? undefined, isSelected: true} : option); + newSelectedOptions.push( + option.login && option.login in detailsMap ? {...detailsMap[option.login], accountID: detailsMap[option.login].accountID ?? undefined, isSelected: true} : option, + ); }); setUserToInvite(inviteOptions.userToInvite); From c25187020df1d23f3b7ba42d4fd6021f324304f7 Mon Sep 17 00:00:00 2001 From: BrtqKr Date: Fri, 5 Apr 2024 00:15:55 +0200 Subject: [PATCH 24/29] review fixes --- src/ROUTES.ts | 3 ++- src/components/CategoryPicker.tsx | 2 +- src/libs/API/parameters/TrackExpenseParams.ts | 2 +- src/libs/IOUUtils.ts | 3 ++- src/libs/OptionsListUtils.ts | 2 +- src/libs/TransactionUtils.ts | 3 ++- src/libs/actions/IOU.ts | 12 ++++++------ src/pages/RoomInvitePage.tsx | 6 ++---- .../iou/request/step/IOURequestStepCategory.tsx | 4 +++- .../iou/request/step/IOURequestStepConfirmation.tsx | 10 ++-------- src/types/onyx/Transaction.ts | 3 ++- 11 files changed, 24 insertions(+), 26 deletions(-) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 8130c271a2db2..067e8ec8b43a7 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -1,5 +1,6 @@ import type {IsEqual, ValueOf} from 'type-fest'; import type CONST from './CONST'; +import type {IOURequestType} from './libs/actions/IOU'; // This is a file containing constants for all the routes we want to be able to go to @@ -393,7 +394,7 @@ const ROUTES = { // straight to those flows without needing to have optimistic transaction and report IDs. MONEY_REQUEST_START: { route: 'start/:iouType/:iouRequestType', - getRoute: (iouType: ValueOf, iouRequestType: ValueOf) => `start/${iouType}/${iouRequestType}` as const, + getRoute: (iouType: ValueOf, iouRequestType: IOURequestType) => `start/${iouType}/${iouRequestType}` as const, }, MONEY_REQUEST_CREATE_TAB_DISTANCE: { route: ':action/:iouType/start/:transactionID/:reportID/distance', diff --git a/src/components/CategoryPicker.tsx b/src/components/CategoryPicker.tsx index 799b89d1e84fc..f26d7c25c7e21 100644 --- a/src/components/CategoryPicker.tsx +++ b/src/components/CategoryPicker.tsx @@ -21,7 +21,7 @@ type CategoryPickerProps = CategoryPickerOnyxProps & { /** It's used by withOnyx HOC */ // eslint-disable-next-line react/no-unused-prop-types policyID: string; - selectedCategory: string | undefined; + selectedCategory?: string; onSubmit: (item: ListItem) => void; }; diff --git a/src/libs/API/parameters/TrackExpenseParams.ts b/src/libs/API/parameters/TrackExpenseParams.ts index f48c8666f109f..9c8d9761d8889 100644 --- a/src/libs/API/parameters/TrackExpenseParams.ts +++ b/src/libs/API/parameters/TrackExpenseParams.ts @@ -15,7 +15,7 @@ type TrackExpenseParams = { createdChatReportActionID: string; createdIOUReportActionID?: string; reportPreviewReportActionID?: string; - receipt: Receipt; + receipt?: Receipt; receiptState?: ValueOf; category?: string; tag?: string; diff --git a/src/libs/IOUUtils.ts b/src/libs/IOUUtils.ts index 65390982f18cc..4158727502434 100644 --- a/src/libs/IOUUtils.ts +++ b/src/libs/IOUUtils.ts @@ -3,11 +3,12 @@ import type {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import type {Report, Transaction} from '@src/types/onyx'; +import type {IOURequestType} from './actions/IOU'; import * as CurrencyUtils from './CurrencyUtils'; import Navigation from './Navigation/Navigation'; import * as TransactionUtils from './TransactionUtils'; -function navigateToStartMoneyRequestStep(requestType: ValueOf, iouType: ValueOf, transactionID: string, reportID: string) { +function navigateToStartMoneyRequestStep(requestType: IOURequestType, iouType: ValueOf, transactionID: string, reportID: string) { // If the participants were automatically added to the transaction, then the user needs taken back to the starting step switch (requestType) { case CONST.IOU.REQUEST_TYPE.DISTANCE: diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 7c9c264f42434..1879896e93dde 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -166,7 +166,7 @@ type MemberForList = { keyForList: string; isSelected: boolean; isDisabled: boolean; - accountID?: number | null; + accountID?: number; login: string; icons?: OnyxCommon.Icon[]; pendingAction?: OnyxCommon.PendingAction; diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index 8b37288c711a8..f6f519c784c45 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -8,6 +8,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type {RecentWaypoint, Report, TaxRate, TaxRates, TaxRatesWithDefault, Transaction, TransactionViolation} from '@src/types/onyx'; import type {Comment, Receipt, TransactionChanges, TransactionPendingFieldsKey, Waypoint, WaypointCollection} from '@src/types/onyx/Transaction'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import type {IOURequestType} from './actions/IOU'; import {isCorporateCard, isExpensifyCard} from './CardUtils'; import DateUtils from './DateUtils'; import * as Localize from './Localize'; @@ -55,7 +56,7 @@ function isScanRequest(transaction: OnyxEntry): boolean { return Boolean(transaction?.receipt?.source); } -function getRequestType(transaction: OnyxEntry): ValueOf { +function getRequestType(transaction: OnyxEntry): IOURequestType { if (isDistanceRequest(transaction)) { return CONST.IOU.REQUEST_TYPE.DISTANCE; } diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 17fafa5961de2..a914439bc30d9 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -343,7 +343,7 @@ function updateMoneyRequestTypeParams(routes: StackNavigationState, reportID: string, requestType?: ValueOf) { +function startMoneyRequest(iouType: ValueOf, reportID: string, requestType?: IOURequestType) { clearMoneyRequest(CONST.IOU.OPTIMISTIC_TRANSACTION_ID); switch (requestType) { case CONST.IOU.REQUEST_TYPE.MANUAL: @@ -415,7 +415,7 @@ function setMoneyRequestBillable_temporaryForRefactor(transactionID: string, bil } // eslint-disable-next-line @typescript-eslint/naming-convention -function setMoneyRequestParticipants_temporaryForRefactor(transactionID: string, participants: Participant[] | undefined = []) { +function setMoneyRequestParticipants_temporaryForRefactor(transactionID: string, participants: Participant[] = []) { Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {participants}); } @@ -2068,7 +2068,7 @@ function updateMoneyRequestDistance( function updateMoneyRequestCategory( transactionID: string, transactionThreadReportID: string, - category: string | undefined, + category: string, policy: OnyxEntry, policyTagList: OnyxEntry, policyCategories: OnyxEntry, @@ -2227,7 +2227,7 @@ function trackExpense( payeeAccountID: number, participant: Participant, comment: string, - receipt: Receipt, + receipt?: Receipt, category?: string, tag?: string, taxCode = '', @@ -5292,7 +5292,7 @@ function navigateToStartStepIfScanFileCannotBeRead( receiptFilename: string | undefined, receiptPath: ReceiptSource | undefined, onSuccess: (file: File) => void, - requestType: ValueOf, + requestType: IOURequestType, iouType: ValueOf, transactionID: string, reportID: string, @@ -5318,7 +5318,7 @@ function savePreferredPaymentMethod(policyID: string, paymentMethod: PaymentMeth Onyx.merge(`${ONYXKEYS.NVP_LAST_PAYMENT_METHOD}`, {[policyID]: paymentMethod}); } -export type {GPSPoint as GpsPoint}; +export type {GPSPoint as GpsPoint, IOURequestType}; export { setMoneyRequestParticipants, createDistanceRequest, diff --git a/src/pages/RoomInvitePage.tsx b/src/pages/RoomInvitePage.tsx index 28d6f5db27807..b9f9fd09fa449 100644 --- a/src/pages/RoomInvitePage.tsx +++ b/src/pages/RoomInvitePage.tsx @@ -70,9 +70,7 @@ function RoomInvitePage({betas, report, policies}: RoomInvitePageProps) { }); const newSelectedOptions: ReportUtils.OptionData[] = []; selectedOptions.forEach((option) => { - newSelectedOptions.push( - option.login && option.login in detailsMap ? {...detailsMap[option.login], accountID: detailsMap[option.login].accountID ?? undefined, isSelected: true} : option, - ); + newSelectedOptions.push(option.login && option.login in detailsMap ? {...detailsMap[option.login], accountID: detailsMap[option.login].accountID, isSelected: true} : option); }); setUserToInvite(inviteOptions.userToInvite); @@ -136,7 +134,7 @@ function RoomInvitePage({betas, report, policies}: RoomInvitePageProps) { if (isOptionInList) { newSelectedOptions = selectedOptions.filter((selectedOption) => selectedOption.login !== option.login); } else { - newSelectedOptions = [...selectedOptions, {...option, accountID: option.accountID ?? undefined, isSelected: true}]; + newSelectedOptions = [...selectedOptions, {...option, accountID: option.accountID, isSelected: true}]; } setSelectedOptions(newSelectedOptions); diff --git a/src/pages/iou/request/step/IOURequestStepCategory.tsx b/src/pages/iou/request/step/IOURequestStepCategory.tsx index 3a343244a0be7..8aeb9250a4c95 100644 --- a/src/pages/iou/request/step/IOURequestStepCategory.tsx +++ b/src/pages/iou/request/step/IOURequestStepCategory.tsx @@ -69,8 +69,10 @@ function IOURequestStepCategory({ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const reportAction = reportActions?.[report?.parentReportActionID || reportActionID] ?? null; + // The transactionCategory can be an empty string, so to maintain the logic we'd like to keep it in this shape until utils refactor // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const shouldShowCategory = ReportUtils.isGroupPolicy(report) && (transactionCategory || OptionsListUtils.hasEnabledOptions(Object.values(policyCategories ?? {}))); + const shouldShowCategory = ReportUtils.isGroupPolicy(report) && (!!transactionCategory || OptionsListUtils.hasEnabledOptions(Object.values(policyCategories ?? {}))); + const isSplitBill = iouType === CONST.IOU.TYPE.SPLIT; const canEditSplitBill = isSplitBill && reportAction && session?.accountID === reportAction.actorAccountID && TransactionUtils.areRequiredFieldsEmpty(transaction); // eslint-disable-next-line rulesdir/no-negated-variables diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index d00a8a532d73f..2e205de7145d4 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -206,7 +206,7 @@ function IOURequestStepConfirmation({ const trackExpense = useCallback( (selectedParticipants: Participant[], trimmedComment: string, receiptObj?: Receipt, gpsPoints?: IOU.GpsPoint) => { - if (!receiptObj || !report || !transaction) { + if (!report || !transaction) { return; } IOU.trackExpense( @@ -489,6 +489,7 @@ function IOURequestStepConfirmation({ /> {isLoading && } + {/* @ts-expect-error wait until MoneyRequestConfirmationList is migrated to typescript */} diff --git a/src/types/onyx/Transaction.ts b/src/types/onyx/Transaction.ts index 8d05a9af5bd7f..589fc8a6a3912 100644 --- a/src/types/onyx/Transaction.ts +++ b/src/types/onyx/Transaction.ts @@ -1,4 +1,5 @@ import type {KeysOfUnion, ValueOf} from 'type-fest'; +import type {IOURequestType} from '@libs/actions/IOU'; import type CONST from '@src/CONST'; import type ONYXKEYS from '@src/ONYXKEYS'; import type CollectionDataSet from '@src/types/utils/CollectionDataSet'; @@ -136,7 +137,7 @@ type Transaction = OnyxCommon.OnyxValueWithOfflineFeedback< filename?: string; /** Used during the creation flow before the transaction is saved to the server */ - iouRequestType?: ValueOf; + iouRequestType?: IOURequestType; /** The original merchant name */ merchant: string; From 160be500cb74ea831e33040945085bcc18394e7d Mon Sep 17 00:00:00 2001 From: BrtqKr Date: Fri, 5 Apr 2024 00:18:56 +0200 Subject: [PATCH 25/29] review fixes --- src/libs/TransactionUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index f6f519c784c45..a22baa1ffc7ce 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -454,7 +454,7 @@ function getCreated(transaction: OnyxEntry, dateFormat: string = CO * Returns the translation key to use for the header title */ function getHeaderTitleTranslationKey(transaction: OnyxEntry): TranslationPaths { - const headerTitles: Record = { + const headerTitles: Record = { [CONST.IOU.REQUEST_TYPE.DISTANCE]: 'tabSelector.distance', [CONST.IOU.REQUEST_TYPE.MANUAL]: 'tabSelector.manual', [CONST.IOU.REQUEST_TYPE.SCAN]: 'tabSelector.scan', From 5e3adc16080ba2595d59fd4191ecaf3f20e41553 Mon Sep 17 00:00:00 2001 From: BrtqKr Date: Fri, 5 Apr 2024 09:41:58 +0200 Subject: [PATCH 26/29] review fixes --- src/libs/actions/IOU.ts | 4 ---- src/pages/RoomInvitePage.tsx | 4 ++-- src/pages/iou/request/step/IOURequestStepConfirmation.tsx | 2 +- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 83a7e7563b7c0..7de6369f175f0 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -128,10 +128,6 @@ type SendMoneyParamsData = { failureData: OnyxUpdate[]; }; -type OutstandingChildRequest = { - hasOutstandingChildRequest?: boolean; -}; - type GPSPoint = { lat: number; long: number; diff --git a/src/pages/RoomInvitePage.tsx b/src/pages/RoomInvitePage.tsx index b9f9fd09fa449..49e53381e0407 100644 --- a/src/pages/RoomInvitePage.tsx +++ b/src/pages/RoomInvitePage.tsx @@ -70,7 +70,7 @@ function RoomInvitePage({betas, report, policies}: RoomInvitePageProps) { }); const newSelectedOptions: ReportUtils.OptionData[] = []; selectedOptions.forEach((option) => { - newSelectedOptions.push(option.login && option.login in detailsMap ? {...detailsMap[option.login], accountID: detailsMap[option.login].accountID, isSelected: true} : option); + newSelectedOptions.push(option.login && option.login in detailsMap ? {...detailsMap[option.login], isSelected: true} : option); }); setUserToInvite(inviteOptions.userToInvite); @@ -134,7 +134,7 @@ function RoomInvitePage({betas, report, policies}: RoomInvitePageProps) { if (isOptionInList) { newSelectedOptions = selectedOptions.filter((selectedOption) => selectedOption.login !== option.login); } else { - newSelectedOptions = [...selectedOptions, {...option, accountID: option.accountID, isSelected: true}]; + newSelectedOptions = [...selectedOptions, {...option, isSelected: true}]; } setSelectedOptions(newSelectedOptions); diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index 2e205de7145d4..40f3de833d9e6 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -489,7 +489,7 @@ function IOURequestStepConfirmation({ /> {isLoading && } - {/* @ts-expect-error wait until MoneyRequestConfirmationList is migrated to typescript */} + {/* @ts-expect-error TODO: Remove this once MoneyRequestConfirmationList (https://github.com/Expensify/App/issues/36130) is migrated to TypeScript. */} Date: Mon, 8 Apr 2024 10:49:12 +0200 Subject: [PATCH 27/29] remove forwardRef --- src/pages/iou/request/step/StepScreenWrapper.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/iou/request/step/StepScreenWrapper.tsx b/src/pages/iou/request/step/StepScreenWrapper.tsx index c7ed6862ee113..e64f2792d2e49 100644 --- a/src/pages/iou/request/step/StepScreenWrapper.tsx +++ b/src/pages/iou/request/step/StepScreenWrapper.tsx @@ -1,4 +1,4 @@ -import React, {forwardRef} from 'react'; +import React from 'react'; import type {PropsWithChildren} from 'react'; import {View} from 'react-native'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; @@ -72,4 +72,4 @@ function StepScreenWrapper({ ); } -export default forwardRef(StepScreenWrapper); +export default StepScreenWrapper; From 9c4c43ab87e7a06725e33811f35910cf84fc8cec Mon Sep 17 00:00:00 2001 From: BrtqKr Date: Mon, 8 Apr 2024 11:58:17 +0200 Subject: [PATCH 28/29] fix tests --- tests/unit/OptionsListUtilsTest.ts | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/tests/unit/OptionsListUtilsTest.ts b/tests/unit/OptionsListUtilsTest.ts index baefd1bd6d669..6333ee6f1bc7a 100644 --- a/tests/unit/OptionsListUtilsTest.ts +++ b/tests/unit/OptionsListUtilsTest.ts @@ -1213,22 +1213,22 @@ describe('OptionsListUtils', () => { Engineering: { enabled: false, name: 'Engineering', - accountID: null, + accountID: undefined, }, Medical: { enabled: true, name: 'Medical', - accountID: null, + accountID: undefined, }, Accounting: { enabled: true, name: 'Accounting', - accountID: null, + accountID: undefined, }, HR: { enabled: true, name: 'HR', - accountID: null, + accountID: undefined, }, }; const smallResultList: OptionsListUtils.CategorySection[] = [ @@ -1291,57 +1291,57 @@ describe('OptionsListUtils', () => { Engineering: { enabled: false, name: 'Engineering', - accountID: null, + accountID: undefined, }, Medical: { enabled: true, name: 'Medical', - accountID: null, + accountID: undefined, }, Accounting: { enabled: true, name: 'Accounting', - accountID: null, + accountID: undefined, }, HR: { enabled: true, name: 'HR', - accountID: null, + accountID: undefined, }, Food: { enabled: true, name: 'Food', - accountID: null, + accountID: undefined, }, Traveling: { enabled: false, name: 'Traveling', - accountID: null, + accountID: undefined, }, Cleaning: { enabled: true, name: 'Cleaning', - accountID: null, + accountID: undefined, }, Software: { enabled: true, name: 'Software', - accountID: null, + accountID: undefined, }, OfficeSupplies: { enabled: false, name: 'Office Supplies', - accountID: null, + accountID: undefined, }, Taxes: { enabled: true, name: 'Taxes', - accountID: null, + accountID: undefined, }, Benefits: { enabled: true, name: 'Benefits', - accountID: null, + accountID: undefined, }, }; const largeResultList: OptionsListUtils.CategorySection[] = [ From 1314c4d8c4670580e1415b2f31d40b17b185f86c Mon Sep 17 00:00:00 2001 From: BrtqKr Date: Tue, 9 Apr 2024 22:31:40 +0200 Subject: [PATCH 29/29] fix request money flow --- src/libs/actions/IOU.ts | 12 +++++++----- .../iou/request/step/IOURequestStepConfirmation.tsx | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 8ca752a182df7..c75a5fa7a7b48 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -2256,7 +2256,7 @@ function updateDistanceRequest( * Request money from another user */ function requestMoney( - report: OnyxTypes.Report, + report: OnyxEntry, amount: number, currency: string, created: string, @@ -2278,8 +2278,8 @@ function requestMoney( ) { // If the report is iou or expense report, we should get the linked chat report to be passed to the getMoneyRequestInformation function const isMoneyRequestReport = ReportUtils.isMoneyRequestReport(report); - const currentChatReport = isMoneyRequestReport ? ReportUtils.getReport(report.chatReportID) : report; - const moneyRequestReportID = isMoneyRequestReport ? report.reportID : ''; + const currentChatReport = isMoneyRequestReport ? ReportUtils.getReport(report?.chatReportID) : report; + const moneyRequestReportID = isMoneyRequestReport ? report?.reportID : ''; const currentCreated = DateUtils.enrichMoneyRequestTimestamp(created); const { payerAccountID, @@ -2314,7 +2314,7 @@ function requestMoney( payeeEmail, moneyRequestReportID, ); - const activeReportID = isMoneyRequestReport ? report.reportID : chatReport.reportID; + const activeReportID = isMoneyRequestReport ? report?.reportID : chatReport.reportID; const parameters: RequestMoneyParams = { debtorEmail: payerEmail, @@ -2347,7 +2347,9 @@ function requestMoney( API.write(WRITE_COMMANDS.REQUEST_MONEY, parameters, onyxData); resetMoneyRequestInfo(); Navigation.dismissModal(activeReportID); - Report.notifyNewAction(activeReportID, payeeAccountID); + if (activeReportID) { + Report.notifyNewAction(activeReportID, payeeAccountID); + } } /** diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index 5a9dd7343ae26..d20a576d279e3 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -174,7 +174,7 @@ function IOURequestStepConfirmation({ const requestMoney = useCallback( (selectedParticipants: Participant[], trimmedComment: string, receiptObj?: Receipt, gpsPoints?: IOU.GpsPoint) => { - if (!report || !transaction) { + if (!transaction) { return; }