From af72662c45314b80df2776bccf435335a0e19fa1 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Fri, 24 Jul 2026 14:04:47 +0530 Subject: [PATCH 1/5] refactor: align expense preview card metadata line with mobile row styling Signed-off-by: krishna2323 --- .../TransactionPreviewContent.tsx | 170 ++++++------------ src/libs/TransactionPreviewUtils.ts | 34 ++-- tests/unit/TransactionPreviewUtils.test.ts | 18 +- 3 files changed, 84 insertions(+), 138 deletions(-) diff --git a/src/components/ReportActionItem/TransactionPreview/TransactionPreviewContent.tsx b/src/components/ReportActionItem/TransactionPreview/TransactionPreviewContent.tsx index 2dd423b1c878..330efc5faba4 100644 --- a/src/components/ReportActionItem/TransactionPreview/TransactionPreviewContent.tsx +++ b/src/components/ReportActionItem/TransactionPreview/TransactionPreviewContent.tsx @@ -25,7 +25,6 @@ import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; import {calculateAmount} from '@libs/IOUUtils'; import Parser from '@libs/Parser'; import {getLoginByAccountID} from '@libs/PersonalDetailsUtils'; -import {getCommaSeparatedTagNameWithSanitizedColons} from '@libs/PolicyUtils'; import {getThumbnailAndImageURIs} from '@libs/ReceiptUtils'; import {getOriginalMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils'; import {isMarkAsCashActionForTransaction} from '@libs/ReportPrimaryActionUtils'; @@ -78,7 +77,7 @@ function TransactionPreviewContent({ isReviewDuplicateTransactionPage = false, shouldHighlight = false, }: TransactionPreviewContentProps) { - const icons = useMemoizedLazyExpensifyIcons(['DotIndicator', 'Folder', 'Tag']); + const icons = useMemoizedLazyExpensifyIcons(['DotIndicator']); const theme = useTheme(); const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -89,7 +88,7 @@ function TransactionPreviewContent({ () => getTransactionDetails(transaction, undefined, policy, isParentPolicyExpenseChat) ?? {}, [transaction, policy, isParentPolicyExpenseChat], ); - const {amount, comment: requestComment, merchant, tag, category, currency: requestCurrency} = transactionDetails; + const {amount, comment: requestComment, merchant, category, currency: requestCurrency} = transactionDetails; const [originalTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${getNonEmptyStringOnyxID(transaction?.comment?.originalTransactionID)}`); const filteredViolations = filterReceiptViolations(violations); const firstViolation = filteredViolations.at(0); @@ -132,7 +131,7 @@ function TransactionPreviewContent({ [areThereDuplicates, transactionPreviewCommonArguments, isParentPolicyExpenseChat, currentUserEmail, currentUserAccountID, reportActions], ); - const {shouldShowRBR, shouldShowMerchant, shouldShowSplitShare, shouldShowTag, shouldShowCategory, shouldShowSkeleton, shouldShowDescription} = conditionals; + const {shouldShowRBR, shouldShowMerchant, shouldShowSplitShare, shouldShowCategory, shouldShowSkeleton, shouldShowDescription} = conditionals; const isIOUActionType = isMoneyRequestAction(action); const canEdit = isIOUActionType && canEditMoneyRequest(action, transaction, isChatReportArchived, report, policy); @@ -172,16 +171,12 @@ function TransactionPreviewContent({ ); const getTranslatedText = (item: TranslationPathOrText) => (item.translationPath ? translate(item.translationPath) : (item.text ?? '')); - const previewHeaderText = previewText.previewHeaderText.reduce((text, currentKey) => { - return `${text}${getTranslatedText(currentKey)}`; - }, ''); - const RBRMessage = getTranslatedText(previewText.RBRMessage); const displayAmountText = getTranslatedText(previewText.displayAmountText); const displayDeleteAmountText = getTranslatedText(previewText.displayDeleteAmountText); + const displayTypeText = getTranslatedText(previewText.previewTypeText); const isDeleted = action?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || transaction?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; - const shouldShowCategoryOrTag = shouldShowCategory || shouldShowTag; const shouldShowMerchantOrDescription = shouldShowDescription || shouldShowMerchant; const description = truncate(StringUtils.lineBreaksToSpaces(Parser.htmlToText(requestComment ?? '')), {length: CONST.REQUEST_PREVIEW.MAX_LENGTH}); @@ -194,6 +189,10 @@ function TransactionPreviewContent({ const displayAmount = isDeleted ? displayDeleteAmountText : displayAmountText; const receiptImages = [{...getThumbnailAndImageURIs(transaction), transaction}]; const merchantOrDescription = shouldShowMerchant ? requestMerchant : description || ''; + const previewSupportingText = [previewText.previewDateText, shouldShowCategory && category ? {text: getDecodedLeafCategoryName(category)} : undefined, ...previewText.previewStatusText] + .filter((item): item is TranslationPathOrText => !!item) + .map(getTranslatedText) + .join(` ${CONST.DOT_SEPARATOR} `); const participantAccountIDs = isMoneyRequestAction(action) && isBillSplit ? (getOriginalMessage(action)?.participantAccountIDs ?? []) : [managerID, ownerAccountID]; const isCardTransaction = isCardTransactionUtils(transaction); @@ -255,10 +254,6 @@ function TransactionPreviewContent({ actorAccountID, ]); - const shouldWrapDisplayAmount = !(isBillSplit || shouldShowMerchantOrDescription || isTransactionScanning); - const previewTextViewGap = (shouldShowCategoryOrTag || !shouldWrapDisplayAmount) && styles.gap2; - const previewTextMargin = shouldShowIOUHeader && shouldShowMerchantOrDescription && !isBillSplit && !shouldShowCategoryOrTag && styles.mbn1; - const animatedHighlightStyle = useAnimatedHighlightStyle({ shouldHighlight, highlightColor: theme.messageHighlightBG, @@ -314,114 +309,59 @@ function TransactionPreviewContent({ style={[styles.flex1, styles.dFlex, styles.alignItemsCenter, styles.gap2, styles.flexRow]} /> )} - - - {previewHeaderText} - {isBillSplit && ( - - - - )} - {shouldWrapDisplayAmount && ( - - {displayAmount} - - )} - - - - - {shouldShowMerchantOrDescription && ( - - {merchantOrDescription} - - )} - {!shouldWrapDisplayAmount && ( - - {displayAmount} - - )} - - - - {!!splitShare && ( - - {translate('iou.yourSplit', convertToDisplayString(splitShare, requestCurrency))} - - )} - - - {shouldShowCategoryOrTag && ( - - {shouldShowCategory && ( - + + + {shouldShowMerchantOrDescription && ( + - - - {getDecodedLeafCategoryName(category ?? '')} - - + {merchantOrDescription} + )} - {shouldShowTag && !!tag && ( - - + - - {getCommaSeparatedTagNameWithSanitizedColons(tag)} - )} + + {displayAmount} + + + + + {previewSupportingText} + + + {displayTypeText} + + + {!!splitShare && ( + + + {translate('iou.yourSplit', convertToDisplayString(splitShare, requestCurrency))} + + )} {!isIOUSettled && shouldShowRBR && ( diff --git a/src/libs/TransactionPreviewUtils.ts b/src/libs/TransactionPreviewUtils.ts index 2ccea5a90e62..d781b0e72b84 100644 --- a/src/libs/TransactionPreviewUtils.ts +++ b/src/libs/TransactionPreviewUtils.ts @@ -123,8 +123,6 @@ type TranslationPathOrText = { text?: string; }; -const dotSeparator: TranslationPathOrText = {text: ` ${CONST.DOT_SEPARATOR} `}; - /** * Normalize the last four digits to always return 4 characters. * If the number is shorter than 4 digits, it will be padded with X's. @@ -303,12 +301,12 @@ function getTransactionPreviewTextAndTranslationPaths({ } } - let previewHeaderText: TranslationPathOrText[] = [{translationPath: getExpenseTypeTranslationKey(getTransactionType(transaction))}]; + let previewTypeText: TranslationPathOrText = {translationPath: getExpenseTypeTranslationKey(getTransactionType(transaction))}; if (isTransactionScanning) { - previewHeaderText = [{translationPath: 'common.receipt'}]; + previewTypeText = {translationPath: 'common.receipt'}; } else if (isBillSplit) { - previewHeaderText = [{translationPath: 'iou.split'}]; + previewTypeText = {translationPath: 'iou.split'}; } if (RBRMessage?.text === CONST.ERROR.BANK_ACCOUNT_SAME_DEPOSIT_AND_WITHDRAWAL_ERROR) { @@ -317,36 +315,42 @@ function getTransactionPreviewTextAndTranslationPaths({ RBRMessage ??= {text: ''}; + let previewDateText: TranslationPathOrText | undefined; if (!isCreatedMissing(transaction)) { const created = getFormattedCreated(transaction); const date = DateUtils.formatWithUTCTimeZone(created, DateUtils.doesDateBelongToAPastYear(created) ? CONST.DATE.MONTH_DAY_YEAR_ABBR_FORMAT : CONST.DATE.MONTH_DAY_ABBR_FORMAT); - previewHeaderText.unshift({text: date}, dotSeparator); + previewDateText = {text: date}; } + const previewStatusText: TranslationPathOrText[] = []; + const addPreviewStatusText = (statusText: TranslationPathOrText) => { + previewStatusText.push(statusText); + }; + if (isPending(transaction)) { - previewHeaderText.push(dotSeparator, {translationPath: 'iou.pending'}); + previewTypeText = {translationPath: 'iou.pending'}; } if (hasPendingRTERViolation(violations)) { - previewHeaderText.push(dotSeparator, {translationPath: 'iou.pendingMatch'}); + addPreviewStatusText({translationPath: 'iou.pendingMatch'}); } let isPreviewHeaderTextComplete = false; if (isMoneyRequestSettled && !iouReport?.isCancelledIOU && !isPartialHold && !hasActionWithErrors) { - previewHeaderText.push(dotSeparator, {translationPath: isTransactionMadeWithCard ? 'common.done' : 'iou.settledExpensify'}); + addPreviewStatusText({translationPath: isTransactionMadeWithCard ? 'common.done' : 'iou.settledExpensify'}); isPreviewHeaderTextComplete = true; } if (!isPreviewHeaderTextComplete) { if (hasViolationsOfTypeNotice && transaction && !isReportApproved({report: iouReport}) && !isSettled(iouReport?.reportID)) { - previewHeaderText.push(dotSeparator, {translationPath: 'violations.reviewRequired'}); + addPreviewStatusText({translationPath: 'violations.reviewRequired'}); } else if (isExpenseReport(iouReport) && isGroupPolicyUtil(policy) && isReportApproved({report: iouReport}) && !isSettled(iouReport?.reportID) && !isPartialHold) { - previewHeaderText.push(dotSeparator, {translationPath: 'iou.approved'}); + addPreviewStatusText({translationPath: 'iou.approved'}); } else if (iouReport?.isCancelledIOU) { - previewHeaderText.push(dotSeparator, {translationPath: 'iou.canceled'}); + addPreviewStatusText({translationPath: 'iou.canceled'}); } else if (shouldShowHoldMessage) { - previewHeaderText.push(dotSeparator, {translationPath: 'violations.hold'}); + addPreviewStatusText({translationPath: 'violations.hold'}); } } @@ -363,7 +367,9 @@ function getTransactionPreviewTextAndTranslationPaths({ RBRMessage, displayAmountText, displayDeleteAmountText, - previewHeaderText, + previewDateText, + previewStatusText, + previewTypeText, }; } diff --git a/tests/unit/TransactionPreviewUtils.test.ts b/tests/unit/TransactionPreviewUtils.test.ts index 732cd46d34b8..fe1840639822 100644 --- a/tests/unit/TransactionPreviewUtils.test.ts +++ b/tests/unit/TransactionPreviewUtils.test.ts @@ -113,7 +113,7 @@ describe('TransactionPreviewUtils', () => { const functionArgs = {...basicProps, iouReport: undefined, transaction: undefined, originalTransaction: undefined}; const result = getTransactionPreviewTextAndTranslationPaths(functionArgs); expect(result.RBRMessage.text).toEqual(''); - expect(result.previewHeaderText).toContainEqual({translationPath: 'iou.cash'}); + expect(result.previewTypeText).toEqual({translationPath: 'iou.cash'}); expect(result.displayAmountText.text).toEqual('$0.00'); }); @@ -148,7 +148,7 @@ describe('TransactionPreviewUtils', () => { expect(result.RBRMessage.translationPath).toEqual('iou.missingAmount'); }); - it('should display showCashOrCard in previewHeaderText', () => { + it('should display cash or card as the preview type', () => { const functionArgsWithCardTransaction = { ...basicProps, transaction: { @@ -160,14 +160,14 @@ describe('TransactionPreviewUtils', () => { const cardTransaction = getTransactionPreviewTextAndTranslationPaths(functionArgsWithCardTransaction); const cashTransaction = getTransactionPreviewTextAndTranslationPaths({...basicProps}); - expect(cardTransaction.previewHeaderText).toEqual(expect.arrayContaining([{translationPath: 'common.card'}])); - expect(cashTransaction.previewHeaderText).toEqual(expect.arrayContaining([{translationPath: 'iou.cash'}])); + expect(cardTransaction.previewTypeText).toEqual({translationPath: 'common.card'}); + expect(cashTransaction.previewTypeText).toEqual({translationPath: 'iou.cash'}); }); it('displays appropriate header text if the transaction is bill split', () => { const functionArgs = {...basicProps, isBillSplit: true, originalTransaction: undefined}; const result = getTransactionPreviewTextAndTranslationPaths(functionArgs); - expect(result.previewHeaderText).toEqual(expect.arrayContaining([{translationPath: 'iou.split'}])); + expect(result.previewTypeText).toEqual({translationPath: 'iou.split'}); }); it('displays description when receipt is being scanned', () => { @@ -178,13 +178,13 @@ describe('TransactionPreviewUtils', () => { merchant: 'Expense', }; const result = getTransactionPreviewTextAndTranslationPaths(functionArgs); - expect(result.previewHeaderText).toEqual(expect.arrayContaining([{translationPath: 'common.receipt'}])); + expect(result.previewTypeText).toEqual({translationPath: 'common.receipt'}); }); it('should apply correct text when transaction is pending and not a bill split', () => { const functionArgs = {...basicProps, transaction: {...basicProps.transaction, status: CONST.TRANSACTION.STATUS.PENDING}, originalTransaction: undefined}; const result = getTransactionPreviewTextAndTranslationPaths(functionArgs); - expect(result.previewHeaderText).toEqual(expect.arrayContaining([{translationPath: 'iou.pending'}])); + expect(result.previewTypeText).toEqual({translationPath: 'iou.pending'}); }); it('handles currency and amount display during scanning correctly', () => { @@ -228,7 +228,7 @@ describe('TransactionPreviewUtils', () => { it('shows approved message when the iouReport is canceled', () => { const functionArgs = {...basicProps, iouReport: {...basicProps.iouReport, isCancelledIOU: true}, originalTransaction: undefined}; const result = getTransactionPreviewTextAndTranslationPaths(functionArgs); - expect(result.previewHeaderText).toContainEqual({translationPath: 'iou.canceled'}); + expect(result.previewStatusText).toContainEqual({translationPath: 'iou.canceled'}); }); it('should include "Approved" in the preview when the report is approved, regardless of whether RBR is shown', () => { @@ -246,7 +246,7 @@ describe('TransactionPreviewUtils', () => { }; const result = getTransactionPreviewTextAndTranslationPaths(functionArgs); - expect(result.previewHeaderText).toContainEqual({translationPath: 'iou.approved'}); + expect(result.previewStatusText).toContainEqual({translationPath: 'iou.approved'}); }); it('should display the correct amount for a bill split transaction', () => { From 9be8da5bd598874098868104b712748f4e0a0041 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Wed, 29 Jul 2026 20:18:04 +0530 Subject: [PATCH 2/5] test: update MoneyRequestReportPreview assertions for split metadata line Signed-off-by: krishna2323 --- tests/ui/MoneyRequestReportPreview.test.tsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/tests/ui/MoneyRequestReportPreview.test.tsx b/tests/ui/MoneyRequestReportPreview.test.tsx index faab3f85eb5d..eca807671e13 100644 --- a/tests/ui/MoneyRequestReportPreview.test.tsx +++ b/tests/ui/MoneyRequestReportPreview.test.tsx @@ -176,14 +176,15 @@ const renderPage = ({isWhisper = false, isHovered = false}: Partial { +const getTransactionDisplayAmountAndMetadataText = (transaction: Transaction) => { const created = getFormattedCreated(transaction); const date = DateUtils.formatWithUTCTimeZone(created, DateUtils.doesDateBelongToAPastYear(created) ? CONST.DATE.MONTH_DAY_YEAR_ABBR_FORMAT : CONST.DATE.MONTH_DAY_ABBR_FORMAT); const isTransactionMadeWithCard = isManagedCardTransaction(transaction); - const cashOrCard = isTransactionMadeWithCard ? TestHelper.translateLocal('iou.card') : TestHelper.translateLocal('iou.cash'); - const transactionHeaderText = `${date} ${CONST.DOT_SEPARATOR} ${cashOrCard}`; + // The date leads the supporting line, which can also carry the category and the report status. + const transactionSupportingText = new RegExp(`^${date}`); + const transactionTypeText = isTransactionMadeWithCard ? TestHelper.translateLocal('iou.card') : TestHelper.translateLocal('iou.cash'); const transactionDisplayAmount = TestHelper.convertToDisplayString(-transaction.amount, transaction.currency); - return {transactionHeaderText, transactionDisplayAmount}; + return {transactionSupportingText, transactionTypeText, transactionDisplayAmount}; }; const setCurrentWidth = () => { @@ -273,10 +274,11 @@ describe('MoneyRequestReportPreview', () => { expect(screen.getByText(getReportName(mockIOUReport))).toBeOnTheScreen(); for (const transaction of arrayOfTransactions) { - const {transactionDisplayAmount, transactionHeaderText} = getTransactionDisplayAmountAndHeaderText(transaction); + const {transactionDisplayAmount, transactionSupportingText, transactionTypeText} = getTransactionDisplayAmountAndMetadataText(transaction); expect(screen.getAllByText(transactionDisplayAmount).length).toBeGreaterThan(0); - expect(screen.getAllByText(transactionHeaderText)).toHaveLength(arrayOfTransactions.length); + expect(screen.getAllByText(transactionSupportingText)).toHaveLength(arrayOfTransactions.length); + expect(screen.getAllByText(transactionTypeText)).toHaveLength(arrayOfTransactions.length); expect(screen.getAllByText(transaction.merchant)).toHaveLength(arrayOfTransactions.length); } }); From 2c014466f05b95e824673c095d3f8145beda928d Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Tue, 4 Aug 2026 23:05:46 +0530 Subject: [PATCH 3/5] refactor: keep only transaction statuses on the expense preview supporting line Signed-off-by: krishna2323 --- .../ReportPreviewHeader.tsx | 8 +++- .../TransactionPreviewContent.tsx | 4 +- src/libs/TransactionPreviewUtils.ts | 41 +++---------------- tests/unit/TransactionPreviewUtils.test.ts | 12 +++--- 4 files changed, 21 insertions(+), 44 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestReportPreview/ReportPreviewHeader.tsx b/src/components/ReportActionItem/MoneyRequestReportPreview/ReportPreviewHeader.tsx index 1776bc0e0644..298e56e813fd 100644 --- a/src/components/ReportActionItem/MoneyRequestReportPreview/ReportPreviewHeader.tsx +++ b/src/components/ReportActionItem/MoneyRequestReportPreview/ReportPreviewHeader.tsx @@ -67,6 +67,12 @@ function ReportPreviewHeader() { [translate, numberOfRequests], ); + // A cancelled payment is a report level event and it isn't surfaced by the status badge, so we show it next to the expense count. + const supportingText = useMemo( + () => (iouReport?.isCancelledIOU ? `${translate('iou.canceled')} ${CONST.DOT_SEPARATOR} ${expenseCount}` : expenseCount), + [iouReport?.isCancelledIOU, translate, expenseCount], + ); + const reportStateNum = iouReport?.stateNum ?? action?.childStateNum; const reportStatusNum = iouReport?.statusNum ?? action?.childStatusNum; @@ -123,7 +129,7 @@ function ReportPreviewHeader() { tooltipText={reportStatusTooltip} /> )} - {!shouldShowAccessPlaceHolder && {expenseCount}} + {!shouldShowAccessPlaceHolder && {supportingText}} ) )} diff --git a/src/components/ReportActionItem/TransactionPreview/TransactionPreviewContent.tsx b/src/components/ReportActionItem/TransactionPreview/TransactionPreviewContent.tsx index e56cd1c5dfae..9df824922c5c 100644 --- a/src/components/ReportActionItem/TransactionPreview/TransactionPreviewContent.tsx +++ b/src/components/ReportActionItem/TransactionPreview/TransactionPreviewContent.tsx @@ -162,12 +162,10 @@ function TransactionPreviewContent({ shouldShowRBR, violationMessage, reportActions, - currentUserEmail, - currentUserAccountID, originalTransaction, convertToDisplayString, }), - [transactionPreviewCommonArguments, shouldShowRBR, violationMessage, reportActions, currentUserEmail, currentUserAccountID, originalTransaction, convertToDisplayString], + [transactionPreviewCommonArguments, shouldShowRBR, violationMessage, reportActions, originalTransaction, convertToDisplayString], ); const getTranslatedText = (item: TranslationPathOrText) => (item.translationPath ? translate(item.translationPath) : (item.text ?? '')); diff --git a/src/libs/TransactionPreviewUtils.ts b/src/libs/TransactionPreviewUtils.ts index d4a36907dd77..4c697af17987 100644 --- a/src/libs/TransactionPreviewUtils.ts +++ b/src/libs/TransactionPreviewUtils.ts @@ -36,7 +36,6 @@ import { isCreatedMissing, isDistanceRequest, isFetchingWaypointsFromServer, - isManagedCardTransaction, isMerchantMissing, isOnHold, isPending, @@ -196,7 +195,6 @@ function getUniqueActionErrorsForTransaction(reportActions: OnyxTypes.ReportActi function getTransactionPreviewTextAndTranslationPaths({ iouReport, - iouReportOwnerLogin, policy, transaction, action, @@ -206,13 +204,10 @@ function getTransactionPreviewTextAndTranslationPaths({ shouldShowRBR, violationMessage, reportActions, - currentUserEmail, - currentUserAccountID, originalTransaction, convertToDisplayString, }: { iouReport: OnyxEntry; - iouReportOwnerLogin: string | undefined; policy: OnyxEntry; transaction: OnyxEntry; action: OnyxEntry; @@ -222,17 +217,13 @@ function getTransactionPreviewTextAndTranslationPaths({ shouldShowRBR: boolean; violationMessage?: string; reportActions?: OnyxTypes.ReportActions; - currentUserEmail: string; - currentUserAccountID: number; originalTransaction?: OnyxEntry; convertToDisplayString: CurrencyListActionsContextType['convertToDisplayString']; }) { const isFetchingWaypoints = isFetchingWaypointsFromServer(transaction); const isTransactionOnHold = isOnHold(transaction); - const isTransactionMadeWithCard = isManagedCardTransaction(transaction); const isMoneyRequestSettled = isSettled(iouReport?.reportID); const isSettlementOrApprovalPartial = !!iouReport?.pendingFields?.partial; - const isPartialHold = isSettlementOrApprovalPartial && isTransactionOnHold; // We don't use isOnHold because it's true for duplicated transaction too and we only want to show hold message if the transaction is truly on hold const shouldShowHoldMessage = !(isMoneyRequestSettled && !isSettlementOrApprovalPartial) && !!transaction?.comment?.hold; @@ -240,8 +231,6 @@ function getTransactionPreviewTextAndTranslationPaths({ const hasFieldErrors = hasMissingSmartscanFields(transaction, iouReport); const isGroupPolicy = isGroupPolicyUtil(policy); - const hasViolationsOfTypeNotice = - hasNoticeTypeViolation(transaction, violations, currentUserEmail ?? '', currentUserAccountID, iouReport, iouReportOwnerLogin, policy, true) && isGroupPolicy; const hasActionWithErrors = hasActionWithErrorsForTransaction(iouReport?.reportID, transaction, reportActions); const {amount: requestAmount, currency: requestCurrency} = transactionDetails; @@ -323,36 +312,18 @@ function getTransactionPreviewTextAndTranslationPaths({ previewDateText = {text: date}; } + // Report level statuses (Paid, Approved, Review required, Canceled) are intentionally omitted here because they are + // already surfaced by the report status badge and the violation row. Only transaction level statuses belong on this line. const previewStatusText: TranslationPathOrText[] = []; - const addPreviewStatusText = (statusText: TranslationPathOrText) => { - previewStatusText.push(statusText); - }; if (isPending(transaction)) { - previewTypeText = {translationPath: 'iou.pending'}; + previewStatusText.push({translationPath: 'iou.pending'}); } if (hasPendingRTERViolation(violations)) { - addPreviewStatusText({translationPath: 'iou.pendingMatch'}); - } - - let isPreviewHeaderTextComplete = false; - - if (isMoneyRequestSettled && !iouReport?.isCancelledIOU && !isPartialHold && !hasActionWithErrors) { - addPreviewStatusText({translationPath: isTransactionMadeWithCard ? 'common.done' : 'iou.settledExpensify'}); - isPreviewHeaderTextComplete = true; - } - - if (!isPreviewHeaderTextComplete) { - if (hasViolationsOfTypeNotice && transaction && !isReportApproved({report: iouReport}) && !isSettled(iouReport?.reportID)) { - addPreviewStatusText({translationPath: 'violations.reviewRequired'}); - } else if (isExpenseReport(iouReport) && isGroupPolicyUtil(policy) && isReportApproved({report: iouReport}) && !isSettled(iouReport?.reportID) && !isPartialHold) { - addPreviewStatusText({translationPath: 'iou.approved'}); - } else if (iouReport?.isCancelledIOU) { - addPreviewStatusText({translationPath: 'iou.canceled'}); - } else if (shouldShowHoldMessage) { - addPreviewStatusText({translationPath: 'violations.hold'}); - } + previewStatusText.push({translationPath: 'iou.pendingMatch'}); + } else if (shouldShowHoldMessage) { + previewStatusText.push({translationPath: 'violations.hold'}); } const amount = isBillSplit ? getAmount(originalTransaction ?? transaction) : requestAmount; diff --git a/tests/unit/TransactionPreviewUtils.test.ts b/tests/unit/TransactionPreviewUtils.test.ts index 7f32405476b6..800f59bf74e4 100644 --- a/tests/unit/TransactionPreviewUtils.test.ts +++ b/tests/unit/TransactionPreviewUtils.test.ts @@ -185,7 +185,9 @@ describe('TransactionPreviewUtils', () => { it('should apply correct text when transaction is pending and not a bill split', () => { const functionArgs = {...basicProps, transaction: {...basicProps.transaction, status: CONST.TRANSACTION.STATUS.PENDING}, originalTransaction: undefined}; const result = getTransactionPreviewTextAndTranslationPaths(functionArgs); - expect(result.previewTypeText).toEqual({translationPath: 'iou.pending'}); + // Pending is a transaction status, so it belongs to the supporting line and must not replace the expense type. + expect(result.previewStatusText).toContainEqual({translationPath: 'iou.pending'}); + expect(result.previewTypeText).toEqual({translationPath: 'iou.cash'}); }); it('handles currency and amount display during scanning correctly', () => { @@ -226,13 +228,13 @@ describe('TransactionPreviewUtils', () => { expect(result.displayAmountText.text).toEqual(convertAmountToDisplayString(modifiedAmount, currency)); }); - it('shows approved message when the iouReport is canceled', () => { + it('does not show the canceled status when the iouReport is canceled, because it is shown at the report level instead', () => { const functionArgs = {...basicProps, iouReport: {...basicProps.iouReport, isCancelledIOU: true}, originalTransaction: undefined}; const result = getTransactionPreviewTextAndTranslationPaths(functionArgs); - expect(result.previewStatusText).toContainEqual({translationPath: 'iou.canceled'}); + expect(result.previewStatusText).toEqual([]); }); - it('should include "Approved" in the preview when the report is approved, regardless of whether RBR is shown', () => { + it('does not show the approved status when the report is approved, because it is redundant with the report status badge', () => { const functionArgs = { ...basicProps, iouReport: { @@ -247,7 +249,7 @@ describe('TransactionPreviewUtils', () => { }; const result = getTransactionPreviewTextAndTranslationPaths(functionArgs); - expect(result.previewStatusText).toContainEqual({translationPath: 'iou.approved'}); + expect(result.previewStatusText).toEqual([]); }); it('should display the correct amount for a bill split transaction', () => { From 71f0ce97a8ff1648242cbb46728f7de4a389f123 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Wed, 5 Aug 2026 18:32:59 +0530 Subject: [PATCH 4/5] fix: keep the canceled status on previews without a report status badge Signed-off-by: krishna2323 --- .../TransactionPreview/TransactionPreviewContent.tsx | 10 ++++++++-- src/libs/TransactionPreviewUtils.ts | 12 +++++++++--- tests/unit/TransactionPreviewUtils.test.ts | 11 +++++++++-- 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/components/ReportActionItem/TransactionPreview/TransactionPreviewContent.tsx b/src/components/ReportActionItem/TransactionPreview/TransactionPreviewContent.tsx index 3c8aacaf7522..a24cf1379a06 100644 --- a/src/components/ReportActionItem/TransactionPreview/TransactionPreviewContent.tsx +++ b/src/components/ReportActionItem/TransactionPreview/TransactionPreviewContent.tsx @@ -2,6 +2,7 @@ import Button from '@components/Button'; import Icon from '@components/Icon'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import ReportActionAvatars from '@components/ReportActionAvatars'; +import {ReportPreviewDataContext} from '@components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContext'; import ReportActionItemImages from '@components/ReportActionItem/ReportActionItemImages'; import UserInfoCellsWithArrow from '@components/Search/SearchList/ListItem/UserInfoCellsWithArrow'; import Text from '@components/Text'; @@ -46,7 +47,7 @@ import {cardByIdSelector} from '@src/selectors/Card'; import {getStableReportSelector} from '@src/selectors/Report'; import truncate from 'lodash/truncate'; -import React, {useMemo} from 'react'; +import React, {useContext, useMemo} from 'react'; import {View} from 'react-native'; import Animated from 'react-native-reanimated'; @@ -133,6 +134,10 @@ function TransactionPreviewContent({ const {shouldShowRBR, shouldShowMerchant, shouldShowSplitShare, shouldShowCategory, shouldShowSkeleton, shouldShowDescription} = conditionals; + // Inside a report preview the header already reports a cancelled payment next to the expense count. Standalone previews + // (an expense report with a single expense, a self-DM, a split) have no such header, so they have to report it themselves. + const shouldShowCanceledStatus = !useContext(ReportPreviewDataContext); + const isIOUActionType = isMoneyRequestAction(action); const canEdit = isIOUActionType && canEditMoneyRequest(action, transaction, isChatReportArchived, report, policy); const companyCardPageURL = `${environmentURL}/${ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(report?.policyID)}`; @@ -160,12 +165,13 @@ function TransactionPreviewContent({ getTransactionPreviewTextAndTranslationPaths({ ...transactionPreviewCommonArguments, shouldShowRBR, + shouldShowCanceledStatus, violationMessage, reportActions, originalTransaction, convertToDisplayString, }), - [transactionPreviewCommonArguments, shouldShowRBR, violationMessage, reportActions, originalTransaction, convertToDisplayString], + [transactionPreviewCommonArguments, shouldShowRBR, shouldShowCanceledStatus, violationMessage, reportActions, originalTransaction, convertToDisplayString], ); const getTranslatedText = (item: TranslationPathOrText) => (item.translationPath ? translate(item.translationPath) : (item.text ?? '')); diff --git a/src/libs/TransactionPreviewUtils.ts b/src/libs/TransactionPreviewUtils.ts index 4c697af17987..1a217531749b 100644 --- a/src/libs/TransactionPreviewUtils.ts +++ b/src/libs/TransactionPreviewUtils.ts @@ -202,6 +202,7 @@ function getTransactionPreviewTextAndTranslationPaths({ transactionDetails, isBillSplit, shouldShowRBR, + shouldShowCanceledStatus, violationMessage, reportActions, originalTransaction, @@ -215,6 +216,8 @@ function getTransactionPreviewTextAndTranslationPaths({ transactionDetails: Partial; isBillSplit: boolean; shouldShowRBR: boolean; + /** Whether a cancelled payment has to be reported on this line, because the enclosing surface doesn't show it anywhere else */ + shouldShowCanceledStatus: boolean; violationMessage?: string; reportActions?: OnyxTypes.ReportActions; originalTransaction?: OnyxEntry; @@ -312,15 +315,18 @@ function getTransactionPreviewTextAndTranslationPaths({ previewDateText = {text: date}; } - // Report level statuses (Paid, Approved, Review required, Canceled) are intentionally omitted here because they are - // already surfaced by the report status badge and the violation row. Only transaction level statuses belong on this line. + // Paid, Approved and Review required are intentionally omitted here because the report status badge and the violation + // row already show them, so repeating them on this line is noise. Canceled is the exception: it can't be derived from + // stateNum/statusNum, so surfaces without their own report status badge have to report it here. const previewStatusText: TranslationPathOrText[] = []; if (isPending(transaction)) { previewStatusText.push({translationPath: 'iou.pending'}); } - if (hasPendingRTERViolation(violations)) { + if (shouldShowCanceledStatus && iouReport?.isCancelledIOU) { + previewStatusText.push({translationPath: 'iou.canceled'}); + } else if (hasPendingRTERViolation(violations)) { previewStatusText.push({translationPath: 'iou.pendingMatch'}); } else if (shouldShowHoldMessage) { previewStatusText.push({translationPath: 'violations.hold'}); diff --git a/tests/unit/TransactionPreviewUtils.test.ts b/tests/unit/TransactionPreviewUtils.test.ts index 800f59bf74e4..52c6bf8fba94 100644 --- a/tests/unit/TransactionPreviewUtils.test.ts +++ b/tests/unit/TransactionPreviewUtils.test.ts @@ -51,6 +51,7 @@ const basicProps = { transactionDetails: {}, isBillSplit: false, shouldShowRBR: false, + shouldShowCanceledStatus: false, isReportAPolicyExpenseChat: false, areThereDuplicates: false, currentUserEmail: '', @@ -228,12 +229,18 @@ describe('TransactionPreviewUtils', () => { expect(result.displayAmountText.text).toEqual(convertAmountToDisplayString(modifiedAmount, currency)); }); - it('does not show the canceled status when the iouReport is canceled, because it is shown at the report level instead', () => { - const functionArgs = {...basicProps, iouReport: {...basicProps.iouReport, isCancelledIOU: true}, originalTransaction: undefined}; + it('does not show the canceled status inside a report preview, because the preview header already shows it', () => { + const functionArgs = {...basicProps, iouReport: {...basicProps.iouReport, isCancelledIOU: true}, originalTransaction: undefined, shouldShowCanceledStatus: false}; const result = getTransactionPreviewTextAndTranslationPaths(functionArgs); expect(result.previewStatusText).toEqual([]); }); + it('shows the canceled status in a standalone preview, because nothing else on that surface reports it', () => { + const functionArgs = {...basicProps, iouReport: {...basicProps.iouReport, isCancelledIOU: true}, originalTransaction: undefined, shouldShowCanceledStatus: true}; + const result = getTransactionPreviewTextAndTranslationPaths(functionArgs); + expect(result.previewStatusText).toContainEqual({translationPath: 'iou.canceled'}); + }); + it('does not show the approved status when the report is approved, because it is redundant with the report status badge', () => { const functionArgs = { ...basicProps, From 0d81363e791435fdb82617fca39cabb5e87501a0 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Thu, 6 Aug 2026 03:29:19 +0530 Subject: [PATCH 5/5] address review comments. Signed-off-by: krishna2323 --- .../MoneyRequestReportPreview/ReportPreviewHeader.tsx | 5 +---- .../TransactionPreview/TransactionPreviewContent.tsx | 7 ++++--- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestReportPreview/ReportPreviewHeader.tsx b/src/components/ReportActionItem/MoneyRequestReportPreview/ReportPreviewHeader.tsx index 298e56e813fd..d938720f4996 100644 --- a/src/components/ReportActionItem/MoneyRequestReportPreview/ReportPreviewHeader.tsx +++ b/src/components/ReportActionItem/MoneyRequestReportPreview/ReportPreviewHeader.tsx @@ -68,10 +68,7 @@ function ReportPreviewHeader() { ); // A cancelled payment is a report level event and it isn't surfaced by the status badge, so we show it next to the expense count. - const supportingText = useMemo( - () => (iouReport?.isCancelledIOU ? `${translate('iou.canceled')} ${CONST.DOT_SEPARATOR} ${expenseCount}` : expenseCount), - [iouReport?.isCancelledIOU, translate, expenseCount], - ); + const supportingText = iouReport?.isCancelledIOU ? `${translate('iou.canceled')} ${CONST.DOT_SEPARATOR} ${expenseCount}` : expenseCount; const reportStateNum = iouReport?.stateNum ?? action?.childStateNum; const reportStatusNum = iouReport?.statusNum ?? action?.childStatusNum; diff --git a/src/components/ReportActionItem/TransactionPreview/TransactionPreviewContent.tsx b/src/components/ReportActionItem/TransactionPreview/TransactionPreviewContent.tsx index a24cf1379a06..8dd40722559b 100644 --- a/src/components/ReportActionItem/TransactionPreview/TransactionPreviewContent.tsx +++ b/src/components/ReportActionItem/TransactionPreview/TransactionPreviewContent.tsx @@ -134,9 +134,10 @@ function TransactionPreviewContent({ const {shouldShowRBR, shouldShowMerchant, shouldShowSplitShare, shouldShowCategory, shouldShowSkeleton, shouldShowDescription} = conditionals; - // Inside a report preview the header already reports a cancelled payment next to the expense count. Standalone previews - // (an expense report with a single expense, a self-DM, a split) have no such header, so they have to report it themselves. - const shouldShowCanceledStatus = !useContext(ReportPreviewDataContext); + // Raw useContext (not the useReportPreviewData slice hook, which throws when absent): a missing provider means this is a + // standalone preview with no report header to carry the status, so the preview has to report a cancelled payment itself. + const isInsideReportPreview = !!useContext(ReportPreviewDataContext); + const shouldShowCanceledStatus = !isInsideReportPreview; const isIOUActionType = isMoneyRequestAction(action); const canEdit = isIOUActionType && canEditMoneyRequest(action, transaction, isChatReportArchived, report, policy);