diff --git a/src/components/MoneyRequestConfirmationList/hooks/useConfirmationAmount.ts b/src/components/MoneyRequestConfirmationList/hooks/useConfirmationAmount.ts index 76093d54cdf2..42a68e906c83 100644 --- a/src/components/MoneyRequestConfirmationList/hooks/useConfirmationAmount.ts +++ b/src/components/MoneyRequestConfirmationList/hooks/useConfirmationAmount.ts @@ -3,7 +3,7 @@ import useLocalize from '@hooks/useLocalize'; import {computePerDiemExpenseAmount} from '@libs/actions/IOU/PerDiem'; import type {getAttendees} from '@libs/TransactionUtils'; -import {isScanning, isScanRequest as isScanRequestUtil} from '@libs/TransactionUtils'; +import {isFailedScanAmountPlaceholder, isScanning, isScanRequest as isScanRequestUtil} from '@libs/TransactionUtils'; import type * as OnyxTypes from '@src/types/onyx'; @@ -96,6 +96,8 @@ function useConfirmationAmount({ formattedAmount = ''; } else if (isScanning(transaction)) { formattedAmount = translate('iou.receiptStatusTitle'); + } else if (isFailedScanAmountPlaceholder(transaction)) { + formattedAmount = ''; } const attendeeCount = iouAttendees?.length && iouAttendees.length > 0 ? iouAttendees.length : 1; diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index ca5415e11225..5aea106f1835 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -117,6 +117,7 @@ import { isDistanceRequest as isDistanceRequestTransactionUtils, isDistanceTypeRequest, isExpenseUnreported as isExpenseUnreportedTransactionUtils, + isFailedScanAmountPlaceholder, isGPSDistanceRequest as isGPSDistanceRequestTransactionUtils, isManagedCardTransaction as isManagedCardTransactionTransactionUtils, isManualDistanceRequest as isManualDistanceRequestTransactionUtils, @@ -331,6 +332,7 @@ function MoneyRequestView({ const isOdometerDistanceRequest = isOdometerDistanceRequestTransactionUtils(transaction); const isMapDistanceRequest = isMapDistanceRequestTransactionUtils(transaction) || isDistanceTypeRequest(transaction); const isTransactionScanning = isScanning(updatedTransaction ?? transaction); + const hasFailedScanAmountPlaceholder = isFailedScanAmountPlaceholder(updatedTransaction ?? transaction); const hasRoute = hasRouteTransactionUtils(transactionBackup ?? transaction, isDistanceRequest); const rawActualAttendees = isFromMergeTransaction && updatedTransaction ? updatedTransaction.comment?.attendees : transactionAttendees; @@ -628,6 +630,8 @@ function MoneyRequestView({ if (isTransactionScanning) { merchantTitle = translate('iou.receiptStatusTitle'); amountTitle = translate('iou.receiptStatusTitle'); + } else if (hasFailedScanAmountPlaceholder) { + amountTitle = ''; } const updatedTransactionDescription = getDescription(updatedTransaction) || undefined; @@ -790,6 +794,10 @@ function MoneyRequestView({ isError: transactionDate === '', translationPath: canEditDate ? 'common.error.enterDate' : 'common.error.missingDate', }, + amount: { + isError: !isSettled && !isCancelled && hasFailedScanAmountPlaceholder, + translationPath: canEditAmount ? 'common.error.enterAmount' : 'common.error.missingAmount', + }, }; const {isError, translationPath} = fieldChecks[field] ?? {}; diff --git a/src/components/TransactionItemRow/DataCells/TotalCell.tsx b/src/components/TransactionItemRow/DataCells/TotalCell.tsx index f396e9641ad8..908e175d80fd 100644 --- a/src/components/TransactionItemRow/DataCells/TotalCell.tsx +++ b/src/components/TransactionItemRow/DataCells/TotalCell.tsx @@ -14,7 +14,14 @@ import {formatToParts} from '@libs/NumberFormatUtils'; import {parseFloatAnyLocale, roundToTwoDecimalPlaces} from '@libs/NumberUtils'; import {isGroupPolicy} from '@libs/PolicyUtils'; import {isExpenseReport, isInvoiceReport, shouldEnableNegative} from '@libs/ReportUtils'; -import {getAmount as getTransactionAmount, getCurrency as getTransactionCurrency, isDeletedTransaction, isExpenseUnreported, isScanning} from '@libs/TransactionUtils'; +import { + getAmount as getTransactionAmount, + getCurrency as getTransactionCurrency, + isDeletedTransaction, + isExpenseUnreported, + isFailedScanAmountPlaceholder, + isScanning, +} from '@libs/TransactionUtils'; import CONST from '@src/CONST'; import type {Policy, Report} from '@src/types/onyx'; @@ -54,9 +61,12 @@ function TotalCell({shouldShowTooltip, transactionItem, canEdit, onSave, report, const isDeleted = isDeletedTransaction(transactionItem); const isFromExpenseReport = (!isEmptyObject(effectiveReport) && isExpenseReport(effectiveReport)) || isGroupPolicy(effectivePolicy); const amount = getTransactionAmount(transactionItem, isFromExpenseReport, transactionItem.reportID === CONST.REPORT.UNREPORTED_REPORT_ID, isDeleted); + const hasFailedScanAmountPlaceholder = isFailedScanAmountPlaceholder(transactionItem); let amountToDisplay = convertToDisplayString(amount, currency); if (isScanning(transactionItem)) { amountToDisplay = translate('iou.receiptStatusTitle'); + } else if (hasFailedScanAmountPlaceholder) { + amountToDisplay = ''; } const iouType = getTransactionItemIouType({...transactionItem, report: effectiveReport}); @@ -67,6 +77,9 @@ function TotalCell({shouldShowTooltip, transactionItem, canEdit, onSave, report, const absoluteAmount = Math.abs(amount ?? 0); const isOriginalAmountNegative = (amount ?? 0) < 0; const [isNegative, setIsNegative] = useState(isOriginalAmountNegative); + // Tracks whether the user actually typed in this edit session, so that merely opening and + // closing the cell without input isn't mistaken for an explicit confirmation of the amount. + const hasUserTypedRef = useRef(false); const getNormalizedValue = (amountString: string, isAmountNegative: boolean) => { const parsedValue = parseFloatAnyLocale(amountString); @@ -92,7 +105,11 @@ function TotalCell({shouldShowTooltip, transactionItem, canEdit, onSave, report, onSave(normalizedValue); } : undefined, - (value, originalValue) => getNormalizedValue(value, isNegative) === getNormalizedValue(originalValue, isOriginalAmountNegative), + // A failed-scan placeholder amount that the user actually typed into is treated as changed so that + // explicitly re-entering 0 still submits and clears the scan-failure error, mirroring submitEditAmount in + // IOUAmountSubmission.ts. Merely opening and blurring the cell without typing is left as a no-op. + (value, originalValue) => + !(hasFailedScanAmountPlaceholder && hasUserTypedRef.current) && getNormalizedValue(value, isNegative) === getNormalizedValue(originalValue, isOriginalAmountNegative), ); // Ref used to programmatically focus the input when edit mode starts @@ -105,14 +122,21 @@ function TotalCell({shouldShowTooltip, transactionItem, canEdit, onSave, report, const handleStartEditing = () => { setIsNegative(isOriginalAmountNegative); + hasUserTypedRef.current = false; startEditing(); }; const handleAmountChange = (amountString: string) => { + hasUserTypedRef.current = true; setLocalValue(amountString); }; const onFormatAmount = (amountAsInt: number, currencyParam?: string) => { + // Seed the edit input as empty for a failed-scan placeholder, matching the blanked display above and the + // same falsy-amount-is-blank convention MoneyRequestAmountForm already uses for an unset amount. + if (hasFailedScanAmountPlaceholder) { + return ''; + } const decimals = getCurrencyDecimals(currencyParam); return convertToFrontendAmountAsString(amountAsInt, decimals); }; diff --git a/src/libs/IOUAmountSubmission.ts b/src/libs/IOUAmountSubmission.ts index 90950979ae4d..c29aa89e475b 100644 --- a/src/libs/IOUAmountSubmission.ts +++ b/src/libs/IOUAmountSubmission.ts @@ -53,7 +53,17 @@ import {getLoginByAccountID} from './PersonalDetailsUtils'; import {isTaxTrackingEnabled} from './PolicyUtils'; import {getPolicyExpenseChat, getTransactionDetails, isMoneyRequestReport, isPolicyExpenseChat, isSelfDM, shouldEnableNegative} from './ReportUtils'; import shouldUseDefaultExpensePolicy from './shouldUseDefaultExpensePolicy'; -import {calculateTaxAmount, getAmount, getCurrency, getDefaultTaxCode, getIsFromGlobalCreate, getTaxValue, hasReceipt, isExpenseUnreported} from './TransactionUtils'; +import { + calculateTaxAmount, + getAmount, + getCurrency, + getDefaultTaxCode, + getIsFromGlobalCreate, + getTaxValue, + hasReceipt, + isExpenseUnreported, + isFailedScanAmountPlaceholder, +} from './TransactionUtils'; type SubmitAmountArgs = { dateFnsLocale: DateFnsLocale | undefined; @@ -616,7 +626,8 @@ function submitEditAmount(args: SubmitAmountArgs, ctx: SubmitAmountContext): voi // If the value hasn't changed, don't request to save changes on the server and just close the modal const transactionCurrency = getCurrency(currentTransaction); - if (newAmount === getAmount(currentTransaction, false, false, allowNegative, disableOppositeConversion) && selectedCurrency === transactionCurrency) { + const hasFailedScanAmountPlaceholder = isFailedScanAmountPlaceholder(currentTransaction); + if (!hasFailedScanAmountPlaceholder && newAmount === getAmount(currentTransaction, false, false, allowNegative, disableOppositeConversion) && selectedCurrency === transactionCurrency) { navigateBack(); return; } diff --git a/src/libs/TransactionPreviewUtils.ts b/src/libs/TransactionPreviewUtils.ts index 8933f4e71166..e7bccab09a5f 100644 --- a/src/libs/TransactionPreviewUtils.ts +++ b/src/libs/TransactionPreviewUtils.ts @@ -43,6 +43,7 @@ import { isAmountMissing, isCreatedMissing, isDistanceRequest, + isFailedScanAmountPlaceholder, isFetchingWaypointsFromServer, isMerchantMissing, isOnHold, @@ -348,6 +349,8 @@ function getTransactionPreviewTextAndTranslationPaths({ let displayAmountText: TranslationPathOrText = isTransactionScanning ? {translationPath: 'iou.receiptStatusTitle'} : {text: convertToDisplayString(amount, requestCurrency)}; if (isFetchingWaypoints && !requestAmount) { displayAmountText = {translationPath: 'iou.fieldPending'}; + } else if (isFailedScanAmountPlaceholder(transaction)) { + displayAmountText = {text: ''}; } const iouOriginalMessage: OnyxEntry = isMoneyRequestAction(action) ? (getOriginalMessage(action) ?? undefined) : undefined; diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index a7487f90138e..72678d854faf 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -558,11 +558,26 @@ function isPartialMerchant(merchant: string): boolean { return merchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT; } +function isFailedScanAmountPlaceholder(transaction: OnyxEntry) { + // OPEN is included since editing another field (e.g. merchant) optimistically flips receipt.state to OPEN, + // which would otherwise flicker the amount back to "$0.00" until the server confirms it's still missing. + return ( + isScanRequest(transaction) && + (transaction?.receipt?.state === CONST.IOU.RECEIPT_STATE.SCAN_FAILED || transaction?.receipt?.state === CONST.IOU.RECEIPT_STATE.OPEN) && + (transaction?.amount === 0 || transaction?.amount === undefined) && + !hasValidModifiedAmount(transaction) + ); +} + function isAmountMissing(transaction: OnyxEntry, isFromExpenseReport = true) { + if (isFailedScanAmountPlaceholder(transaction)) { + return true; + } + if (isFromExpenseReport) { return transaction?.amount === undefined && (transaction?.modifiedAmount === undefined || transaction?.modifiedAmount === ''); } - return (transaction?.amount === 0 || transaction?.amount === undefined) && (!transaction?.modifiedAmount || transaction?.modifiedAmount === 0 || transaction?.modifiedAmount === ''); + return (transaction?.amount === 0 || transaction?.amount === undefined) && !hasValidModifiedAmount(transaction); } function hasValidModifiedAmount(transaction: OnyxEntry | null): boolean { @@ -597,7 +612,7 @@ function isCreatedMissing(transaction: OnyxEntry) { function areRequiredFieldsEmpty(transaction: OnyxEntry, transactionReport: OnyxEntry): boolean { const isFromExpenseReport = transactionReport?.type === CONST.REPORT.TYPE.EXPENSE; - return (isFromExpenseReport && isMerchantMissing(transaction)) || isCreatedMissing(transaction) || (!isFromExpenseReport && getAmount(transaction) === 0); + return (isFromExpenseReport && isMerchantMissing(transaction)) || isCreatedMissing(transaction) || isAmountMissing(transaction, isFromExpenseReport); } function getClearedPendingFields(transactionChanges: TransactionChanges) { @@ -3583,6 +3598,7 @@ export { isDistanceTypeRequest, recalculateUnreportedTransactionDetails, hasSmartScanFailedWithMissingFields, + isFailedScanAmountPlaceholder, isDeletedTransaction, getDistanceRequestType, isUnreportedManagedCardTransaction, diff --git a/src/libs/actions/IOU/BulkEdit.ts b/src/libs/actions/IOU/BulkEdit.ts index 8cdcaf73c19f..9b88702eca08 100644 --- a/src/libs/actions/IOU/BulkEdit.ts +++ b/src/libs/actions/IOU/BulkEdit.ts @@ -32,6 +32,7 @@ import { getTaxValue, getUpdatedTransaction, isDistanceRequest, + isFailedScanAmountPlaceholder, isOnHold, isSplitChildTransaction, } from '@libs/TransactionUtils'; @@ -74,7 +75,12 @@ function removeUnchangedBulkEditFields( const nextValue = transactionChanges[field]; const currentValue = currentDetails[field as keyof TransactionDetails]; - if (nextValue !== currentValue) { + // A failed-scan placeholder amount must always be treated as changed so that bulk-confirming the same + // displayed value (e.g. re-entering 0) still submits and clears the scan-failure error, mirroring the + // no-op bypass already used in IOUAmountSubmission.ts and TotalCell.tsx. + const isFailedScanAmountEdit = field === 'amount' && isFailedScanAmountPlaceholder(transaction); + + if (isFailedScanAmountEdit || nextValue !== currentValue) { filteredChanges = { ...filteredChanges, [field]: nextValue, diff --git a/tests/ui/TotalCellTest.tsx b/tests/ui/TotalCellTest.tsx new file mode 100644 index 000000000000..cb770a8792bf --- /dev/null +++ b/tests/ui/TotalCellTest.tsx @@ -0,0 +1,219 @@ +import {fireEvent, render, screen} from '@testing-library/react-native'; + +import ComposeProviders from '@components/ComposeProviders'; +import {LocaleContextProvider} from '@components/LocaleContextProvider'; +import OnyxListItemProvider from '@components/OnyxListItemProvider'; +import TotalCell from '@components/TransactionItemRow/DataCells/TotalCell'; + +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Transaction} from '@src/types/onyx'; + +import type * as NativeNavigation from '@react-navigation/native'; +import type ReactNative from 'react-native'; + +import React from 'react'; +import Onyx from 'react-native-onyx'; + +import createRandomTransaction from '../utils/collections/transaction'; +import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; + +jest.mock('@libs/Navigation/Navigation'); + +// The amount edit input (NumberWithSymbolForm) calls useIsFocused/useNavigation, which need a NavigationContainer +// ancestor we don't render here. Matches the mock pattern in NumberWithSymbolFormTest.tsx. +jest.mock('@react-navigation/native', () => ({ + ...jest.requireActual('@react-navigation/native'), + useNavigation: jest.fn(() => ({ + navigate: jest.fn(), + addListener: jest.fn(() => jest.fn()), + })), + useIsFocused: () => true, + useRoute: jest.fn(() => ({key: '', name: '', params: {}})), +})); +jest.mock('@hooks/useCurrencyList', () => ({ + useCurrencyListActions: () => ({ + convertToDisplayString: (amount?: number, currency?: string) => `${currency === 'USD' ? '$' : `${currency ?? 'USD'} `}${((amount ?? 0) / 100).toFixed(2)}`, + getCurrencyDecimals: () => 2, + getCurrencySymbol: () => '$', + }), +})); + +// Forces the cell into the "wide/editable" layout branch, which jsdom's default viewport doesn't naturally satisfy. +jest.mock('@hooks/useResponsiveLayoutOnWideRHP', () => ({ + __esModule: true, + default: () => ({isLargeScreenWidth: true, shouldUseNarrowLayout: false, isInNarrowPaneModal: false}), +})); + +// EditableCell only shows/enables the edit-pencil button while the cell is hovered (isCellHovered from Hoverable), +// which jsdom can't simulate reliably. Force it hovered so the wrapping View's pointerEvents isn't "none" — RNTL +// v13's fireEvent.press respects pointerEvents and silently no-ops when a "none" ancestor blocks the target. +jest.mock('@components/Hoverable', () => ({ + __esModule: true, + default: ({children}: {children: ((isHovered: boolean) => React.ReactNode) | React.ReactNode}) => (typeof children === 'function' ? children(true) : children), +})); + +// Mirrors the mock pattern used in AgentsTableRowTest.tsx to make the pencil button directly pressable. +jest.mock('@components/Pressable/PressableWithFeedback', () => { + const {TouchableOpacity} = jest.requireActual('react-native'); + function mockPressableWithFeedback({ + children, + onPress, + accessibilityLabel, + }: { + children: React.ReactNode | ((state: {hovered: boolean; pressed: boolean}) => React.ReactNode); + onPress: () => void; + accessibilityLabel?: string; + }) { + const content = typeof children === 'function' ? children({hovered: false, pressed: false}) : children; + return ( + + {content} + + ); + } + return {__esModule: true, default: mockPressableWithFeedback}; +}); + +const MOCK_TRANSACTION_ID = '1'; + +const createBaseTransaction = (overrides: Partial = {}): Transaction => ({ + ...createRandomTransaction(1), + transactionID: MOCK_TRANSACTION_ID, + currency: CONST.CURRENCY.USD, + modifiedAmount: undefined, + ...overrides, +}); + +const renderTotalCell = (transactionItem: Transaction) => { + return render( + + + , + ); +}; + +describe('TotalCell', () => { + beforeAll(async () => { + Onyx.init({keys: ONYXKEYS}); + await Onyx.set(ONYXKEYS.NVP_PREFERRED_LOCALE, CONST.LOCALES.DEFAULT); + return waitForBatchedUpdates(); + }); + + it('blanks the amount for a failed-scan amount placeholder', async () => { + const mockTransaction = createBaseTransaction({ + amount: 0, + iouRequestType: CONST.IOU.REQUEST_TYPE.SCAN, + receipt: {state: CONST.IOU.RECEIPT_STATE.SCAN_FAILED}, + }); + + renderTotalCell(mockTransaction); + await waitForBatchedUpdates(); + + expect(screen.queryByText('$0.00')).not.toBeOnTheScreen(); + }); + + it('shows the formatted amount for a normal transaction', async () => { + const mockTransaction = createBaseTransaction({ + amount: 1000, + iouRequestType: CONST.IOU.REQUEST_TYPE.MANUAL, + }); + + renderTotalCell(mockTransaction); + await waitForBatchedUpdates(); + + expect(screen.getByText('$10.00')).toBeOnTheScreen(); + }); + + it('does not blank a legitimate manual $0.00 amount', async () => { + const mockTransaction = createBaseTransaction({ + amount: 0, + iouRequestType: CONST.IOU.REQUEST_TYPE.MANUAL, + }); + + renderTotalCell(mockTransaction); + await waitForBatchedUpdates(); + + expect(screen.getByText('$0.00')).toBeOnTheScreen(); + }); + + it('does not blank the amount once the failed-scan placeholder amount is confirmed', async () => { + const mockTransaction = createBaseTransaction({ + amount: 0, + modifiedAmount: 0, + iouRequestType: CONST.IOU.REQUEST_TYPE.SCAN, + receipt: {state: CONST.IOU.RECEIPT_STATE.SCAN_FAILED}, + }); + + renderTotalCell(mockTransaction); + await waitForBatchedUpdates(); + + expect(screen.getByText('$0.00')).toBeOnTheScreen(); + }); + + it('saves when the user types 0 to confirm a failed-scan placeholder amount', async () => { + const onSave = jest.fn(); + const mockTransaction = createBaseTransaction({ + amount: 0, + iouRequestType: CONST.IOU.REQUEST_TYPE.SCAN, + receipt: {state: CONST.IOU.RECEIPT_STATE.SCAN_FAILED}, + }); + + render( + + + , + ); + await waitForBatchedUpdates(); + + fireEvent.press(await screen.findByTestId('mock-edit-button')); + const input = await screen.findByLabelText('Amount (USD)'); + + fireEvent.changeText(input, '0'); + fireEvent(input, 'blur'); + + expect(onSave).toHaveBeenCalledWith(0); + }); + + it('does not save when the cell is opened and blurred without typing', async () => { + const onSave = jest.fn(); + const mockTransaction = createBaseTransaction({ + amount: 0, + iouRequestType: CONST.IOU.REQUEST_TYPE.SCAN, + receipt: {state: CONST.IOU.RECEIPT_STATE.SCAN_FAILED}, + }); + + render( + + + , + ); + await waitForBatchedUpdates(); + + fireEvent.press(await screen.findByTestId('mock-edit-button')); + const input = await screen.findByLabelText('Amount (USD)'); + + fireEvent(input, 'blur'); + + expect(onSave).not.toHaveBeenCalled(); + }); +}); diff --git a/tests/unit/TransactionPreviewUtils.test.ts b/tests/unit/TransactionPreviewUtils.test.ts index 06abc3eb869c..9f085e92d0f1 100644 --- a/tests/unit/TransactionPreviewUtils.test.ts +++ b/tests/unit/TransactionPreviewUtils.test.ts @@ -229,6 +229,35 @@ describe('TransactionPreviewUtils', () => { expect(result.displayAmountText.translationPath).toEqual('iou.receiptStatusTitle'); }); + it('blanks the displayed amount for a failed-scan amount placeholder', () => { + const functionArgs = { + ...basicProps, + transaction: { + ...basicProps.transaction, + amount: 0, + iouRequestType: CONST.IOU.REQUEST_TYPE.SCAN, + receipt: {state: CONST.IOU.RECEIPT_STATE.SCAN_FAILED}, + }, + originalTransaction: undefined, + }; + const result = getTransactionPreviewTextAndTranslationPaths(functionArgs); + expect(result.displayAmountText.text).toEqual(''); + }); + + it('does not blank a legitimate manual $0.00 amount', () => { + const functionArgs = { + ...basicProps, + transaction: { + ...basicProps.transaction, + amount: 0, + iouRequestType: CONST.IOU.REQUEST_TYPE.MANUAL, + }, + originalTransaction: undefined, + }; + const result = getTransactionPreviewTextAndTranslationPaths(functionArgs); + expect(result.displayAmountText.text).toEqual('$0.00'); + }); + it('handles currency and amount display correctly for scan split bill manually completed', () => { const modifiedAmount = 300; const currency = 'EUR'; diff --git a/tests/unit/hooks/useConfirmationAmount.test.tsx b/tests/unit/hooks/useConfirmationAmount.test.tsx index 7d27aaccbd13..dabcf26b4cfe 100644 --- a/tests/unit/hooks/useConfirmationAmount.test.tsx +++ b/tests/unit/hooks/useConfirmationAmount.test.tsx @@ -100,4 +100,43 @@ describe('useConfirmationAmount', () => { // 100 / 4 = 25 expect(result.current.formattedAmountPerAttendee).toContain('25.00'); }); + + it('formattedAmount is empty string for a failed-scan amount placeholder', () => { + const {result} = renderHook( + () => + useConfirmationAmount({ + ...baseParams, + iouAmount: 0, + transaction: createMock({ + transactionID: 'txn1', + amount: 0, + iouRequestType: CONST.IOU.REQUEST_TYPE.SCAN, + receipt: {state: CONST.IOU.RECEIPT_STATE.SCAN_FAILED}, + comment: {}, + }), + }), + {wrapper: Wrapper}, + ); + expect(result.current.formattedAmount).toBe(''); + }); + + it('formattedAmount is not blanked once the failed-scan amount is confirmed', () => { + const {result} = renderHook( + () => + useConfirmationAmount({ + ...baseParams, + iouAmount: 0, + transaction: createMock({ + transactionID: 'txn1', + amount: 0, + modifiedAmount: 0, + iouRequestType: CONST.IOU.REQUEST_TYPE.SCAN, + receipt: {state: CONST.IOU.RECEIPT_STATE.SCAN_FAILED}, + comment: {}, + }), + }), + {wrapper: Wrapper}, + ); + expect(result.current.formattedAmount).toContain('0.00'); + }); });