From 42b51048b351b14bc12a1b70e8606cd4b654726c Mon Sep 17 00:00:00 2001 From: Test Date: Tue, 12 May 2026 10:47:06 +0200 Subject: [PATCH 1/3] refactor: extract sections from MoneyRequestConfirmationListFooter --- src/components/ConfirmedRoute.tsx | 8 +- .../MoneyRequestConfirmationListFooter.tsx | 162 +++++++----------- .../fieldGroups/ClassificationFields.tsx | 150 +++++++++++++--- .../sections/DistanceMapSection.tsx | 50 ++++++ .../sections/InvoiceSenderSection.tsx | 34 ++++ .../sections/PerDiemSection.tsx | 46 +++++ .../sections/ReceiptSection.tsx | 138 +++++++++++++++ 7 files changed, 453 insertions(+), 135 deletions(-) create mode 100644 src/components/MoneyRequestConfirmationListFooter/sections/DistanceMapSection.tsx create mode 100644 src/components/MoneyRequestConfirmationListFooter/sections/InvoiceSenderSection.tsx create mode 100644 src/components/MoneyRequestConfirmationListFooter/sections/PerDiemSection.tsx create mode 100644 src/components/MoneyRequestConfirmationListFooter/sections/ReceiptSection.tsx diff --git a/src/components/ConfirmedRoute.tsx b/src/components/ConfirmedRoute.tsx index cd01342c56a5..dc95b1228f78 100644 --- a/src/components/ConfirmedRoute.tsx +++ b/src/components/ConfirmedRoute.tsx @@ -1,4 +1,4 @@ -import React, {useEffect} from 'react'; +import React from 'react'; import type {ReactNode} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; @@ -9,7 +9,6 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import getArrayDepth from '@libs/getArrayDepth'; import {getWaypointIndex} from '@libs/TransactionUtils'; -import {init as initMapboxToken, stop as stopMapboxToken} from '@userActions/MapboxToken'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Transaction} from '@src/types/onyx'; @@ -82,11 +81,6 @@ function ConfirmedRoute({transaction, isSmallerIcon, shouldHaveBorderRadius = tr }); } - useEffect(() => { - initMapboxToken(); - return stopMapboxToken; - }, []); - const hasCoordinates = getArrayDepth(coordinates) === 3 ? !!coordinates.flat().length : !!coordinates.length; const shouldDisplayMap = !requireRouteToDisplayMap || hasCoordinates; diff --git a/src/components/MoneyRequestConfirmationListFooter.tsx b/src/components/MoneyRequestConfirmationListFooter.tsx index d7e4cc0f369c..cc3210ee2912 100644 --- a/src/components/MoneyRequestConfirmationListFooter.tsx +++ b/src/components/MoneyRequestConfirmationListFooter.tsx @@ -1,30 +1,23 @@ -import React, {memo} from 'react'; +import React from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import useIsInLandscapeMode from '@hooks/useIsInLandscapeMode'; import usePermissions from '@hooks/usePermissions'; import useThemeStyles from '@hooks/useThemeStyles'; -import useWindowDimensions from '@hooks/useWindowDimensions'; -import {shouldShowReceiptEmptyState} from '@libs/IOUUtils'; -import Navigation from '@libs/Navigation/Navigation'; -import {getPerDiemCustomUnit} from '@libs/PolicyUtils'; +import {isScanRequest} from '@libs/TransactionUtils'; import CONST from '@src/CONST'; import type {IOUAction, IOUType} from '@src/CONST'; -import ROUTES from '@src/ROUTES'; import type * as OnyxTypes from '@src/types/onyx'; import type {Participant} from '@src/types/onyx/IOU'; import type {Unit} from '@src/types/onyx/Policy'; -import ConfirmedRoute from './ConfirmedRoute'; -import InvoiceSenderField from './MoneyRequestConfirmationList/sections/InvoiceSenderField'; -import PerDiemFields from './MoneyRequestConfirmationList/sections/PerDiemFields'; import ConfirmationFieldList from './MoneyRequestConfirmationListFooter/ConfirmationFieldList'; -import ConfirmationReceiptThumbnail from './MoneyRequestConfirmationListFooter/ConfirmationReceiptThumbnail'; -import useCompactReceiptDimensions from './MoneyRequestConfirmationListFooter/hooks/useCompactReceiptDimensions'; import useFooterDerivedFlags from './MoneyRequestConfirmationListFooter/hooks/useFooterDerivedFlags'; import useFooterTagVisibility from './MoneyRequestConfirmationListFooter/hooks/useFooterTagVisibility'; -import useReceiptThumbnailSource from './MoneyRequestConfirmationListFooter/hooks/useReceiptThumbnailSource'; -import ReceiptEmptyState from './ReceiptEmptyState'; +import DistanceMapSection from './MoneyRequestConfirmationListFooter/sections/DistanceMapSection'; +import InvoiceSenderSection from './MoneyRequestConfirmationListFooter/sections/InvoiceSenderSection'; +import PerDiemSection from './MoneyRequestConfirmationListFooter/sections/PerDiemSection'; +import ReceiptSection from './MoneyRequestConfirmationListFooter/sections/ReceiptSection'; type MoneyRequestConfirmationListFooterProps = { /** The action to perform */ @@ -235,7 +228,6 @@ function MoneyRequestConfirmationListFooter({ setShowMoreFields = () => {}, }: MoneyRequestConfirmationListFooterProps) { const styles = useThemeStyles(); - const {windowWidth} = useWindowDimensions(); const isInLandscapeMode = useIsInLandscapeMode(); const {isBetaEnabled} = usePermissions(); const isNewManualExpenseFlowEnabled = isBetaEnabled(CONST.BETAS.NEW_MANUAL_EXPENSE_FLOW); @@ -264,96 +256,64 @@ function MoneyRequestConfirmationListFooter({ transaction, }); - const receiptSource = useReceiptThumbnailSource({transaction, receiptPath, receiptFilename}); - - const horizontalMargin = typeof styles.moneyRequestImage.marginHorizontal === 'number' ? styles.moneyRequestImage.marginHorizontal : 0; - const compact = useCompactReceiptDimensions({ - showMoreFields, - isScan: flags.isScan, - isInLandscapeMode, - windowWidth, - horizontalMargin, - }); - - const showReceiptEmptyState = shouldShowReceiptEmptyState(iouType, action, policy, isPerDiemRequest); - const perDiemCustomUnit = getPerDiemCustomUnit(policy); + // ReceiptSection owns the receipt thumbnail + compact-mode hooks; the outer wrapper only needs this boolean. + const isCompactMode = !showMoreFields && isScanRequest(transaction) && !isInLandscapeMode; return ( - + - {isTypeInvoice && ( - - )} - {flags.shouldShowMap && ( - - - - )} - {isPerDiemRequest && action !== CONST.IOU.ACTION.SUBMIT && ( - - )} + + + - {(!flags.shouldShowMap || isManualDistanceRequest || isOdometerDistanceRequest) && - (receiptSource.hasReceiptImageOrThumbnail || isLoadingReceipt ? ( - - ) : ( - showReceiptEmptyState && ( - { - if (!transactionID) { - return; - } - Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_SCAN.getRoute(CONST.IOU.ACTION.CREATE, iouType, transactionID, reportID, Navigation.getActiveRoute())); - }} - style={[ - compact.isCompactMode ? undefined : styles.mv3, - compact.isCompactMode && compact.compactReceiptStyle ? compact.compactReceiptStyle : styles.moneyRequestViewImage, - ]} - /> - ) - ))} + ); } -export default memo(MoneyRequestConfirmationListFooter); +export default MoneyRequestConfirmationListFooter; diff --git a/src/components/MoneyRequestConfirmationListFooter/fieldGroups/ClassificationFields.tsx b/src/components/MoneyRequestConfirmationListFooter/fieldGroups/ClassificationFields.tsx index 634e6493d778..77511d7060ae 100644 --- a/src/components/MoneyRequestConfirmationListFooter/fieldGroups/ClassificationFields.tsx +++ b/src/components/MoneyRequestConfirmationListFooter/fieldGroups/ClassificationFields.tsx @@ -11,6 +11,81 @@ import type {IOUAction, IOUType} from '@src/CONST'; import type * as OnyxTypes from '@src/types/onyx'; import type {FieldVisibility, TagEntry} from './fieldVisibility'; +type TagFieldRowProps = { + /** Tag entry to render (carries name, index, and required flag) */ + entry: TagEntry; + + /** Tag lists configured on the policy (used to look up the list at the entry's index) */ + policyTagLists: Array>; + + /** Previous render's per-tag-list `shouldShow` projection (drives transition styling) */ + previousTagsVisibility: boolean[]; + + /** Whether the user has confirmed (locks editable controls) */ + didConfirm: boolean; + + /** Whether the surface is read-only */ + isReadOnly: boolean; + + /** ID of the active transaction */ + transactionID: string | undefined; + + /** Action being performed (drives section navigation targets) */ + action: IOUAction; + + /** Type of IOU being confirmed */ + iouType: Exclude; + + /** ID of the report the transaction belongs to */ + reportID: string; + + /** ID of the originating report action when editing */ + reportActionID: string | undefined; + + /** Active transaction */ + transaction: OnyxEntry; + + /** Form-level error message */ + formError: string; +}; + +function TagFieldRow({ + entry: {index, isTagRequired}, + policyTagLists, + previousTagsVisibility, + didConfirm, + isReadOnly, + transactionID, + action, + iouType, + reportID, + reportActionID, + transaction, + formError, +}: TagFieldRowProps) { + const policyTagList = policyTagLists.at(index); + if (!policyTagList) { + return null; + } + return ( + + ); +} + type ClassificationFieldsProps = { /** Action being performed (drives section navigation targets) */ action: IOUAction; @@ -103,31 +178,19 @@ function ClassificationFields({ isCompactMode, fieldVisibility, }: ClassificationFieldsProps) { - const renderTagFields = (entries: TagEntry[]) => - entries.map(({name, index, isTagRequired}) => { - const policyTagList = policyTagLists.at(index); - if (!policyTagList) { - return null; - } - return ( - - ); - }); + const tagRowSharedProps = { + policyTagLists, + previousTagsVisibility, + didConfirm, + isReadOnly, + transactionID, + action, + iouType, + reportID, + reportActionID, + transaction, + formError, + }; return ( <> @@ -163,9 +226,42 @@ function ClassificationFields({ /> )} - {renderTagFields(fieldVisibility.tagsRequired)} + {fieldVisibility.tagsRequired.map((entry) => ( + + ))} - {!isCompactMode && renderTagFields(fieldVisibility.tagsOptional)} + {!isCompactMode && + fieldVisibility.tagsOptional.map((entry) => ( + + ))} {!isCompactMode && fieldVisibility.tax && ( ; + isDistanceRequest: boolean; + isManualDistanceRequest: boolean; + isOdometerDistanceRequest: boolean; + iouType: Exclude; + isReadOnly: boolean; +}; + +function DistanceMapSection({transaction, isDistanceRequest, isManualDistanceRequest, isOdometerDistanceRequest, iouType, isReadOnly}: DistanceMapSectionProps) { + const styles = useThemeStyles(); + + const hasPendingWaypoints = transaction && isFetchingWaypointsFromServer(transaction); + const hasErrors = !isEmptyObject(transaction?.errors) || !isEmptyObject(transaction?.errorFields?.route) || !isEmptyObject(transaction?.errorFields?.waypoints); + const shouldShowMap = + isDistanceRequest && !isManualDistanceRequest && !isOdometerDistanceRequest && [hasErrors, hasPendingWaypoints, iouType !== CONST.IOU.TYPE.SPLIT, !isReadOnly].some(Boolean); + + // Mapbox token lifecycle is gated here so it only runs when the map is visible + useEffect(() => { + if (!shouldShowMap) { + return; + } + initMapboxToken(); + return stopMapboxToken; + }, [shouldShowMap]); + + if (!shouldShowMap) { + return null; + } + + return ( + + + + ); +} + +export default DistanceMapSection; diff --git a/src/components/MoneyRequestConfirmationListFooter/sections/InvoiceSenderSection.tsx b/src/components/MoneyRequestConfirmationListFooter/sections/InvoiceSenderSection.tsx new file mode 100644 index 000000000000..bf13a0d73543 --- /dev/null +++ b/src/components/MoneyRequestConfirmationListFooter/sections/InvoiceSenderSection.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; +import InvoiceSenderField from '@components/MoneyRequestConfirmationList/sections/InvoiceSenderField'; +import CONST from '@src/CONST'; +import type {IOUType} from '@src/CONST'; +import type * as OnyxTypes from '@src/types/onyx'; +import type {Participant} from '@src/types/onyx/IOU'; + +type InvoiceSenderSectionProps = { + iouType: Exclude; + reportID: string; + selectedParticipants: Participant[]; + isReadOnly: boolean; + didConfirm: boolean; + transaction: OnyxEntry; +}; + +function InvoiceSenderSection({iouType, reportID, selectedParticipants, isReadOnly, didConfirm, transaction}: InvoiceSenderSectionProps) { + if (iouType !== CONST.IOU.TYPE.INVOICE) { + return null; + } + return ( + + ); +} + +export default InvoiceSenderSection; diff --git a/src/components/MoneyRequestConfirmationListFooter/sections/PerDiemSection.tsx b/src/components/MoneyRequestConfirmationListFooter/sections/PerDiemSection.tsx new file mode 100644 index 000000000000..aab1ed6c89b7 --- /dev/null +++ b/src/components/MoneyRequestConfirmationListFooter/sections/PerDiemSection.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; +import PerDiemFields from '@components/MoneyRequestConfirmationList/sections/PerDiemFields'; +import {getPerDiemCustomUnit} from '@libs/PolicyUtils'; +import CONST from '@src/CONST'; +import type {IOUAction, IOUType} from '@src/CONST'; +import type * as OnyxTypes from '@src/types/onyx'; + +type PerDiemSectionProps = { + action: IOUAction; + iouType: Exclude; + isPerDiemRequest: boolean; + transaction: OnyxEntry; + reportID: string; + transactionID: string | undefined; + policy: OnyxEntry; + isReadOnly: boolean; + didConfirm: boolean; + shouldDisplayFieldError: boolean; + formError: string; +}; + +function PerDiemSection({action, iouType, isPerDiemRequest, transaction, reportID, transactionID, policy, isReadOnly, didConfirm, shouldDisplayFieldError, formError}: PerDiemSectionProps) { + if (!isPerDiemRequest || action === CONST.IOU.ACTION.SUBMIT) { + return null; + } + + const perDiemCustomUnit = getPerDiemCustomUnit(policy); + + return ( + + ); +} + +export default PerDiemSection; diff --git a/src/components/MoneyRequestConfirmationListFooter/sections/ReceiptSection.tsx b/src/components/MoneyRequestConfirmationListFooter/sections/ReceiptSection.tsx new file mode 100644 index 000000000000..4ae0b9b7fbbf --- /dev/null +++ b/src/components/MoneyRequestConfirmationListFooter/sections/ReceiptSection.tsx @@ -0,0 +1,138 @@ +import React from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; +import ConfirmationReceiptThumbnail from '@components/MoneyRequestConfirmationListFooter/ConfirmationReceiptThumbnail'; +import useCompactReceiptDimensions from '@components/MoneyRequestConfirmationListFooter/hooks/useCompactReceiptDimensions'; +import useReceiptThumbnailSource from '@components/MoneyRequestConfirmationListFooter/hooks/useReceiptThumbnailSource'; +import ReceiptEmptyState from '@components/ReceiptEmptyState'; +import useIsInLandscapeMode from '@hooks/useIsInLandscapeMode'; +import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; +import {shouldShowReceiptEmptyState} from '@libs/IOUUtils'; +import Navigation from '@libs/Navigation/Navigation'; +import {isFetchingWaypointsFromServer, isScanRequest} from '@libs/TransactionUtils'; +import CONST from '@src/CONST'; +import type {IOUAction, IOUType} from '@src/CONST'; +import ROUTES from '@src/ROUTES'; +import type * as OnyxTypes from '@src/types/onyx'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; + +type ReceiptSectionProps = { + transaction: OnyxEntry; + transactionID: string | undefined; + reportID: string; + action: IOUAction; + iouType: Exclude; + policy: OnyxEntry; + isPerDiemRequest: boolean; + isDistanceRequest: boolean; + isManualDistanceRequest: boolean; + isOdometerDistanceRequest: boolean; + isReadOnly: boolean; + isReceiptEditable: boolean; + shouldDisplayReceipt: boolean; + isLoadingReceipt: boolean; + receiptPath: string | number; + receiptFilename: string; + showMoreFields: boolean; + onPDFLoadError?: () => void; + onPDFPassword?: () => void; +}; + +function ReceiptSection({ + transaction, + transactionID, + reportID, + action, + iouType, + policy, + isPerDiemRequest, + isDistanceRequest, + isManualDistanceRequest, + isOdometerDistanceRequest, + isReadOnly, + isReceiptEditable, + shouldDisplayReceipt, + isLoadingReceipt, + receiptPath, + receiptFilename, + showMoreFields, + onPDFLoadError, + onPDFPassword, +}: ReceiptSectionProps) { + const styles = useThemeStyles(); + const {windowWidth} = useWindowDimensions(); + const isInLandscapeMode = useIsInLandscapeMode(); + + const receiptSource = useReceiptThumbnailSource({transaction, receiptPath, receiptFilename}); + + const horizontalMargin = typeof styles.moneyRequestImage.marginHorizontal === 'number' ? styles.moneyRequestImage.marginHorizontal : 0; + const isScan = isScanRequest(transaction); + const compact = useCompactReceiptDimensions({ + showMoreFields, + isScan, + isInLandscapeMode, + windowWidth, + horizontalMargin, + }); + + // Mirror the shouldShowMap logic to determine whether the receipt area should render. + // When a GPS distance map is visible, the receipt is hidden (unless manual/odometer). + const hasPendingWaypoints = transaction && isFetchingWaypointsFromServer(transaction); + const hasErrors = !isEmptyObject(transaction?.errors) || !isEmptyObject(transaction?.errorFields?.route) || !isEmptyObject(transaction?.errorFields?.waypoints); + const shouldShowMap = + isDistanceRequest && !isManualDistanceRequest && !isOdometerDistanceRequest && [hasErrors, hasPendingWaypoints, iouType !== CONST.IOU.TYPE.SPLIT, !isReadOnly].some(Boolean); + const shouldShowReceiptArea = !shouldShowMap || isManualDistanceRequest || isOdometerDistanceRequest; + + if (!shouldShowReceiptArea) { + return null; + } + + if (receiptSource.hasReceiptImageOrThumbnail || isLoadingReceipt) { + return ( + + ); + } + + const showReceiptEmptyState = shouldShowReceiptEmptyState(iouType, action, policy, isPerDiemRequest); + if (!showReceiptEmptyState) { + return null; + } + + return ( + { + if (!transactionID) { + return; + } + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_SCAN.getRoute(CONST.IOU.ACTION.CREATE, iouType, transactionID, reportID, Navigation.getActiveRoute())); + }} + style={[compact.isCompactMode ? undefined : styles.mv3, compact.isCompactMode && compact.compactReceiptStyle ? compact.compactReceiptStyle : styles.moneyRequestViewImage]} + /> + ); +} + +export default ReceiptSection; +export type {ReceiptSectionProps}; From 04d339fa1d8bad07006feb53685ab796be3c5698 Mon Sep 17 00:00:00 2001 From: Test Date: Tue, 12 May 2026 13:50:10 +0200 Subject: [PATCH 2/3] extract shouldShowDistanceMap util and restore mapbox --- src/components/ConfirmedRoute.tsx | 8 +++++- .../hooks/useFooterDerivedFlags.ts | 11 +++----- .../sections/DistanceMapSection.tsx | 22 +++------------- .../sections/ReceiptSection.tsx | 10 +++---- .../shouldShowDistanceMap.ts | 26 +++++++++++++++++++ 5 files changed, 43 insertions(+), 34 deletions(-) create mode 100644 src/components/MoneyRequestConfirmationListFooter/shouldShowDistanceMap.ts diff --git a/src/components/ConfirmedRoute.tsx b/src/components/ConfirmedRoute.tsx index dc95b1228f78..eabbdfbbced8 100644 --- a/src/components/ConfirmedRoute.tsx +++ b/src/components/ConfirmedRoute.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {useEffect} from 'react'; import type {ReactNode} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; @@ -9,6 +9,7 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import getArrayDepth from '@libs/getArrayDepth'; import {getWaypointIndex} from '@libs/TransactionUtils'; +import {init as initMapboxToken, stop as stopMapboxToken} from '@userActions/MapboxToken'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Transaction} from '@src/types/onyx'; @@ -48,6 +49,11 @@ function ConfirmedRoute({transaction, isSmallerIcon, shouldHaveBorderRadius = tr const [mapboxAccessToken] = useOnyx(ONYXKEYS.MAPBOX_ACCESS_TOKEN); + useEffect(() => { + initMapboxToken(); + return stopMapboxToken; + }, []); + const getMarkerComponent = (icon: IconAsset): ReactNode => ( ; @@ -22,19 +20,7 @@ type DistanceMapSectionProps = { function DistanceMapSection({transaction, isDistanceRequest, isManualDistanceRequest, isOdometerDistanceRequest, iouType, isReadOnly}: DistanceMapSectionProps) { const styles = useThemeStyles(); - const hasPendingWaypoints = transaction && isFetchingWaypointsFromServer(transaction); - const hasErrors = !isEmptyObject(transaction?.errors) || !isEmptyObject(transaction?.errorFields?.route) || !isEmptyObject(transaction?.errorFields?.waypoints); - const shouldShowMap = - isDistanceRequest && !isManualDistanceRequest && !isOdometerDistanceRequest && [hasErrors, hasPendingWaypoints, iouType !== CONST.IOU.TYPE.SPLIT, !isReadOnly].some(Boolean); - - // Mapbox token lifecycle is gated here so it only runs when the map is visible - useEffect(() => { - if (!shouldShowMap) { - return; - } - initMapboxToken(); - return stopMapboxToken; - }, [shouldShowMap]); + const shouldShowMap = shouldShowDistanceMap({transaction, isDistanceRequest, isManualDistanceRequest, isOdometerDistanceRequest, iouType, isReadOnly}); if (!shouldShowMap) { return null; diff --git a/src/components/MoneyRequestConfirmationListFooter/sections/ReceiptSection.tsx b/src/components/MoneyRequestConfirmationListFooter/sections/ReceiptSection.tsx index 4ae0b9b7fbbf..c4e732057cab 100644 --- a/src/components/MoneyRequestConfirmationListFooter/sections/ReceiptSection.tsx +++ b/src/components/MoneyRequestConfirmationListFooter/sections/ReceiptSection.tsx @@ -3,18 +3,18 @@ import type {OnyxEntry} from 'react-native-onyx'; import ConfirmationReceiptThumbnail from '@components/MoneyRequestConfirmationListFooter/ConfirmationReceiptThumbnail'; import useCompactReceiptDimensions from '@components/MoneyRequestConfirmationListFooter/hooks/useCompactReceiptDimensions'; import useReceiptThumbnailSource from '@components/MoneyRequestConfirmationListFooter/hooks/useReceiptThumbnailSource'; +import shouldShowDistanceMap from '@components/MoneyRequestConfirmationListFooter/shouldShowDistanceMap'; import ReceiptEmptyState from '@components/ReceiptEmptyState'; import useIsInLandscapeMode from '@hooks/useIsInLandscapeMode'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import {shouldShowReceiptEmptyState} from '@libs/IOUUtils'; import Navigation from '@libs/Navigation/Navigation'; -import {isFetchingWaypointsFromServer, isScanRequest} from '@libs/TransactionUtils'; +import {isScanRequest} from '@libs/TransactionUtils'; import CONST from '@src/CONST'; import type {IOUAction, IOUType} from '@src/CONST'; import ROUTES from '@src/ROUTES'; import type * as OnyxTypes from '@src/types/onyx'; -import {isEmptyObject} from '@src/types/utils/EmptyObject'; type ReceiptSectionProps = { transaction: OnyxEntry; @@ -75,12 +75,8 @@ function ReceiptSection({ horizontalMargin, }); - // Mirror the shouldShowMap logic to determine whether the receipt area should render. // When a GPS distance map is visible, the receipt is hidden (unless manual/odometer). - const hasPendingWaypoints = transaction && isFetchingWaypointsFromServer(transaction); - const hasErrors = !isEmptyObject(transaction?.errors) || !isEmptyObject(transaction?.errorFields?.route) || !isEmptyObject(transaction?.errorFields?.waypoints); - const shouldShowMap = - isDistanceRequest && !isManualDistanceRequest && !isOdometerDistanceRequest && [hasErrors, hasPendingWaypoints, iouType !== CONST.IOU.TYPE.SPLIT, !isReadOnly].some(Boolean); + const shouldShowMap = shouldShowDistanceMap({transaction, isDistanceRequest, isManualDistanceRequest, isOdometerDistanceRequest, iouType, isReadOnly}); const shouldShowReceiptArea = !shouldShowMap || isManualDistanceRequest || isOdometerDistanceRequest; if (!shouldShowReceiptArea) { diff --git a/src/components/MoneyRequestConfirmationListFooter/shouldShowDistanceMap.ts b/src/components/MoneyRequestConfirmationListFooter/shouldShowDistanceMap.ts new file mode 100644 index 000000000000..b1a87f7df758 --- /dev/null +++ b/src/components/MoneyRequestConfirmationListFooter/shouldShowDistanceMap.ts @@ -0,0 +1,26 @@ +import type {OnyxEntry} from 'react-native-onyx'; +import {isFetchingWaypointsFromServer} from '@libs/TransactionUtils'; +import CONST from '@src/CONST'; +import type {IOUType} from '@src/CONST'; +import type {Transaction} from '@src/types/onyx'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; + +type ShouldShowDistanceMapParams = { + transaction: OnyxEntry; + isDistanceRequest: boolean; + isManualDistanceRequest: boolean; + isOdometerDistanceRequest: boolean; + iouType: Exclude; + isReadOnly: boolean; +}; + +function shouldShowDistanceMap({transaction, isDistanceRequest, isManualDistanceRequest, isOdometerDistanceRequest, iouType, isReadOnly}: ShouldShowDistanceMapParams): boolean { + if (!isDistanceRequest || isManualDistanceRequest || isOdometerDistanceRequest) { + return false; + } + const hasPendingWaypoints = transaction && isFetchingWaypointsFromServer(transaction); + const hasErrors = !isEmptyObject(transaction?.errors) || !isEmptyObject(transaction?.errorFields?.route) || !isEmptyObject(transaction?.errorFields?.waypoints); + return [hasErrors, hasPendingWaypoints, iouType !== CONST.IOU.TYPE.SPLIT, !isReadOnly].some(Boolean); +} + +export default shouldShowDistanceMap; From 49b1670f49c134e94814d2e9dc2d46a8d4e7d64f Mon Sep 17 00:00:00 2001 From: Test Date: Thu, 14 May 2026 10:13:45 +0200 Subject: [PATCH 3/3] add comments to section props and remove dead code --- .../MoneyRequestConfirmationListFooter.tsx | 2 - .../hooks/useFooterDerivedFlags.ts | 12 ------ .../sections/DistanceMapSection.tsx | 11 ++++++ .../sections/InvoiceSenderSection.tsx | 11 ++++++ .../sections/PerDiemSection.tsx | 21 +++++++++++ .../sections/ReceiptSection.tsx | 37 +++++++++++++++++++ 6 files changed, 80 insertions(+), 14 deletions(-) diff --git a/src/components/MoneyRequestConfirmationListFooter.tsx b/src/components/MoneyRequestConfirmationListFooter.tsx index 8d6c5f63a221..dbdabc16b9d0 100644 --- a/src/components/MoneyRequestConfirmationListFooter.tsx +++ b/src/components/MoneyRequestConfirmationListFooter.tsx @@ -254,8 +254,6 @@ function MoneyRequestConfirmationListFooter({ isPolicyExpenseChat, isReadOnly, isDistanceRequest, - isManualDistanceRequest, - isOdometerDistanceRequest, isPerDiemRequest, isTimeRequest, isTypeInvoice, diff --git a/src/components/MoneyRequestConfirmationListFooter/hooks/useFooterDerivedFlags.ts b/src/components/MoneyRequestConfirmationListFooter/hooks/useFooterDerivedFlags.ts index ffa61a75f529..72919c3565bc 100644 --- a/src/components/MoneyRequestConfirmationListFooter/hooks/useFooterDerivedFlags.ts +++ b/src/components/MoneyRequestConfirmationListFooter/hooks/useFooterDerivedFlags.ts @@ -1,6 +1,5 @@ import type {OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; -import shouldShowDistanceMap from '@components/MoneyRequestConfirmationListFooter/shouldShowDistanceMap'; import usePolicyForMovingExpenses from '@hooks/usePolicyForMovingExpenses'; import {hasEnabledTags} from '@libs/TagsOptionsListUtils'; import {getCurrency, isManagedCardTransaction, isScanRequest, shouldShowAttendees as shouldShowAttendeesTransactionUtils} from '@libs/TransactionUtils'; @@ -33,12 +32,6 @@ type UseFooterDerivedFlagsParams = { /** Whether the active transaction is a distance request */ isDistanceRequest: boolean; - /** Whether the active transaction is a manual distance request */ - isManualDistanceRequest: boolean; - - /** Whether the active transaction is an odometer-driven distance request */ - isOdometerDistanceRequest: boolean; - /** Whether the active transaction is a per-diem request */ isPerDiemRequest: boolean; @@ -61,8 +54,6 @@ function useFooterDerivedFlags({ isPolicyExpenseChat, isReadOnly, isDistanceRequest, - isManualDistanceRequest, - isOdometerDistanceRequest, isPerDiemRequest, isTimeRequest, isTypeInvoice, @@ -81,8 +72,6 @@ function useFooterDerivedFlags({ const shouldShowTags = (isPolicyExpenseChat || isUnreported || isCreatingTrackExpense) && hasEnabledTags(policyTagLists); const shouldShowAttendees = shouldShowAttendeesTransactionUtils(iouType, policy); - const shouldShowMap = shouldShowDistanceMap({transaction, isDistanceRequest, isManualDistanceRequest, isOdometerDistanceRequest, iouType, isReadOnly}); - // 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; @@ -107,7 +96,6 @@ function useFooterDerivedFlags({ isCreatingTrackExpense, shouldShowTags, shouldShowAttendees, - shouldShowMap, shouldShowDate, canModifyTaxFields, shouldShowBillable, diff --git a/src/components/MoneyRequestConfirmationListFooter/sections/DistanceMapSection.tsx b/src/components/MoneyRequestConfirmationListFooter/sections/DistanceMapSection.tsx index 5159a77405fd..9e1416943fef 100644 --- a/src/components/MoneyRequestConfirmationListFooter/sections/DistanceMapSection.tsx +++ b/src/components/MoneyRequestConfirmationListFooter/sections/DistanceMapSection.tsx @@ -9,11 +9,22 @@ import type {IOUType} from '@src/CONST'; import type {Transaction} from '@src/types/onyx'; type DistanceMapSectionProps = { + /** Active transaction (drives the rendered route + waypoint pending/error state) */ transaction: OnyxEntry; + + /** Whether the active transaction is a distance request (gate for showing the map) */ isDistanceRequest: boolean; + + /** Whether the active transaction is a manual distance request (suppresses the map) */ isManualDistanceRequest: boolean; + + /** Whether the active transaction is an odometer-driven distance request (suppresses the map) */ isOdometerDistanceRequest: boolean; + + /** Type of IOU being confirmed (splits never show the map) */ iouType: Exclude; + + /** Whether the surface is read-only (read-only without errors/pending hides the map) */ isReadOnly: boolean; }; diff --git a/src/components/MoneyRequestConfirmationListFooter/sections/InvoiceSenderSection.tsx b/src/components/MoneyRequestConfirmationListFooter/sections/InvoiceSenderSection.tsx index bf13a0d73543..7a806793ce87 100644 --- a/src/components/MoneyRequestConfirmationListFooter/sections/InvoiceSenderSection.tsx +++ b/src/components/MoneyRequestConfirmationListFooter/sections/InvoiceSenderSection.tsx @@ -7,11 +7,22 @@ import type * as OnyxTypes from '@src/types/onyx'; import type {Participant} from '@src/types/onyx/IOU'; type InvoiceSenderSectionProps = { + /** Type of IOU being confirmed (this section only renders when iouType === INVOICE) */ iouType: Exclude; + + /** ID of the report the transaction belongs to */ reportID: string; + + /** Selected participants (used to derive the sender workspace) */ selectedParticipants: Participant[]; + + /** Whether the surface is read-only */ isReadOnly: boolean; + + /** Whether the user has confirmed (locks editable controls) */ didConfirm: boolean; + + /** Active transaction */ transaction: OnyxEntry; }; diff --git a/src/components/MoneyRequestConfirmationListFooter/sections/PerDiemSection.tsx b/src/components/MoneyRequestConfirmationListFooter/sections/PerDiemSection.tsx index aab1ed6c89b7..141db1f5f56e 100644 --- a/src/components/MoneyRequestConfirmationListFooter/sections/PerDiemSection.tsx +++ b/src/components/MoneyRequestConfirmationListFooter/sections/PerDiemSection.tsx @@ -7,16 +7,37 @@ import type {IOUAction, IOUType} from '@src/CONST'; import type * as OnyxTypes from '@src/types/onyx'; type PerDiemSectionProps = { + /** Action being performed (per-diem fields are hidden on SUBMIT) */ action: IOUAction; + + /** Type of IOU being confirmed */ iouType: Exclude; + + /** Whether the active transaction is a per-diem request (gate for rendering this section) */ isPerDiemRequest: boolean; + + /** Active transaction */ transaction: OnyxEntry; + + /** ID of the report the transaction belongs to */ reportID: string; + + /** ID of the active transaction */ transactionID: string | undefined; + + /** Active policy (used to resolve the per-diem custom unit) */ policy: OnyxEntry; + + /** Whether the surface is read-only */ isReadOnly: boolean; + + /** Whether the user has confirmed (locks editable controls) */ didConfirm: boolean; + + /** Whether to display per-field validation errors */ shouldDisplayFieldError: boolean; + + /** Form-level error message */ formError: string; }; diff --git a/src/components/MoneyRequestConfirmationListFooter/sections/ReceiptSection.tsx b/src/components/MoneyRequestConfirmationListFooter/sections/ReceiptSection.tsx index c4e732057cab..1ceb128295bc 100644 --- a/src/components/MoneyRequestConfirmationListFooter/sections/ReceiptSection.tsx +++ b/src/components/MoneyRequestConfirmationListFooter/sections/ReceiptSection.tsx @@ -17,24 +17,61 @@ import ROUTES from '@src/ROUTES'; import type * as OnyxTypes from '@src/types/onyx'; type ReceiptSectionProps = { + /** Active transaction (drives receipt source resolution + scan-mode compact dimensions) */ transaction: OnyxEntry; + + /** ID of the active transaction */ transactionID: string | undefined; + + /** ID of the report the transaction belongs to */ reportID: string; + + /** Action being performed (drives the receipt-scan navigation target) */ action: IOUAction; + + /** Type of IOU being confirmed */ iouType: Exclude; + + /** Active policy (used to decide whether the receipt empty state should render) */ policy: OnyxEntry; + + /** Whether the active transaction is a per-diem request */ isPerDiemRequest: boolean; + + /** Whether the active transaction is a distance request (suppresses receipt area unless manual/odometer) */ isDistanceRequest: boolean; + + /** Whether the active transaction is a manual distance request */ isManualDistanceRequest: boolean; + + /** Whether the active transaction is an odometer-driven distance request */ isOdometerDistanceRequest: boolean; + + /** Whether the surface is read-only */ isReadOnly: boolean; + + /** Whether the receipt can be replaced */ isReceiptEditable: boolean; + + /** Whether the receipt should be displayed */ shouldDisplayReceipt: boolean; + + /** Whether the receipt is currently being stitched */ isLoadingReceipt: boolean; + + /** Path of the receipt asset (URL or local) */ receiptPath: string | number; + + /** Filename of the receipt asset */ receiptFilename: string; + + /** Whether optional fields are expanded (drives compact-mode dimensions) */ showMoreFields: boolean; + + /** Callback when the receipt PDF fails to load */ onPDFLoadError?: () => void; + + /** Callback when the receipt PDF requires a password */ onPDFPassword?: () => void; };