From de807ff8ec80560e18ab40337a171545d0dd19c0 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Fri, 14 Jun 2024 11:16:56 +0200 Subject: [PATCH 01/12] integrate MoneyRequestConfirmationListFooter --- .../MoneyRequestConfirmationList.tsx | 565 +++-------------- .../MoneyRequestConfirmationListFooter.tsx | 568 ++++++++++++++++++ 2 files changed, 650 insertions(+), 483 deletions(-) create mode 100644 src/components/MoneyRequestConfirmationListFooter.tsx diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index e08b35dd3866..85c22ba82f40 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -1,20 +1,16 @@ import {useIsFocused} from '@react-navigation/native'; -import {format} from 'date-fns'; -import {Str} from 'expensify-common'; -import React, {useCallback, useEffect, useMemo, useReducer, useState} from 'react'; +import React, {useCallback, useEffect, useMemo, useState} from 'react'; import type {TextStyle} from 'react-native'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useDebouncedState from '@hooks/useDebouncedState'; import useLocalize from '@hooks/useLocalize'; import {MouseProvider} from '@hooks/useMouseContext'; -import useNetwork from '@hooks/useNetwork'; import usePermissions from '@hooks/usePermissions'; import usePrevious from '@hooks/usePrevious'; import useStyleUtils from '@hooks/useStyleUtils'; -import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import * as CurrencyUtils from '@libs/CurrencyUtils'; import DistanceRequestUtils from '@libs/DistanceRequestUtils'; @@ -26,12 +22,9 @@ import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; import {getCustomUnitRate, isTaxTrackingEnabled} from '@libs/PolicyUtils'; -import * as ReceiptUtils from '@libs/ReceiptUtils'; import * as ReportUtils from '@libs/ReportUtils'; -import {getDefaultWorkspaceAvatar} from '@libs/ReportUtils'; import playSound, {SOUNDS} from '@libs/Sound'; import * as TransactionUtils from '@libs/TransactionUtils'; -import tryResolveUrlFromApiRoot from '@libs/tryResolveUrlFromApiRoot'; import * as IOU from '@userActions/IOU'; import type {IOUAction, IOUType} from '@src/CONST'; import CONST from '@src/CONST'; @@ -44,23 +37,14 @@ import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage'; import type {SplitShares} from '@src/types/onyx/Transaction'; import ButtonWithDropdownMenu from './ButtonWithDropdownMenu'; import type {DropdownOption} from './ButtonWithDropdownMenu/types'; -import ConfirmedRoute from './ConfirmedRoute'; -import ConfirmModal from './ConfirmModal'; import FormHelpMessage from './FormHelpMessage'; -import MenuItem from './MenuItem'; -import MenuItemWithTopDescription from './MenuItemWithTopDescription'; import MoneyRequestAmountInput from './MoneyRequestAmountInput'; -import PDFThumbnail from './PDFThumbnail'; +import MoneyRequestConfirmationListFooter from './MoneyRequestConfirmationListFooter'; import {PressableWithFeedback} from './Pressable'; -import PressableWithoutFocus from './Pressable/PressableWithoutFocus'; -import ReceiptEmptyState from './ReceiptEmptyState'; -import ReceiptImage from './ReceiptImage'; import SelectionList from './SelectionList'; import type {SectionListDataType} from './SelectionList/types'; import UserListItem from './SelectionList/UserListItem'; import SettlementButton from './SettlementButton'; -import ShowMoreButton from './ShowMoreButton'; -import Switch from './Switch'; import Text from './Text'; type MoneyRequestConfirmationListOnyxProps = { @@ -88,9 +72,6 @@ type MoneyRequestConfirmationListOnyxProps = { /** Last selected distance rates */ lastSelectedDistanceRates: OnyxEntry>; - /** The list of all policies */ - allPolicies: OnyxCollection; - /** List of currencies */ currencyList: OnyxEntry; }; @@ -218,20 +199,18 @@ function MoneyRequestConfirmationList({ reportActionID, defaultMileageRate, lastSelectedDistanceRates, - allPolicies, action = CONST.IOU.ACTION.CREATE, currencyList, shouldDisplayReceipt = false, }: MoneyRequestConfirmationListProps) { const policy = policyReal ?? policyDraft; const policyCategories = policyCategoriesReal ?? policyCategoriesDraft; - const theme = useTheme(); + const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const {translate, toLocaleDigit} = useLocalize(); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); - const {canUseP2PDistanceRequests, canUseViolations} = usePermissions(iouType); - const {isOffline} = useNetwork(); + const {canUseP2PDistanceRequests} = usePermissions(iouType); const isTypeRequest = iouType === CONST.IOU.TYPE.SUBMIT; const isTypeSplit = iouType === CONST.IOU.TYPE.SPLIT; @@ -266,44 +245,15 @@ function MoneyRequestConfirmationList({ const currency = (mileageRate as MileageRate)?.currency ?? policyCurrency; - const taxRates = policy?.taxRates ?? null; - // A flag for showing the categories field const shouldShowCategories = isPolicyExpenseChat && (!!iouCategory || OptionsListUtils.hasEnabledOptions(Object.values(policyCategories ?? {}))); - // A flag and a toggler for showing the rest of the form fields - const [shouldExpandFields, toggleShouldExpandFields] = useReducer((state) => !state, false); - - // Do not hide fields in case of paying someone - const shouldShowAllFields = !!isDistanceRequest || shouldExpandFields || !shouldShowSmartScanFields || isTypeSend || !!isEditingSplitBill; - - // In Send Money and Split Bill with Scan flow, we don't allow the Merchant or Date to be edited. For distance requests, don't show the merchant as there's already another "Distance" menu item - const shouldShowDate = (shouldShowSmartScanFields || isDistanceRequest) && !isTypeSend; const shouldShowMerchant = shouldShowSmartScanFields && !isDistanceRequest && !isTypeSend; const policyTagLists = useMemo(() => PolicyUtils.getTagLists(policyTags), [policyTags]); - const senderWorkspace = useMemo(() => { - const senderWorkspaceParticipant = selectedParticipantsProp.find((participant) => participant.isSender); - return allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${senderWorkspaceParticipant?.policyID}`]; - }, [allPolicies, selectedParticipantsProp]); - - const canUpdateSenderWorkspace = useMemo(() => { - const isInvoiceRoomParticipant = selectedParticipantsProp.some((participant) => participant.isInvoiceRoom); - - return PolicyUtils.canSendInvoice(allPolicies) && !!transaction?.isFromGlobalCreate && !isInvoiceRoomParticipant; - }, [allPolicies, selectedParticipantsProp, transaction?.isFromGlobalCreate]); - - const canModifyTaxFields = !isReadOnly && !isDistanceRequest; - - // A flag for showing the tags field - // TODO: remove the !isTypeInvoice from this condition after BE supports tags for invoices: https://github.com/Expensify/App/issues/41281 - const shouldShowTags = useMemo(() => isPolicyExpenseChat && OptionsListUtils.hasEnabledTags(policyTagLists) && !isTypeInvoice, [isPolicyExpenseChat, policyTagLists, isTypeInvoice]); - const shouldShowTax = isTaxTrackingEnabled(isPolicyExpenseChat, policy, isDistanceRequest) && !isTypeInvoice; - // A flag for showing the billable field - const shouldShowBillable = policy?.disabledFields?.defaultBillable === false; const isMovingTransactionFromTrackExpense = IOUUtils.isMovingTransactionFromTrackExpense(action); const hasRoute = TransactionUtils.hasRoute(transaction, isDistanceRequest); const isDistanceRequestWithPendingRoute = isDistanceRequest && (!hasRoute || !rate) && !isMovingTransactionFromTrackExpense; @@ -313,8 +263,6 @@ function MoneyRequestConfirmationList({ shouldCalculateDistanceAmount ? DistanceRequestUtils.getDistanceRequestAmount(distance, unit ?? CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, rate ?? 0) : iouAmount, isDistanceRequest ? currency : iouCurrencyCode, ); - const formattedTaxAmount = CurrencyUtils.convertToDisplayString(transaction?.taxAmount, iouCurrencyCode); - const taxRateTitle = TransactionUtils.getTaxName(policy, transaction); const previousTransactionAmount = usePrevious(transaction?.amount); const previousTransactionCurrency = usePrevious(transaction?.currency); @@ -325,14 +273,6 @@ function MoneyRequestConfirmationList({ const [didConfirm, setDidConfirm] = useState(false); const [didConfirmSplit, setDidConfirmSplit] = useState(false); - const [isAttachmentInvalid, setIsAttachmentInvalid] = useState(false); - const [invalidAttachmentPromt, setInvalidAttachmentPromt] = useState(translate('attachmentPicker.protectedPDFNotSupported')); - - const navigateBack = useCallback( - () => Navigation.goBack(ROUTES.MONEY_REQUEST_CREATE_TAB_SCAN.getRoute(CONST.IOU.ACTION.CREATE, iouType, transactionID, reportID)), - [iouType, reportID, transactionID], - ); - const shouldDisplayFieldError: boolean = useMemo(() => { if (!isEditingSplitBill) { return false; @@ -343,7 +283,6 @@ function MoneyRequestConfirmationList({ const isMerchantEmpty = useMemo(() => !iouMerchant || TransactionUtils.isMerchantMissing(transaction), [transaction, iouMerchant]); const isMerchantRequired = (isPolicyExpenseChat || isTypeInvoice) && (!isScanRequest || isEditingSplitBill) && shouldShowMerchant; - const shouldDisplayMerchantError = isMerchantRequired && (shouldDisplayFieldError || formError === 'iou.error.invalidMerchant') && isMerchantEmpty; const isCategoryRequired = !!policy?.requiresCategory; @@ -840,433 +779,96 @@ function MoneyRequestConfirmationList({ styles.mb2, ]); - // An intermediate structure that helps us classify the fields as "primary" and "supplementary". - // The primary fields are always shown to the user, while an extra action is needed to reveal the supplementary ones. - const classifiedFields = [ - { - item: ( - { - if (isDistanceRequest) { - return; - } - - Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_AMOUNT.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams())); - }} - style={[styles.moneyRequestMenuItem, styles.mt2]} - titleStyle={styles.moneyRequestConfirmationAmount} - disabled={didConfirm} - brickRoadIndicator={shouldDisplayFieldError && TransactionUtils.isAmountMissing(transaction) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} - errorText={shouldDisplayFieldError && TransactionUtils.isAmountMissing(transaction) ? translate('common.error.enterAmount') : ''} - /> - ), - shouldShow: shouldShowSmartScanFields, - isSupplementary: false, - }, - { - item: ( - { - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_DESCRIPTION.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams(), reportActionID), - ); - }} - style={[styles.moneyRequestMenuItem]} - titleStyle={styles.flex1} - disabled={didConfirm} - interactive={!isReadOnly} - numberOfLinesTitle={2} - /> - ), - shouldShow: true, - isSupplementary: false, - }, - { - item: ( - Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()))} - disabled={didConfirm} - // todo: handle edit for transaction while moving from track expense - interactive={!isReadOnly && !isMovingTransactionFromTrackExpense} - /> - ), - shouldShow: isDistanceRequest && !canUseP2PDistanceRequests, - isSupplementary: false, - }, - { - item: ( - Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()))} - disabled={didConfirm} - interactive={!isReadOnly} - /> - ), - shouldShow: isDistanceRequest && canUseP2PDistanceRequests, - isSupplementary: false, - }, - { - item: ( - Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE_RATE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()))} - disabled={didConfirm} - interactive={!!rate && !isReadOnly && isPolicyExpenseChat} - /> - ), - shouldShow: isDistanceRequest && canUseP2PDistanceRequests, - isSupplementary: false, - }, - { - item: ( - { - Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_MERCHANT.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams())); - }} - disabled={didConfirm} - interactive={!isReadOnly} - brickRoadIndicator={shouldDisplayMerchantError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} - errorText={shouldDisplayMerchantError ? translate('common.error.fieldRequired') : ''} - rightLabel={isMerchantRequired && !shouldDisplayMerchantError ? translate('common.required') : ''} - numberOfLinesTitle={2} - /> - ), - shouldShow: shouldShowMerchant, - isSupplementary: !isMerchantRequired, - }, - { - item: ( - { - Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DATE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams(), reportActionID)); - }} - disabled={didConfirm} - interactive={!isReadOnly} - brickRoadIndicator={shouldDisplayFieldError && TransactionUtils.isCreatedMissing(transaction) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} - errorText={shouldDisplayFieldError && TransactionUtils.isCreatedMissing(transaction) ? translate('common.error.enterDate') : ''} - /> - ), - shouldShow: shouldShowDate, - isSupplementary: true, - }, - { - item: ( - - Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams(), reportActionID)) - } - style={[styles.moneyRequestMenuItem]} - titleStyle={styles.flex1} - disabled={didConfirm} - interactive={!isReadOnly} - rightLabel={isCategoryRequired && canUseViolations ? translate('common.required') : ''} - /> - ), - shouldShow: shouldShowCategories, - isSupplementary: action === CONST.IOU.ACTION.CATEGORIZE ? false : !isCategoryRequired, - }, - ...policyTagLists.map(({name, required}, index) => { - const isTagRequired = required ?? false; - return { - item: ( - - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_TAG.getRoute(action, iouType, index, transactionID, reportID, Navigation.getActiveRouteWithoutParams(), reportActionID), - ) - } - style={[styles.moneyRequestMenuItem]} - disabled={didConfirm} - interactive={!isReadOnly} - rightLabel={isTagRequired && canUseViolations ? translate('common.required') : ''} - /> - ), - shouldShow: shouldShowTags, - isSupplementary: !isTagRequired, - }; - }), - { - item: ( - Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_TAX_RATE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()))} - disabled={didConfirm} - interactive={canModifyTaxFields} - /> - ), - shouldShow: shouldShowTax, - isSupplementary: true, - }, - { - item: ( - Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_TAX_AMOUNT.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()))} - disabled={didConfirm} - interactive={canModifyTaxFields} - /> - ), - shouldShow: shouldShowTax, - isSupplementary: true, - }, - { - item: ( - - {translate('common.billable')} - onToggleBillable?.(isOn)} - /> - - ), - shouldShow: shouldShowBillable, - isSupplementary: true, - }, - ]; - - const primaryFields = classifiedFields.filter((classifiedField) => classifiedField.shouldShow && !classifiedField.isSupplementary).map((primaryField) => primaryField.item); - - const supplementaryFields = classifiedFields - .filter((classifiedField) => classifiedField.shouldShow && classifiedField.isSupplementary) - .map((supplementaryField) => supplementaryField.item); - - const { - image: receiptImage, - thumbnail: receiptThumbnail, - isThumbnail, - fileExtension, - isLocalFile, - } = receiptPath && receiptFilename ? ReceiptUtils.getThumbnailAndImageURIs(transaction, receiptPath, receiptFilename) : ({} as ReceiptUtils.ThumbnailAndImageURI); - - const resolvedThumbnail = isLocalFile ? receiptThumbnail : tryResolveUrlFromApiRoot(receiptThumbnail ?? ''); - const resolvedReceiptImage = isLocalFile ? receiptImage : tryResolveUrlFromApiRoot(receiptImage ?? ''); - - const receiptThumbnailContent = useMemo( - () => ( - - {isLocalFile && Str.isPDF(receiptFilename) ? ( - Navigation.navigate(ROUTES.TRANSACTION_RECEIPT.getRoute(reportID ?? '', transactionID ?? ''))} - accessibilityRole={CONST.ROLE.BUTTON} - accessibilityLabel={translate('accessibilityHints.viewAttachment')} - disabled={!shouldDisplayReceipt} - disabledStyle={styles.cursorDefault} - > - { - setIsAttachmentInvalid(true); - setInvalidAttachmentPromt(translate('attachmentPicker.protectedPDFNotSupported')); - }} - onLoadError={() => { - setInvalidAttachmentPromt(translate('attachmentPicker.errorWhileSelectingCorruptedAttachment')); - setIsAttachmentInvalid(true); - }} - /> - - ) : ( - Navigation.navigate(ROUTES.TRANSACTION_RECEIPT.getRoute(reportID ?? '', transactionID ?? ''))} - disabled={!shouldDisplayReceipt || isThumbnail} - accessibilityRole={CONST.ROLE.BUTTON} - accessibilityLabel={translate('accessibilityHints.viewAttachment')} - disabledStyle={styles.cursorDefault} - > - - - )} - - ), - [ - isLocalFile, - receiptFilename, - resolvedThumbnail, - styles.moneyRequestImage, - isAttachmentInvalid, - isThumbnail, - resolvedReceiptImage, - receiptThumbnail, - fileExtension, - isDistanceRequest, - reportID, - transactionID, - translate, - styles.cursorDefault, - shouldDisplayReceipt, - ], - ); - const listFooterContent = useMemo( () => ( - <> - {isTypeInvoice && ( - { - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_SEND_FROM.getRoute(iouType, transaction?.transactionID ?? '-1', reportID, Navigation.getActiveRouteWithoutParams()), - ); - }} - style={styles.moneyRequestMenuItem} - labelStyle={styles.mt2} - titleStyle={styles.flex1} - disabled={didConfirm} - /> - )} - {isDistanceRequest && ( - - - - )} - {!isDistanceRequest && - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - (receiptImage || receiptThumbnail - ? receiptThumbnailContent - : // The empty receipt component should only show for IOU Requests of a paid policy ("Team" or "Corporate") - PolicyUtils.isPaidGroupPolicy(policy) && - !isDistanceRequest && - iouType === CONST.IOU.TYPE.SUBMIT && ( - - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_SCAN.getRoute(CONST.IOU.ACTION.CREATE, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()), - ) - } - /> - ))} - {primaryFields} - {!shouldShowAllFields && ( - - )} - {shouldShowAllFields && supplementaryFields} - - + ), [ - canUpdateSenderWorkspace, + action, + canUseP2PDistanceRequests, + currency, didConfirm, + distance, + formError, + formattedAmount, + hasRoute, + iouCategory, + iouComment, + iouCreated, + iouCurrencyCode, + iouIsBillable, + iouMerchant, iouType, - isAttachmentInvalid, + isCategoryRequired, isDistanceRequest, + isEditingSplitBill, + isMerchantEmpty, + isMerchantRequired, + isMovingTransactionFromTrackExpense, + isPolicyExpenseChat, isReadOnly, isTypeInvoice, - navigateBack, + onToggleBillable, policy, - primaryFields, - receiptImage, - receiptThumbnail, - receiptThumbnailContent, + policyTagLists, + rate, + receiptFilename, + receiptPath, + reportActionID, reportID, - senderWorkspace?.avatarURL, - senderWorkspace?.name, - senderWorkspace?.id, - shouldShowAllFields, - styles.confirmationListMapItem, - styles.flex1, - styles.mb2, - styles.mb5, - styles.moneyRequestMenuItem, - styles.mt1, - styles.mt2, - supplementaryFields, + selectedParticipants, + shouldDisplayFieldError, + shouldDisplayReceipt, + shouldShowCategories, + shouldShowMerchant, + shouldShowSmartScanFields, + shouldShowTax, transaction, transactionID, - translate, - invalidAttachmentPromt, + unit, ], ); @@ -1317,9 +919,6 @@ export default withOnyx; + isCategoryRequired: boolean; + isDistanceRequest: boolean; + isEditingSplitBill: boolean | undefined; + isMerchantEmpty: boolean; + isMerchantRequired: boolean | undefined; + isMovingTransactionFromTrackExpense: boolean; + isPolicyExpenseChat: boolean; + isReadOnly: boolean; + isTypeInvoice: boolean; + onToggleBillable?: (isOn: boolean) => void; + policy: OnyxEntry; + policyTagLists: Array>; + rate: number | undefined; + receiptFilename: string; + receiptPath: string; + reportActionID: string | undefined; + reportID: string; + selectedParticipants: Participant[]; + shouldDisplayFieldError: boolean; + shouldDisplayReceipt: boolean; + shouldShowCategories: boolean; + shouldShowMerchant: boolean; + shouldShowSmartScanFields: boolean; + shouldShowTax: boolean; + transaction: OnyxEntry; + transactionID: string; + unit: Unit | undefined; +}; + +function MoneyRequestConfirmationListFooter({ + action, + canUseP2PDistanceRequests, + currency, + didConfirm, + distance, + formattedAmount, + formError, + hasRoute, + iouCategory, + iouComment, + iouCreated, + iouCurrencyCode, + iouIsBillable, + iouMerchant, + iouType, + isCategoryRequired, + isDistanceRequest, + isEditingSplitBill, + isMerchantEmpty, + isMerchantRequired, + isMovingTransactionFromTrackExpense, + isPolicyExpenseChat, + isReadOnly, + isTypeInvoice, + onToggleBillable, + policy, + policyTagLists, + rate, + receiptFilename, + receiptPath, + reportActionID, + reportID, + selectedParticipants, + shouldDisplayFieldError, + shouldDisplayReceipt, + shouldShowCategories, + shouldShowMerchant, + shouldShowSmartScanFields, + shouldShowTax, + transaction, + transactionID, + unit, +}: MoneyRequestConfirmationListFooterProps) { + const styles = useThemeStyles(); + const theme = useTheme(); + const {translate, toLocaleDigit} = useLocalize(); + const {isOffline} = useNetwork(); + const {canUseViolations} = usePermissions(iouType); + const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY); + + // A flag and a toggler for showing the rest of the form fields + const [shouldExpandFields, toggleShouldExpandFields] = useReducer((state) => !state, false); + + const [isAttachmentInvalid, setIsAttachmentInvalid] = useState(false); + const [invalidAttachmentPrompt, setInvalidAttachmentPrompt] = useState(translate('attachmentPicker.protectedPDFNotSupported')); + + // A flag for showing the tags field + // TODO: remove the !isTypeInvoice from this condition after BE supports tags for invoices: https://github.com/Expensify/App/issues/41281 + const shouldShowTags = useMemo(() => isPolicyExpenseChat && OptionsListUtils.hasEnabledTags(policyTagLists) && !isTypeInvoice, [isPolicyExpenseChat, isTypeInvoice, policyTagLists]); + + const senderWorkspace = useMemo(() => { + const senderWorkspaceParticipant = selectedParticipants.find((participant) => participant.isSender); + return allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${senderWorkspaceParticipant?.policyID}`]; + }, [allPolicies, selectedParticipants]); + + const canUpdateSenderWorkspace = useMemo(() => { + const isInvoiceRoomParticipant = selectedParticipants.some((participant) => participant.isInvoiceRoom); + + return PolicyUtils.canSendInvoice(allPolicies) && !!transaction?.isFromGlobalCreate && !isInvoiceRoomParticipant; + }, [allPolicies, selectedParticipants, transaction?.isFromGlobalCreate]); + + const isTypeSend = iouType === CONST.IOU.TYPE.PAY; + const taxRates = policy?.taxRates ?? null; + // In Send Money and Split Bill with Scan flow, we don't allow the Merchant or Date to be edited. For distance requests, don't show the merchant as there's already another "Distance" menu item + const shouldShowDate = (shouldShowSmartScanFields || isDistanceRequest) && !isTypeSend; + const canModifyTaxFields = !isReadOnly && !isDistanceRequest; + // A flag for showing the billable field + const shouldShowBillable = policy?.disabledFields?.defaultBillable === false; + // Do not hide fields in case of paying someone + const shouldShowAllFields = !!isDistanceRequest || shouldExpandFields || !shouldShowSmartScanFields || isTypeSend || !!isEditingSplitBill; + const formattedTaxAmount = CurrencyUtils.convertToDisplayString(transaction?.taxAmount, iouCurrencyCode); + const taxRateTitle = TransactionUtils.getTaxName(policy, transaction); + const shouldDisplayMerchantError = isMerchantRequired && (shouldDisplayFieldError || formError === 'iou.error.invalidMerchant') && isMerchantEmpty; + // The empty receipt component should only show for IOU Requests of a paid policy ("Team" or "Corporate") + const shouldShowReceiptEmptyState = PolicyUtils.isPaidGroupPolicy(policy) && !isDistanceRequest && iouType === CONST.IOU.TYPE.SUBMIT; + const { + image: receiptImage, + thumbnail: receiptThumbnail, + isThumbnail, + fileExtension, + isLocalFile, + } = receiptPath && receiptFilename ? ReceiptUtils.getThumbnailAndImageURIs(transaction, receiptPath, receiptFilename) : ({} as ReceiptUtils.ThumbnailAndImageURI); + const resolvedThumbnail = isLocalFile ? receiptThumbnail : tryResolveUrlFromApiRoot(receiptThumbnail ?? ''); + const resolvedReceiptImage = isLocalFile ? receiptImage : tryResolveUrlFromApiRoot(receiptImage ?? ''); + + // An intermediate structure that helps us classify the fields as "primary" and "supplementary". + // The primary fields are always shown to the user, while an extra action is needed to reveal the supplementary ones. + const classifiedFields = [ + { + item: ( + { + if (isDistanceRequest) { + return; + } + + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_AMOUNT.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams())); + }} + style={[styles.moneyRequestMenuItem, styles.mt2]} + titleStyle={styles.moneyRequestConfirmationAmount} + disabled={didConfirm} + brickRoadIndicator={shouldDisplayFieldError && TransactionUtils.isAmountMissing(transaction) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} + errorText={shouldDisplayFieldError && TransactionUtils.isAmountMissing(transaction) ? translate('common.error.enterAmount') : ''} + /> + ), + shouldShow: shouldShowSmartScanFields, + isSupplementary: false, + }, + { + item: ( + { + Navigation.navigate( + ROUTES.MONEY_REQUEST_STEP_DESCRIPTION.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams(), reportActionID), + ); + }} + style={[styles.moneyRequestMenuItem]} + titleStyle={styles.flex1} + disabled={didConfirm} + interactive={!isReadOnly} + numberOfLinesTitle={2} + /> + ), + shouldShow: true, + isSupplementary: false, + }, + { + item: ( + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()))} + disabled={didConfirm} + // todo: handle edit for transaction while moving from track expense + interactive={!isReadOnly && !isMovingTransactionFromTrackExpense} + /> + ), + shouldShow: isDistanceRequest && !canUseP2PDistanceRequests, + isSupplementary: false, + }, + { + item: ( + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()))} + disabled={didConfirm} + interactive={!isReadOnly} + /> + ), + shouldShow: isDistanceRequest && canUseP2PDistanceRequests, + isSupplementary: false, + }, + { + item: ( + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE_RATE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()))} + disabled={didConfirm} + interactive={!!rate && !isReadOnly && isPolicyExpenseChat} + /> + ), + shouldShow: isDistanceRequest && canUseP2PDistanceRequests, + isSupplementary: false, + }, + { + item: ( + { + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_MERCHANT.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams())); + }} + disabled={didConfirm} + interactive={!isReadOnly} + brickRoadIndicator={shouldDisplayMerchantError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} + errorText={shouldDisplayMerchantError ? translate('common.error.fieldRequired') : ''} + rightLabel={isMerchantRequired && !shouldDisplayMerchantError ? translate('common.required') : ''} + numberOfLinesTitle={2} + /> + ), + shouldShow: shouldShowMerchant, + isSupplementary: !isMerchantRequired, + }, + { + item: ( + { + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DATE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams(), reportActionID)); + }} + disabled={didConfirm} + interactive={!isReadOnly} + brickRoadIndicator={shouldDisplayFieldError && TransactionUtils.isCreatedMissing(transaction) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} + errorText={shouldDisplayFieldError && TransactionUtils.isCreatedMissing(transaction) ? translate('common.error.enterDate') : ''} + /> + ), + shouldShow: shouldShowDate, + isSupplementary: true, + }, + { + item: ( + + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams(), reportActionID)) + } + style={[styles.moneyRequestMenuItem]} + titleStyle={styles.flex1} + disabled={didConfirm} + interactive={!isReadOnly} + rightLabel={isCategoryRequired && canUseViolations ? translate('common.required') : ''} + /> + ), + shouldShow: shouldShowCategories, + isSupplementary: action === CONST.IOU.ACTION.CATEGORIZE ? false : !isCategoryRequired, + }, + ...policyTagLists.map(({name, required}, index) => { + const isTagRequired = required ?? false; + return { + item: ( + + Navigation.navigate( + ROUTES.MONEY_REQUEST_STEP_TAG.getRoute(action, iouType, index, transactionID, reportID, Navigation.getActiveRouteWithoutParams(), reportActionID), + ) + } + style={[styles.moneyRequestMenuItem]} + disabled={didConfirm} + interactive={!isReadOnly} + rightLabel={isTagRequired && canUseViolations ? translate('common.required') : ''} + /> + ), + shouldShow: shouldShowTags, + isSupplementary: !isTagRequired, + }; + }), + { + item: ( + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_TAX_RATE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()))} + disabled={didConfirm} + interactive={canModifyTaxFields} + /> + ), + shouldShow: shouldShowTax, + isSupplementary: true, + }, + { + item: ( + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_TAX_AMOUNT.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()))} + disabled={didConfirm} + interactive={canModifyTaxFields} + /> + ), + shouldShow: shouldShowTax, + isSupplementary: true, + }, + { + item: ( + + {translate('common.billable')} + onToggleBillable?.(isOn)} + /> + + ), + shouldShow: shouldShowBillable, + isSupplementary: true, + }, + ]; + + const primaryFields = classifiedFields.filter((classifiedField) => classifiedField.shouldShow && !classifiedField.isSupplementary).map((primaryField) => primaryField.item); + + const supplementaryFields = classifiedFields + .filter((classifiedField) => classifiedField.shouldShow && classifiedField.isSupplementary) + .map((supplementaryField) => supplementaryField.item); + + // TODO: Split this into a separate component + const receiptThumbnailContent = useMemo( + () => ( + + {isLocalFile && Str.isPDF(receiptFilename) ? ( + Navigation.navigate(ROUTES.TRANSACTION_RECEIPT.getRoute(reportID ?? '', transactionID ?? ''))} + accessibilityRole={CONST.ROLE.BUTTON} + accessibilityLabel={translate('accessibilityHints.viewAttachment')} + disabled={!shouldDisplayReceipt} + disabledStyle={styles.cursorDefault} + > + { + setIsAttachmentInvalid(true); + setInvalidAttachmentPrompt(translate('attachmentPicker.protectedPDFNotSupported')); + }} + onLoadError={() => { + setInvalidAttachmentPrompt(translate('attachmentPicker.errorWhileSelectingCorruptedAttachment')); + setIsAttachmentInvalid(true); + }} + /> + + ) : ( + Navigation.navigate(ROUTES.TRANSACTION_RECEIPT.getRoute(reportID ?? '', transactionID ?? ''))} + disabled={!shouldDisplayReceipt || isThumbnail} + accessibilityRole={CONST.ROLE.BUTTON} + accessibilityLabel={translate('accessibilityHints.viewAttachment')} + disabledStyle={styles.cursorDefault} + > + + + )} + + ), + [ + styles.moneyRequestImage, + styles.cursorDefault, + isLocalFile, + receiptFilename, + translate, + shouldDisplayReceipt, + resolvedReceiptImage, + isAttachmentInvalid, + isThumbnail, + resolvedThumbnail, + receiptThumbnail, + fileExtension, + isDistanceRequest, + reportID, + transactionID, + ], + ); + + const navigateBack = () => { + Navigation.goBack(ROUTES.MONEY_REQUEST_CREATE_TAB_SCAN.getRoute(CONST.IOU.ACTION.CREATE, iouType, transactionID, reportID)); + }; + + return ( + <> + {isTypeInvoice && ( + { + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_SEND_FROM.getRoute(iouType, transaction?.transactionID ?? '-1', reportID, Navigation.getActiveRouteWithoutParams())); + }} + style={styles.moneyRequestMenuItem} + labelStyle={styles.mt2} + titleStyle={styles.flex1} + disabled={didConfirm} + /> + )} + {isDistanceRequest && ( + + + + )} + {!isDistanceRequest && + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + (receiptImage || receiptThumbnail + ? receiptThumbnailContent + : shouldShowReceiptEmptyState && ( + + Navigation.navigate( + ROUTES.MONEY_REQUEST_STEP_SCAN.getRoute(CONST.IOU.ACTION.CREATE, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()), + ) + } + /> + ))} + {primaryFields} + {!shouldShowAllFields && ( + + )} + {shouldShowAllFields && supplementaryFields} + + + ); +} + +export default memo(MoneyRequestConfirmationListFooter); From 0bcdeee0e27fce3b332e86ca6045212c7c962741 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Fri, 14 Jun 2024 12:20:38 +0200 Subject: [PATCH 02/12] Add display name to MoneyRequestConfirmationListFooter component --- src/components/MoneyRequestConfirmationListFooter.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/MoneyRequestConfirmationListFooter.tsx b/src/components/MoneyRequestConfirmationListFooter.tsx index d983abc3608a..24a08901527a 100644 --- a/src/components/MoneyRequestConfirmationListFooter.tsx +++ b/src/components/MoneyRequestConfirmationListFooter.tsx @@ -565,4 +565,6 @@ function MoneyRequestConfirmationListFooter({ ); } +MoneyRequestConfirmationListFooter.displayName = 'MoneyRequestConfirmationListFooter'; + export default memo(MoneyRequestConfirmationListFooter); From 4abda469409b09b94f65b51512114e8892e3d04c Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Fri, 14 Jun 2024 12:26:27 +0200 Subject: [PATCH 03/12] selectedParticipants prop in MoneyRequestConfirmationList --- src/components/MoneyRequestConfirmationList.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index 85c22ba82f40..a24ddbddca93 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -814,7 +814,7 @@ function MoneyRequestConfirmationList({ receiptPath={receiptPath} reportActionID={reportActionID} reportID={reportID} - selectedParticipants={selectedParticipants} + selectedParticipants={selectedParticipantsProp} shouldDisplayFieldError={shouldDisplayFieldError} shouldDisplayReceipt={shouldDisplayReceipt} shouldShowCategories={shouldShowCategories} @@ -859,7 +859,7 @@ function MoneyRequestConfirmationList({ receiptPath, reportActionID, reportID, - selectedParticipants, + selectedParticipantsProp, shouldDisplayFieldError, shouldDisplayReceipt, shouldShowCategories, From 60fd28829ca9bff7f889e7f788a496c1dd409e3c Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Fri, 14 Jun 2024 12:37:17 +0200 Subject: [PATCH 04/12] optimize setBillable function in IOURequestStepConfirmation --- .../iou/request/step/IOURequestStepConfirmation.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index 1683ef3e3df2..55d4066b6361 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -534,9 +534,12 @@ function IOURequestStepConfirmation({ [transaction?.amount, transaction?.comment, transaction?.currency, participants, currentUserPersonalDetails.accountID, report], ); - const setBillable = (billable: boolean) => { - IOU.setMoneyRequestBillable(transactionID, billable); - }; + const setBillable = useCallback( + (billable: boolean) => { + IOU.setMoneyRequestBillable(transactionID, billable); + }, + [transactionID], + ); return ( Date: Fri, 14 Jun 2024 12:45:05 +0200 Subject: [PATCH 05/12] comments for props --- .../MoneyRequestConfirmationListFooter.tsx | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/src/components/MoneyRequestConfirmationListFooter.tsx b/src/components/MoneyRequestConfirmationListFooter.tsx index 24a08901527a..59f2bf10a110 100644 --- a/src/components/MoneyRequestConfirmationListFooter.tsx +++ b/src/components/MoneyRequestConfirmationListFooter.tsx @@ -39,47 +39,130 @@ import Switch from './Switch'; import Text from './Text'; type MoneyRequestConfirmationListFooterProps = { + /** The action to perform */ action: IOUAction; + + /** Flag indicating if P2P distance requests can be used */ canUseP2PDistanceRequests: boolean | undefined; + + /** The currency of the transaction */ currency: string; + + /** Flag indicating if the confirmation is done */ didConfirm: boolean; + + /** The distance of the transaction */ distance: number; + + /** The formatted amount of the transaction */ formattedAmount: string; + + /** The error message for the form */ formError: string; + + /** Flag indicating if there is a route */ hasRoute: boolean; + + /** The category of the IOU */ iouCategory: string; + + /** The comment of the IOU */ iouComment: string | undefined; + + /** The creation date of the IOU */ iouCreated: string | undefined; + + /** The currency code of the IOU */ iouCurrencyCode: string | undefined; + + /** Flag indicating if the IOU is billable */ iouIsBillable: boolean; + + /** The merchant of the IOU */ iouMerchant: string | undefined; + + /** The type of the IOU */ iouType: Exclude; + + /** Flag indicating if the category is required */ isCategoryRequired: boolean; + + /** Flag indicating if it is a distance request */ isDistanceRequest: boolean; + + /** Flag indicating if it is editing a split bill */ isEditingSplitBill: boolean | undefined; + + /** Flag indicating if the merchant is empty */ isMerchantEmpty: boolean; + + /** Flag indicating if the merchant is required */ isMerchantRequired: boolean | undefined; + + /** Flag indicating if the transaction is moved from track expense */ isMovingTransactionFromTrackExpense: boolean; + + /** Flag indicating if it is a policy expense chat */ isPolicyExpenseChat: boolean; + + /** Flag indicating if it is read-only */ isReadOnly: boolean; + + /** Flag indicating if it is an invoice type */ isTypeInvoice: boolean; + + /** Function to toggle billable */ onToggleBillable?: (isOn: boolean) => void; + + /** The policy */ policy: OnyxEntry; + + /** The policy tag lists */ policyTagLists: Array>; + + /** The rate of the transaction */ rate: number | undefined; + + /** The filename of the receipt */ receiptFilename: string; + + /** The path of the receipt */ receiptPath: string; + + /** The report action ID */ reportActionID: string | undefined; + + /** The report ID */ reportID: string; + + /** The selected participants */ selectedParticipants: Participant[]; + + /** Flag indicating if the field error should be displayed */ shouldDisplayFieldError: boolean; + + /** Flag indicating if the receipt should be displayed */ shouldDisplayReceipt: boolean; + + /** Flag indicating if the categories should be shown */ shouldShowCategories: boolean; + + /** Flag indicating if the merchant should be shown */ shouldShowMerchant: boolean; + + /** Flag indicating if the smart scan fields should be shown */ shouldShowSmartScanFields: boolean; + + /** Flag indicating if the tax should be shown */ shouldShowTax: boolean; + + /** The transaction */ transaction: OnyxEntry; + + /** The transaction ID */ transactionID: string; + + /** The unit */ unit: Unit | undefined; }; From 4ab8d3227049833f45a36dcc4367b82891a46294 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Fri, 14 Jun 2024 13:03:07 +0200 Subject: [PATCH 06/12] Optimize MoneyRequestConfirmationList component via memo --- .../MoneyRequestConfirmationList.tsx | 41 ++++++++++++++++++- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index a24ddbddca93..6fa2b8dd6fdd 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -1,5 +1,6 @@ import {useIsFocused} from '@react-navigation/native'; -import React, {useCallback, useEffect, useMemo, useState} from 'react'; +import lodashIsEqual from 'lodash/isEqual'; +import React, {memo, useCallback, useEffect, useMemo, useState} from 'react'; import type {TextStyle} from 'react-native'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; @@ -922,4 +923,40 @@ export default withOnyx + lodashIsEqual(prevProps.transaction, nextProps.transaction) && + prevProps.onSendMoney === nextProps.onSendMoney && + prevProps.onConfirm === nextProps.onConfirm && + prevProps.iouType === nextProps.iouType && + prevProps.iouAmount === nextProps.iouAmount && + prevProps.isDistanceRequest === nextProps.isDistanceRequest && + prevProps.isPolicyExpenseChat === nextProps.isPolicyExpenseChat && + prevProps.iouCategory === nextProps.iouCategory && + prevProps.shouldShowSmartScanFields === nextProps.shouldShowSmartScanFields && + prevProps.isEditingSplitBill === nextProps.isEditingSplitBill && + prevProps.iouCurrencyCode === nextProps.iouCurrencyCode && + prevProps.iouMerchant === nextProps.iouMerchant && + lodashIsEqual(prevProps.selectedParticipants, nextProps.selectedParticipants) && + lodashIsEqual(prevProps.payeePersonalDetails, nextProps.payeePersonalDetails) && + prevProps.isReadOnly === nextProps.isReadOnly && + prevProps.bankAccountRoute === nextProps.bankAccountRoute && + prevProps.policyID === nextProps.policyID && + prevProps.reportID === nextProps.reportID && + prevProps.receiptPath === nextProps.receiptPath && + prevProps.iouComment === nextProps.iouComment && + prevProps.receiptFilename === nextProps.receiptFilename && + prevProps.iouCreated === nextProps.iouCreated && + prevProps.iouIsBillable === nextProps.iouIsBillable && + prevProps.onToggleBillable === nextProps.onToggleBillable && + prevProps.hasSmartScanFailed === nextProps.hasSmartScanFailed && + prevProps.reportActionID === nextProps.reportActionID && + lodashIsEqual(prevProps.defaultMileageRate, nextProps.defaultMileageRate) && + lodashIsEqual(prevProps.lastSelectedDistanceRates, nextProps.lastSelectedDistanceRates) && + lodashIsEqual(prevProps.action, nextProps.action) && + lodashIsEqual(prevProps.currencyList, nextProps.currencyList) && + prevProps.shouldDisplayReceipt === nextProps.shouldDisplayReceipt, + ), +); From f6a57733ffa7636ecfb4bef84e25bfce79750c3b Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Mon, 17 Jun 2024 10:46:23 +0200 Subject: [PATCH 07/12] remove todo --- src/components/MoneyRequestConfirmationListFooter.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/MoneyRequestConfirmationListFooter.tsx b/src/components/MoneyRequestConfirmationListFooter.tsx index 59f2bf10a110..28743943dbfc 100644 --- a/src/components/MoneyRequestConfirmationListFooter.tsx +++ b/src/components/MoneyRequestConfirmationListFooter.tsx @@ -512,7 +512,6 @@ function MoneyRequestConfirmationListFooter({ .filter((classifiedField) => classifiedField.shouldShow && classifiedField.isSupplementary) .map((supplementaryField) => supplementaryField.item); - // TODO: Split this into a separate component const receiptThumbnailContent = useMemo( () => ( From b79a0f1a5a7de3b8387b5950bad7c4bb45c8c996 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Mon, 17 Jun 2024 10:55:04 +0200 Subject: [PATCH 08/12] use forEach instead of filter and map --- .../MoneyRequestConfirmationListFooter.tsx | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/components/MoneyRequestConfirmationListFooter.tsx b/src/components/MoneyRequestConfirmationListFooter.tsx index 28743943dbfc..22569f40bc07 100644 --- a/src/components/MoneyRequestConfirmationListFooter.tsx +++ b/src/components/MoneyRequestConfirmationListFooter.tsx @@ -506,11 +506,16 @@ function MoneyRequestConfirmationListFooter({ }, ]; - const primaryFields = classifiedFields.filter((classifiedField) => classifiedField.shouldShow && !classifiedField.isSupplementary).map((primaryField) => primaryField.item); - - const supplementaryFields = classifiedFields - .filter((classifiedField) => classifiedField.shouldShow && classifiedField.isSupplementary) - .map((supplementaryField) => supplementaryField.item); + const primaryFields: JSX.Element[] = []; + const supplementaryFields: JSX.Element[] = []; + + classifiedFields.forEach((field) => { + if (field.shouldShow && !field.isSupplementary) { + primaryFields.push(field.item); + } else if (field.shouldShow && field.isSupplementary) { + supplementaryFields.push(field.item); + } + }); const receiptThumbnailContent = useMemo( () => ( From 84a8bd6e30df3bb8c3cb2fbc2f24d196289c57f1 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Mon, 17 Jun 2024 10:56:26 +0200 Subject: [PATCH 09/12] add comments --- src/components/MoneyRequestConfirmationListFooter.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/components/MoneyRequestConfirmationListFooter.tsx b/src/components/MoneyRequestConfirmationListFooter.tsx index 22569f40bc07..47018df77e69 100644 --- a/src/components/MoneyRequestConfirmationListFooter.tsx +++ b/src/components/MoneyRequestConfirmationListFooter.tsx @@ -242,13 +242,19 @@ function MoneyRequestConfirmationListFooter({ const taxRates = policy?.taxRates ?? null; // In Send Money and Split Bill with Scan flow, we don't allow the Merchant or Date to be edited. For distance requests, don't show the merchant as there's already another "Distance" menu item const shouldShowDate = (shouldShowSmartScanFields || isDistanceRequest) && !isTypeSend; + // Determines whether the tax fields can be modified. + // The tax fields can only be modified if the component is not in read-only mode + // and it is not a distance request. const canModifyTaxFields = !isReadOnly && !isDistanceRequest; // A flag for showing the billable field const shouldShowBillable = policy?.disabledFields?.defaultBillable === false; // Do not hide fields in case of paying someone const shouldShowAllFields = !!isDistanceRequest || shouldExpandFields || !shouldShowSmartScanFields || isTypeSend || !!isEditingSplitBill; + // Calculate the formatted tax amount based on the transaction's tax amount and the IOU currency code const formattedTaxAmount = CurrencyUtils.convertToDisplayString(transaction?.taxAmount, iouCurrencyCode); + // Get the tax rate title based on the policy and transaction const taxRateTitle = TransactionUtils.getTaxName(policy, transaction); + // Determine if the merchant error should be displayed const shouldDisplayMerchantError = isMerchantRequired && (shouldDisplayFieldError || formError === 'iou.error.invalidMerchant') && isMerchantEmpty; // The empty receipt component should only show for IOU Requests of a paid policy ("Team" or "Corporate") const shouldShowReceiptEmptyState = PolicyUtils.isPaidGroupPolicy(policy) && !isDistanceRequest && iouType === CONST.IOU.TYPE.SUBMIT; From e6309d3c12a6f01f5ba2b08eaab95d43714b8920 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Mon, 17 Jun 2024 11:00:01 +0200 Subject: [PATCH 10/12] remove useMemo --- .../MoneyRequestConfirmationList.tsx | 136 ++++++------------ 1 file changed, 45 insertions(+), 91 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index 6fa2b8dd6fdd..b5f4c6075373 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -780,97 +780,51 @@ function MoneyRequestConfirmationList({ styles.mb2, ]); - const listFooterContent = useMemo( - () => ( - - ), - [ - action, - canUseP2PDistanceRequests, - currency, - didConfirm, - distance, - formError, - formattedAmount, - hasRoute, - iouCategory, - iouComment, - iouCreated, - iouCurrencyCode, - iouIsBillable, - iouMerchant, - iouType, - isCategoryRequired, - isDistanceRequest, - isEditingSplitBill, - isMerchantEmpty, - isMerchantRequired, - isMovingTransactionFromTrackExpense, - isPolicyExpenseChat, - isReadOnly, - isTypeInvoice, - onToggleBillable, - policy, - policyTagLists, - rate, - receiptFilename, - receiptPath, - reportActionID, - reportID, - selectedParticipantsProp, - shouldDisplayFieldError, - shouldDisplayReceipt, - shouldShowCategories, - shouldShowMerchant, - shouldShowSmartScanFields, - shouldShowTax, - transaction, - transactionID, - unit, - ], + const listFooterContent = ( + ); return ( From 167be3392d923eb6b848e3dc11de1f7206f6ff7c Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Tue, 18 Jun 2024 12:26:35 +0200 Subject: [PATCH 11/12] simplify shouldShowReceiptEmptyState --- src/components/MoneyRequestConfirmationListFooter.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MoneyRequestConfirmationListFooter.tsx b/src/components/MoneyRequestConfirmationListFooter.tsx index 47018df77e69..e224ae200619 100644 --- a/src/components/MoneyRequestConfirmationListFooter.tsx +++ b/src/components/MoneyRequestConfirmationListFooter.tsx @@ -257,7 +257,7 @@ function MoneyRequestConfirmationListFooter({ // Determine if the merchant error should be displayed const shouldDisplayMerchantError = isMerchantRequired && (shouldDisplayFieldError || formError === 'iou.error.invalidMerchant') && isMerchantEmpty; // The empty receipt component should only show for IOU Requests of a paid policy ("Team" or "Corporate") - const shouldShowReceiptEmptyState = PolicyUtils.isPaidGroupPolicy(policy) && !isDistanceRequest && iouType === CONST.IOU.TYPE.SUBMIT; + const shouldShowReceiptEmptyState = iouType === CONST.IOU.TYPE.SUBMIT && PolicyUtils.isPaidGroupPolicy(policy); const { image: receiptImage, thumbnail: receiptThumbnail, From d859d14c78d7bd7590e38ece9f3c7144b9a31d74 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Tue, 18 Jun 2024 12:31:13 +0200 Subject: [PATCH 12/12] add memo for MoneyRequestConfirmationListFooter --- .../MoneyRequestConfirmationListFooter.tsx | 48 ++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/src/components/MoneyRequestConfirmationListFooter.tsx b/src/components/MoneyRequestConfirmationListFooter.tsx index e224ae200619..5541399d34c7 100644 --- a/src/components/MoneyRequestConfirmationListFooter.tsx +++ b/src/components/MoneyRequestConfirmationListFooter.tsx @@ -1,5 +1,6 @@ import {format} from 'date-fns'; import {Str} from 'expensify-common'; +import lodashIsEqual from 'lodash/isEqual'; import React, {memo, useMemo, useReducer, useState} from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; @@ -660,4 +661,49 @@ function MoneyRequestConfirmationListFooter({ MoneyRequestConfirmationListFooter.displayName = 'MoneyRequestConfirmationListFooter'; -export default memo(MoneyRequestConfirmationListFooter); +export default memo( + MoneyRequestConfirmationListFooter, + (prevProps, nextProps) => + lodashIsEqual(prevProps.action, nextProps.action) && + prevProps.canUseP2PDistanceRequests === nextProps.canUseP2PDistanceRequests && + prevProps.currency === nextProps.currency && + prevProps.didConfirm === nextProps.didConfirm && + prevProps.distance === nextProps.distance && + prevProps.formattedAmount === nextProps.formattedAmount && + prevProps.formError === nextProps.formError && + prevProps.hasRoute === nextProps.hasRoute && + prevProps.iouCategory === nextProps.iouCategory && + prevProps.iouComment === nextProps.iouComment && + prevProps.iouCreated === nextProps.iouCreated && + prevProps.iouCurrencyCode === nextProps.iouCurrencyCode && + prevProps.iouIsBillable === nextProps.iouIsBillable && + prevProps.iouMerchant === nextProps.iouMerchant && + prevProps.iouType === nextProps.iouType && + prevProps.isCategoryRequired === nextProps.isCategoryRequired && + prevProps.isDistanceRequest === nextProps.isDistanceRequest && + prevProps.isEditingSplitBill === nextProps.isEditingSplitBill && + prevProps.isMerchantEmpty === nextProps.isMerchantEmpty && + prevProps.isMerchantRequired === nextProps.isMerchantRequired && + prevProps.isMovingTransactionFromTrackExpense === nextProps.isMovingTransactionFromTrackExpense && + prevProps.isPolicyExpenseChat === nextProps.isPolicyExpenseChat && + prevProps.isReadOnly === nextProps.isReadOnly && + prevProps.isTypeInvoice === nextProps.isTypeInvoice && + prevProps.onToggleBillable === nextProps.onToggleBillable && + lodashIsEqual(prevProps.policy, nextProps.policy) && + lodashIsEqual(prevProps.policyTagLists, nextProps.policyTagLists) && + prevProps.rate === nextProps.rate && + prevProps.receiptFilename === nextProps.receiptFilename && + prevProps.receiptPath === nextProps.receiptPath && + prevProps.reportActionID === nextProps.reportActionID && + prevProps.reportID === nextProps.reportID && + lodashIsEqual(prevProps.selectedParticipants, nextProps.selectedParticipants) && + prevProps.shouldDisplayFieldError === nextProps.shouldDisplayFieldError && + prevProps.shouldDisplayReceipt === nextProps.shouldDisplayReceipt && + prevProps.shouldShowCategories === nextProps.shouldShowCategories && + prevProps.shouldShowMerchant === nextProps.shouldShowMerchant && + prevProps.shouldShowSmartScanFields === nextProps.shouldShowSmartScanFields && + prevProps.shouldShowTax === nextProps.shouldShowTax && + lodashIsEqual(prevProps.transaction, nextProps.transaction) && + prevProps.transactionID === nextProps.transactionID && + prevProps.unit === nextProps.unit, +);