diff --git a/src/components/DistanceEReceipt.tsx b/src/components/DistanceEReceipt.tsx index fda0c5441734..20b927913bfb 100644 --- a/src/components/DistanceEReceipt.tsx +++ b/src/components/DistanceEReceipt.tsx @@ -15,9 +15,9 @@ import Icon from './Icon'; import * as Expensicons from './Icon/Expensicons'; import ImageSVG from './ImageSVG'; import PendingMapView from './MapView/PendingMapView'; +import ReceiptImage from './ReceiptImage'; import ScrollView from './ScrollView'; import Text from './Text'; -import ThumbnailImage from './ThumbnailImage'; type DistanceEReceiptProps = { /** The transaction for the distance request */ @@ -30,7 +30,7 @@ function DistanceEReceipt({transaction}: DistanceEReceiptProps) { const thumbnail = TransactionUtils.hasReceipt(transaction) ? ReceiptUtils.getThumbnailAndImageURIs(transaction).thumbnail : null; const {amount: transactionAmount, currency: transactionCurrency, merchant: transactionMerchant, created: transactionDate} = ReportUtils.getTransactionDetails(transaction) ?? {}; const formattedTransactionAmount = CurrencyUtils.convertToDisplayString(transactionAmount, transactionCurrency); - const thumbnailSource = tryResolveUrlFromApiRoot((thumbnail as string) || ''); + const thumbnailSource = tryResolveUrlFromApiRoot(thumbnail ?? ''); const waypoints = useMemo(() => transaction?.comment?.waypoints ?? {}, [transaction?.comment?.waypoints]); const sortedWaypoints = useMemo( () => @@ -58,11 +58,9 @@ function DistanceEReceipt({transaction}: DistanceEReceiptProps) { {TransactionUtils.isFetchingWaypointsFromServer(transaction) || !thumbnailSource ? ( ) : ( - )} diff --git a/src/components/EReceiptThumbnail.tsx b/src/components/EReceiptThumbnail.tsx index 023dcc16e696..63889f76e67c 100644 --- a/src/components/EReceiptThumbnail.tsx +++ b/src/components/EReceiptThumbnail.tsx @@ -5,6 +5,7 @@ import {withOnyx} from 'react-native-onyx'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ReportUtils from '@libs/ReportUtils'; +import colors from '@styles/theme/colors'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -14,6 +15,7 @@ import * as eReceiptBGs from './Icon/EReceiptBGs'; import * as Expensicons from './Icon/Expensicons'; import * as MCCIcons from './Icon/MCCIcons'; import Image from './Image'; +import Text from './Text'; type EReceiptThumbnailOnyxProps = { transaction: OnyxEntry; @@ -26,6 +28,15 @@ type EReceiptThumbnailProps = EReceiptThumbnailOnyxProps & { // eslint-disable-next-line react/no-unused-prop-types transactionID: string; + /** Border radius to be applied on the parent view. */ + borderRadius?: number; + + /** The file extension of the receipt that the preview thumbnail is being displayed for. */ + fileExtension?: string; + + /** Whether it is a receipt thumbnail we are displaying. */ + isReceiptThumbnail?: boolean; + /** Center the eReceipt Icon vertically */ centerIconV?: boolean; @@ -42,13 +53,14 @@ const backgroundImages = { [CONST.ERECEIPT_COLORS.PINK]: eReceiptBGs.EReceiptBG_Pink, }; -function EReceiptThumbnail({transaction, centerIconV = true, iconSize = 'large'}: EReceiptThumbnailProps) { +function EReceiptThumbnail({transaction, borderRadius, fileExtension, isReceiptThumbnail = false, centerIconV = true, iconSize = 'large'}: EReceiptThumbnailProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); + const colorCode = isReceiptThumbnail ? StyleUtils.getFileExtensionColorCode(fileExtension) : StyleUtils.getEReceiptColorCode(transaction); - const backgroundImage = useMemo(() => backgroundImages[StyleUtils.getEReceiptColorCode(transaction)], [StyleUtils, transaction]); + const backgroundImage = useMemo(() => backgroundImages[colorCode], [colorCode]); - const colorStyles = StyleUtils.getEReceiptColorStyles(StyleUtils.getEReceiptColorCode(transaction)); + const colorStyles = StyleUtils.getEReceiptColorStyles(colorCode); const primaryColor = colorStyles?.backgroundColor; const secondaryColor = colorStyles?.color; const transactionDetails = ReportUtils.getTransactionDetails(transaction); @@ -58,15 +70,21 @@ function EReceiptThumbnail({transaction, centerIconV = true, iconSize = 'large'} let receiptIconWidth: number = variables.eReceiptIconWidth; let receiptIconHeight: number = variables.eReceiptIconHeight; let receiptMCCSize: number = variables.eReceiptMCCHeightWidth; + let labelFontSize: number = variables.fontSizeNormal; + let labelLineHeight: number = variables.lineHeightLarge; if (iconSize === 'small') { receiptIconWidth = variables.eReceiptIconWidthSmall; receiptIconHeight = variables.eReceiptIconHeightSmall; receiptMCCSize = variables.eReceiptMCCHeightWidthSmall; + labelFontSize = variables.fontSizeExtraSmall; + labelLineHeight = variables.lineHeightXSmall; } else if (iconSize === 'medium') { receiptIconWidth = variables.eReceiptIconWidthMedium; receiptIconHeight = variables.eReceiptIconHeightMedium; receiptMCCSize = variables.eReceiptMCCHeightWidthMedium; + labelFontSize = variables.fontSizeLabel; + labelLineHeight = variables.lineHeightNormal; } return ( @@ -77,6 +95,7 @@ function EReceiptThumbnail({transaction, centerIconV = true, iconSize = 'large'} styles.overflowHidden, styles.alignItemsCenter, centerIconV ? styles.justifyContentCenter : {}, + borderRadius ? {borderRadius} : {}, ]} > - {MCCIcon ? ( + {isReceiptThumbnail && fileExtension && ( + + {fileExtension.toUpperCase()} + + )} + {MCCIcon && !isReceiptThumbnail ? ( ({ transaction: { key: ({transactionID}) => `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, }, })(EReceiptThumbnail); -export type {EReceiptThumbnailProps, EReceiptThumbnailOnyxProps}; +export type {IconSize, EReceiptThumbnailProps, EReceiptThumbnailOnyxProps}; diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index 4550a7aef5d2..79e2c5b12a12 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -35,10 +35,10 @@ import ButtonWithDropdownMenu from './ButtonWithDropdownMenu'; import type {DropdownOption} from './ButtonWithDropdownMenu/types'; import ConfirmedRoute from './ConfirmedRoute'; import FormHelpMessage from './FormHelpMessage'; -import Image from './Image'; import MenuItemWithTopDescription from './MenuItemWithTopDescription'; import OptionsSelector from './OptionsSelector'; import ReceiptEmptyState from './ReceiptEmptyState'; +import ReceiptImage from './ReceiptImage'; import SettlementButton from './SettlementButton'; import ShowMoreButton from './ShowMoreButton'; import Switch from './Switch'; @@ -577,8 +577,12 @@ function MoneyRequestConfirmationList({ ); }, [isReadOnly, iouType, bankAccountRoute, iouCurrencyCode, policyID, selectedParticipants.length, confirm, splitOrRequestOptions, formError, styles.ph1, styles.mb2]); - const {image: receiptImage, thumbnail: receiptThumbnail} = - receiptPath && receiptFilename ? ReceiptUtils.getThumbnailAndImageURIs(transaction, receiptPath, receiptFilename) : ({} as ReceiptUtils.ThumbnailAndImageURI); + const { + image: receiptImage, + thumbnail: receiptThumbnail, + isThumbnail, + fileExtension, + } = receiptPath && receiptFilename ? ReceiptUtils.getThumbnailAndImageURIs(transaction, receiptPath, receiptFilename) : ({} as ReceiptUtils.ThumbnailAndImageURI); return ( // @ts-expect-error This component is deprecated and will not be migrated to TypeScript (context: https://expensify.slack.com/archives/C01GTK53T8Q/p1709232289899589?thread_ts=1709156803.359359&cid=C01GTK53T8Q) )} - + {/* eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing */} {receiptImage || receiptThumbnail ? ( - ) : ( // The empty receipt component should only show for IOU Requests of a paid policy ("Team" or "Corporate") diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index f27cd507d668..513fdbfb1fd5 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -37,12 +37,12 @@ import ConfirmedRoute from './ConfirmedRoute'; import ConfirmModal from './ConfirmModal'; import FormHelpMessage from './FormHelpMessage'; import * as Expensicons from './Icon/Expensicons'; -import Image from './Image'; import MenuItemWithTopDescription from './MenuItemWithTopDescription'; import optionPropTypes from './optionPropTypes'; import OptionsSelector from './OptionsSelector'; import PDFThumbnail from './PDFThumbnail'; import ReceiptEmptyState from './ReceiptEmptyState'; +import ReceiptImage from './ReceiptImage'; import SettlementButton from './SettlementButton'; import Switch from './Switch'; import tagPropTypes from './tagPropTypes'; @@ -897,6 +897,8 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const { image: receiptImage, thumbnail: receiptThumbnail, + isThumbnail, + fileExtension, isLocalFile, } = receiptPath && receiptFilename ? ReceiptUtils.getThumbnailAndImageURIs(transaction, receiptPath, receiptFilename) : {}; @@ -911,16 +913,18 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ onPassword={() => setIsAttachmentInvalid(true)} /> ) : ( - ), - [receiptFilename, receiptImage, styles, receiptThumbnail, isLocalFile, isAttachmentInvalid], + [isLocalFile, receiptFilename, receiptImage, styles.moneyRequestImage, isAttachmentInvalid, isThumbnail, receiptThumbnail, fileExtension], ); return ( diff --git a/src/components/ReceiptImage.tsx b/src/components/ReceiptImage.tsx new file mode 100644 index 000000000000..08892f11b021 --- /dev/null +++ b/src/components/ReceiptImage.tsx @@ -0,0 +1,136 @@ +import React from 'react'; +import {View} from 'react-native'; +import useThemeStyles from '@hooks/useThemeStyles'; +import type IconAsset from '@src/types/utils/IconAsset'; +import EReceiptThumbnail from './EReceiptThumbnail'; +import type {IconSize} from './EReceiptThumbnail'; +import Image from './Image'; +import PDFThumbnail from './PDFThumbnail'; +import ThumbnailImage from './ThumbnailImage'; + +type Style = {height: number; borderRadius: number; margin: number}; + +type ReceiptImageProps = ( + | { + /** Transaction ID of the transaction the receipt belongs to */ + transactionID: string; + + /** Whether it is EReceipt */ + isEReceipt: boolean; + + /** Whether it is receipt preview thumbnail we are displaying */ + isThumbnail?: boolean; + + /** Url of the receipt image */ + source?: string; + + /** Whether it is a pdf thumbnail we are displaying */ + isPDFThumbnail?: boolean; + } + | { + transactionID?: string; + isEReceipt?: boolean; + isThumbnail: boolean; + source?: string; + isPDFThumbnail?: boolean; + } + | { + transactionID?: string; + isEReceipt?: boolean; + isThumbnail?: boolean; + source: string; + isPDFThumbnail?: boolean; + } + | { + transactionID?: string; + isEReceipt?: boolean; + isThumbnail?: boolean; + source: string; + isPDFThumbnail: string; + } +) & { + /** Whether we should display the receipt with ThumbnailImage component */ + shouldUseThumbnailImage?: boolean; + + /** Whether the receipt image requires an authToken */ + isAuthTokenRequired?: boolean; + + /** Any additional styles to apply */ + style?: Style; + + /** The file extension of the receipt file */ + fileExtension?: string; + + /** number of images displayed in the same parent container */ + iconSize?: IconSize; + + /** If the image fails to load – show the provided fallback icon */ + fallbackIcon?: IconAsset; + + /** The size of the fallback icon */ + fallbackIconSize?: number; +}; + +function ReceiptImage({ + transactionID, + isPDFThumbnail = false, + isThumbnail = false, + shouldUseThumbnailImage = false, + isEReceipt = false, + source, + isAuthTokenRequired, + style, + fileExtension, + iconSize, + fallbackIcon, + fallbackIconSize, +}: ReceiptImageProps) { + const styles = useThemeStyles(); + + if (isPDFThumbnail) { + return ( + + ); + } + + if (isEReceipt || isThumbnail) { + const props = isThumbnail && {borderRadius: style?.borderRadius, fileExtension, isReceiptThumbnail: true}; + return ( + + + + ); + } + + if (shouldUseThumbnailImage) { + return ( + + ); + } + + return ( + + ); +} + +export type {ReceiptImageProps}; +export default ReceiptImage; diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index bb0308ee4509..5c382ca8ee33 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -259,6 +259,8 @@ function MoneyRequestView({ ) : ( { - if (thumbnail) { - return typeof thumbnail === 'string' ? {uri: thumbnail} : thumbnail; - } - - return typeof image === 'string' ? {uri: image} : image; - }, [image, thumbnail]); + let propsObj: ReceiptImageProps; if (isEReceipt) { - receiptImageComponent = ( - - - - ); + propsObj = {isEReceipt: true, transactionID: transaction.transactionID, iconSize: isSingleImage ? 'medium' : ('small' as IconSize)}; } else if (thumbnail && !isLocalFile) { - receiptImageComponent = ( - - ); + propsObj = { + shouldUseThumbnailImage: true, + source: thumbnailSource, + fallbackIcon: Expensicons.Receipt, + fallbackIconSize: isSingleImage ? variables.iconSizeSuperLarge : variables.iconSizeExtraLarge, + }; } else if (isLocalFile && filename && Str.isPDF(filename) && typeof attachmentModalSource === 'string') { - receiptImageComponent = ( - - ); + propsObj = {isPDFThumbnail: true, source: attachmentModalSource}; } else { - receiptImageComponent = ( - - ); + propsObj = { + isThumbnail, + ...(isThumbnail && {iconSize: (isSingleImage ? 'medium' : 'small') as IconSize, fileExtension}), + source: thumbnail ?? image ?? '', + }; } if (enablePreviewModal) { @@ -113,14 +102,14 @@ function ReportActionItemImage({thumbnail, image, enablePreviewModal = false, tr accessibilityLabel={translate('accessibilityHints.viewAttachment')} accessibilityRole={CONST.ROLE.BUTTON} > - {receiptImageComponent} + )} ); } - return receiptImageComponent; + return ; } ReportActionItemImage.displayName = 'ReportActionItemImage'; diff --git a/src/components/ReportActionItem/ReportActionItemImages.tsx b/src/components/ReportActionItem/ReportActionItemImages.tsx index c66dc36d1ed5..ee8cb0849ca0 100644 --- a/src/components/ReportActionItem/ReportActionItemImages.tsx +++ b/src/components/ReportActionItem/ReportActionItemImages.tsx @@ -65,21 +65,23 @@ function ReportActionItemImages({images, size, total, isHovered = false}: Report return ( - {shownImages.map(({thumbnail, image, transaction, isLocalFile, filename}, index) => { + {shownImages.map(({thumbnail, isThumbnail, image, transaction, isLocalFile, fileExtension, filename}, index) => { // Show a border to separate multiple images. Shown to the right for each except the last. const shouldShowBorder = shownImages.length > 1 && index < shownImages.length - 1; const borderStyle = shouldShowBorder ? styles.reportActionItemImageBorder : {}; return ( diff --git a/src/libs/ReceiptUtils.ts b/src/libs/ReceiptUtils.ts index 75d14be1a907..cf8937874216 100644 --- a/src/libs/ReceiptUtils.ts +++ b/src/libs/ReceiptUtils.ts @@ -1,11 +1,6 @@ import Str from 'expensify-common/lib/str'; import _ from 'lodash'; -import type {ImageSourcePropType} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; -import ReceiptDoc from '@assets/images/receipt-doc.png'; -import ReceiptGeneric from '@assets/images/receipt-generic.png'; -import ReceiptHTML from '@assets/images/receipt-html.png'; -import ReceiptSVG from '@assets/images/receipt-svg.png'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import type {Transaction} from '@src/types/onyx'; @@ -14,16 +9,13 @@ import * as FileUtils from './fileDownload/FileUtils'; import * as TransactionUtils from './TransactionUtils'; type ThumbnailAndImageURI = { - image: ImageSourcePropType | string; - thumbnail: ImageSourcePropType | string | null; - transaction?: Transaction; + image?: string; + thumbnail?: string; + transaction?: OnyxEntry; isLocalFile?: boolean; + isThumbnail?: boolean; filename?: string; -}; - -type FileNameAndExtension = { fileExtension?: string; - fileName?: string; }; /** @@ -35,11 +27,11 @@ type FileNameAndExtension = { */ function getThumbnailAndImageURIs(transaction: OnyxEntry, receiptPath: string | null = null, receiptFileName: string | null = null): ThumbnailAndImageURI { if (TransactionUtils.isFetchingWaypointsFromServer(transaction)) { - return {thumbnail: null, image: ReceiptGeneric, isLocalFile: true}; + return {isThumbnail: true, isLocalFile: true}; } - // URI to image, i.e. blob:new.expensify.com/9ef3a018-4067-47c6-b29f-5f1bd35f213d or expensify.com/receipts/w_e616108497ef940b7210ec6beb5a462d01a878f4.jpg // If there're errors, we need to display them in preview. We can store many files in errors, but we just need to get the last one const errors = _.findLast(transaction?.errors) as ReceiptError | undefined; + // URI to image, i.e. blob:new.expensify.com/9ef3a018-4067-47c6-b29f-5f1bd35f213d or expensify.com/receipts/w_e616108497ef940b7210ec6beb5a462d01a878f4.jpg const path = errors?.source ?? transaction?.receipt?.source ?? receiptPath ?? ''; // filename of uploaded image or last part of remote URI const filename = errors?.filename ?? transaction?.filename ?? receiptFileName ?? ''; @@ -48,12 +40,12 @@ function getThumbnailAndImageURIs(transaction: OnyxEntry, receiptPa const isReceiptPDF = Str.isPDF(filename); if (hasEReceipt) { - return {thumbnail: null, image: ROUTES.ERECEIPT.getRoute(transaction.transactionID), transaction, filename}; + return {image: ROUTES.ERECEIPT.getRoute(transaction.transactionID), transaction, filename}; } // For local files, we won't have a thumbnail yet if ((isReceiptImage || isReceiptPDF) && typeof path === 'string' && (path.startsWith('blob:') || path.startsWith('file:'))) { - return {thumbnail: null, image: path, isLocalFile: true, filename}; + return {image: path, isLocalFile: true, filename}; } if (isReceiptImage) { @@ -64,22 +56,9 @@ function getThumbnailAndImageURIs(transaction: OnyxEntry, receiptPa return {thumbnail: `${path.substring(0, path.length - 4)}.jpg.1024.jpg`, image: path, filename}; } - const {fileExtension} = FileUtils.splitExtensionFromFileName(filename) as FileNameAndExtension; - let image = ReceiptGeneric; - if (fileExtension === CONST.IOU.FILE_TYPES.HTML) { - image = ReceiptHTML; - } - - if (fileExtension === CONST.IOU.FILE_TYPES.DOC || fileExtension === CONST.IOU.FILE_TYPES.DOCX) { - image = ReceiptDoc; - } - - if (fileExtension === CONST.IOU.FILE_TYPES.SVG) { - image = ReceiptSVG; - } - const isLocalFile = typeof path === 'number' || path.startsWith('blob:') || path.startsWith('file:') || path.startsWith('/'); - return {thumbnail: image, image: path, isLocalFile, filename}; + const {fileExtension} = FileUtils.splitExtensionFromFileName(filename); + return {isThumbnail: true, fileExtension: Object.values(CONST.IOU.FILE_TYPES).find((type) => type === fileExtension), image: path, isLocalFile, filename}; } // eslint-disable-next-line import/prefer-default-export diff --git a/src/pages/TransactionReceiptPage.tsx b/src/pages/TransactionReceiptPage.tsx index 8db9e05a5139..f6f2c90e5d2c 100644 --- a/src/pages/TransactionReceiptPage.tsx +++ b/src/pages/TransactionReceiptPage.tsx @@ -28,7 +28,7 @@ type TransactionReceiptProps = TransactionReceiptOnyxProps & StackScreenProps): EReceiptColo return eReceiptColors[colorHash]; } +/** + * Helper method to return eReceipt color code for Receipt Thumbnails + */ +function getFileExtensionColorCode(fileExtension?: string): EReceiptColorName { + switch (fileExtension) { + case CONST.IOU.FILE_TYPES.DOC: + return CONST.ERECEIPT_COLORS.PINK; + case CONST.IOU.FILE_TYPES.HTML: + return CONST.ERECEIPT_COLORS.TANGERINE; + default: + return CONST.ERECEIPT_COLORS.GREEN; + } +} + /** * Helper method to return eReceipt color styles */ @@ -1139,6 +1153,7 @@ const staticStyleUtils = { parseStyleFromFunction, getEReceiptColorStyles, getEReceiptColorCode, + getFileExtensionColorCode, getNavigationModalCardStyle, getCardStyles, getOpacityStyle, diff --git a/src/styles/variables.ts b/src/styles/variables.ts index d911a2fc4b0e..50dc4cbd34fa 100644 --- a/src/styles/variables.ts +++ b/src/styles/variables.ts @@ -169,7 +169,7 @@ export default { addBankAccountLeftSpacing: 3, eReceiptThumbnailSmallBreakpoint: 110, eReceiptThumbnailMediumBreakpoint: 335, - eReceiptThumnailCenterReceiptBreakpoint: 200, + eReceiptThumbnailCenterReceiptBreakpoint: 200, eReceiptIconHeight: 100, eReceiptIconWidth: 72, eReceiptEmptyIconWidth: 76, diff --git a/src/types/onyx/Transaction.ts b/src/types/onyx/Transaction.ts index 1a7541955720..7bb83506035b 100644 --- a/src/types/onyx/Transaction.ts +++ b/src/types/onyx/Transaction.ts @@ -57,7 +57,7 @@ type Geometry = { type?: GeometryType; }; -type ReceiptSource = string | number; +type ReceiptSource = string; type Receipt = { receiptID?: number;