diff --git a/src/components/ReportActionItem/ReportActionItemImage.tsx b/src/components/ReportActionItem/ReportActionItemImage.tsx index 646e84a62a12..8c915eb161e6 100644 --- a/src/components/ReportActionItem/ReportActionItemImage.tsx +++ b/src/components/ReportActionItem/ReportActionItemImage.tsx @@ -134,8 +134,13 @@ function ReportActionItemImage({ ); } - const originalImageSource = tryResolveUrlFromApiRoot(image ?? ''); - const thumbnailSource = tryResolveUrlFromApiRoot(thumbnail ?? ''); + const localSource = transaction?.receipt?.localSource; + const effectiveIsLocalFile = isLocalFile || !!localSource; + const effectiveThumbnail = localSource ?? thumbnail; + const effectiveImage = localSource !== undefined && typeof image === 'string' ? localSource : image; + + const originalImageSource = tryResolveUrlFromApiRoot(effectiveImage ?? ''); + const thumbnailSource = tryResolveUrlFromApiRoot(effectiveThumbnail ?? ''); const isEReceipt = transaction && !hasReceiptSource(transaction) && hasEReceipt(transaction); const isPDF = filename && Str.isPDF(filename); @@ -143,7 +148,7 @@ function ReportActionItemImage({ if (isEReceipt) { propsObj = {isEReceipt: true, transactionID: transaction.transactionID, iconSize: isSingleImage ? 'medium' : ('small' as IconSize), shouldUseFullHeight}; - } else if (thumbnail && !isLocalFile) { + } else if (effectiveThumbnail && !effectiveIsLocalFile) { propsObj = { shouldUseThumbnailImage: shouldUseThumbnailImage ?? true, @@ -158,7 +163,7 @@ function ReportActionItemImage({ // If the image is full height, use initial position to make sure it will grow properly to fill the container shouldUseInitialObjectPosition: isMapDistanceRequest && !shouldUseFullHeight, }; - } else if (isLocalFile && isPDF && typeof originalImageSource === 'string') { + } else if (effectiveIsLocalFile && isPDF && typeof originalImageSource === 'string') { propsObj = {isPDFThumbnail: true, source: originalImageSource}; } else { propsObj = { @@ -166,7 +171,7 @@ function ReportActionItemImage({ ...(isThumbnail && {iconSize: (isSingleImage ? 'medium' : 'small') as IconSize, fileExtension}), shouldUseThumbnailImage: shouldUseThumbnailImage ?? true, isAuthTokenRequired: false, - source: shouldUseThumbnailImage ? (thumbnail ?? image ?? '') : originalImageSource, + source: shouldUseThumbnailImage ? (effectiveThumbnail ?? effectiveImage ?? '') : originalImageSource, // If the image is full height, use initial position to make sure it will grow properly to fill the container shouldUseInitialObjectPosition: isMapDistanceRequest && !shouldUseFullHeight, diff --git a/src/libs/DebugUtils.ts b/src/libs/DebugUtils.ts index cb9387c64e22..07de7e67a712 100644 --- a/src/libs/DebugUtils.ts +++ b/src/libs/DebugUtils.ts @@ -796,6 +796,7 @@ function validateReportActionDraftProperty(key: keyof ReportAction, value: strin name: 'string', receiptID: 'string', source: 'string', + localSource: 'string', filename: 'string', reservationList: 'string', isTestReceipt: 'boolean', @@ -1138,6 +1139,7 @@ function validateTransactionDraftProperty(key: keyof Transaction, value: string) return validateObject>(value, { type: 'string', source: 'string', + localSource: 'string', name: 'string', filename: 'string', state: CONST.IOU.RECEIPT_STATE, diff --git a/src/libs/actions/IOU/index.ts b/src/libs/actions/IOU/index.ts index 5e835767cd9c..5e5d7b1ccf3c 100644 --- a/src/libs/actions/IOU/index.ts +++ b/src/libs/actions/IOU/index.ts @@ -42,7 +42,7 @@ import DateUtils from '@libs/DateUtils'; import {registerDeferredWrite} from '@libs/deferredLayoutWrite'; import DistanceRequestUtils from '@libs/DistanceRequestUtils'; import {getMicroSecondOnyxErrorObject, getMicroSecondOnyxErrorWithTranslationKey} from '@libs/ErrorUtils'; -import {readFileAsync} from '@libs/fileDownload/FileUtils'; +import {isLocalFile, readFileAsync} from '@libs/fileDownload/FileUtils'; import type {MinimalTransaction} from '@libs/Formula'; import getIsNarrowLayout from '@libs/getIsNarrowLayout'; import GoogleTagManager from '@libs/GoogleTagManager'; @@ -1888,6 +1888,18 @@ type BuildOnyxDataForMoneyRequestKeys = | typeof ONYXKEYS.NVP_QUICK_ACTION_GLOBAL_CREATE | typeof ONYXKEYS.COLLECTION.SNAPSHOT; +/** + * When a receipt is a local file (e.g. taken from camera or picked from gallery), its `source` is a local URI + * that will be lost once the optimistic transaction is replaced by the server response. We stash it in + * `localSource` so the UI can continue showing the local image while SmartScan is in progress. + */ +function getTransactionWithPreservedLocalReceiptSource(transaction: OnyxTypes.Transaction, isScanRequest: boolean): OnyxTypes.Transaction { + if (isScanRequest && isLocalFile(transaction.receipt?.source)) { + return {...transaction, receipt: {...transaction.receipt, localSource: String(transaction.receipt?.source)}}; + } + return transaction; +} + /** Builds the Onyx data for an expense */ function buildOnyxDataForMoneyRequest(moneyRequestParams: BuildOnyxDataForMoneyRequestParams): OnyxData { const { @@ -1925,7 +1937,6 @@ function buildOnyxDataForMoneyRequest(moneyRequestParams: BuildOnyxDataForMoneyR const outstandingChildRequest = getOutstandingChildRequest(iou.report); const clearedPendingFields = Object.fromEntries(Object.keys(transaction.pendingFields ?? {}).map((key) => [key, null])); const isMoneyRequestToManagerMcTest = isTestTransactionReport(iou.report); - const onyxData: OnyxData = { optimisticData: [], successData: [], @@ -1980,7 +1991,7 @@ function buildOnyxDataForMoneyRequest(moneyRequestParams: BuildOnyxDataForMoneyR { onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`, - value: transaction, + value: getTransactionWithPreservedLocalReceiptSource(transaction, isScanRequest), }, isNewChatReport ? { @@ -2615,7 +2626,6 @@ function buildOnyxDataForTrackExpense({ const isScanRequest = isScanRequestTransactionUtils(transaction); const isDistanceRequest = isDistanceRequestTransactionUtils(transaction); const clearedPendingFields = Object.fromEntries(Object.keys(transaction.pendingFields ?? {}).map((key) => [key, null])); - const onyxData: OnyxData = { optimisticData: [], successData: [], @@ -2750,7 +2760,7 @@ function buildOnyxDataForTrackExpense({ { onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`, - value: transaction, + value: getTransactionWithPreservedLocalReceiptSource(transaction, isScanRequest), }, { onyxMethod: Onyx.METHOD.MERGE, diff --git a/src/types/onyx/Transaction.ts b/src/types/onyx/Transaction.ts index 383de5bc27ed..da3d22bea107 100644 --- a/src/types/onyx/Transaction.ts +++ b/src/types/onyx/Transaction.ts @@ -218,6 +218,9 @@ type Receipt = { /** Path of the receipt file */ source?: ReceiptSource; + /** Local file URI preserved on the creating device so the remote source from the server does not cause a reload */ + localSource?: string; + /** Name of receipt file */ filename?: string;