diff --git a/assets/images/receipt-placeholder-plus.svg b/assets/images/receipt-placeholder-plus.svg new file mode 100644 index 000000000000..3ebc08b40b06 --- /dev/null +++ b/assets/images/receipt-placeholder-plus.svg @@ -0,0 +1,17 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts index bd4bb64da050..09f9a3b896ad 100644 --- a/src/components/Icon/Expensicons.ts +++ b/src/components/Icon/Expensicons.ts @@ -154,6 +154,7 @@ import Printer from '@assets/images/printer.svg'; import Profile from '@assets/images/profile.svg'; import QrCode from '@assets/images/qrcode.svg'; import QuestionMark from '@assets/images/question-mark-circle.svg'; +import ReceiptPlaceholderPlus from '@assets/images/receipt-placeholder-plus.svg'; import ReceiptPlus from '@assets/images/receipt-plus.svg'; import ReceiptScan from '@assets/images/receipt-scan.svg'; import ReceiptSearch from '@assets/images/receipt-search.svg'; @@ -343,6 +344,7 @@ export { QrCode, QuestionMark, Receipt, + ReceiptPlaceholderPlus, ReceiptPlus, ReceiptScan, ReceiptSlash, diff --git a/src/components/ReceiptEmptyState.tsx b/src/components/ReceiptEmptyState.tsx index 71d64c7483f1..046026190a5b 100644 --- a/src/components/ReceiptEmptyState.tsx +++ b/src/components/ReceiptEmptyState.tsx @@ -1,5 +1,7 @@ import React from 'react'; +import {View} from 'react-native'; import useLocalize from '@hooks/useLocalize'; +import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import variables from '@styles/variables'; import Icon from './Icon'; @@ -14,12 +16,15 @@ type ReceiptEmptyStateProps = { onPress?: () => void; disabled?: boolean; + + isThumbnail?: boolean; }; // Returns an SVG icon indicating that the user should attach a receipt -function ReceiptEmptyState({hasError = false, onPress = () => {}, disabled = false}: ReceiptEmptyStateProps) { +function ReceiptEmptyState({hasError = false, onPress = () => {}, disabled = false, isThumbnail = false}: ReceiptEmptyStateProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); + const theme = useTheme(); return ( {}, disabled = fal onPress={onPress} disabled={disabled} disabledStyle={styles.cursorDefault} - style={[styles.alignItemsCenter, styles.justifyContentCenter, styles.moneyRequestViewImage, styles.moneyRequestAttachReceipt, hasError && styles.borderColorDanger]} + style={[ + styles.alignItemsCenter, + styles.justifyContentCenter, + styles.moneyRequestViewImage, + isThumbnail ? styles.moneyRequestAttachReceiptThumbnail : styles.moneyRequestAttachReceipt, + hasError && styles.borderColorDanger, + ]} > - + + + {!isThumbnail && ( + + )} + ); } diff --git a/src/components/ReceiptImage.tsx b/src/components/ReceiptImage.tsx index 8c980838b841..bca1ef8b74e6 100644 --- a/src/components/ReceiptImage.tsx +++ b/src/components/ReceiptImage.tsx @@ -7,6 +7,7 @@ import EReceiptThumbnail from './EReceiptThumbnail'; import type {IconSize} from './EReceiptThumbnail'; import Image from './Image'; import PDFThumbnail from './PDFThumbnail'; +import ReceiptEmptyState from './ReceiptEmptyState'; import ThumbnailImage from './ThumbnailImage'; type Style = {height: number; borderRadius: number; margin: number}; @@ -79,6 +80,11 @@ type ReceiptImageProps = ( /** The background color of fallback icon */ fallbackIconBackground?: string; + + isEmptyReceipt?: boolean; + + /** Callback to be called on pressing the image */ + onPress?: () => void; }; function ReceiptImage({ @@ -97,9 +103,21 @@ function ReceiptImage({ shouldUseInitialObjectPosition = false, fallbackIconColor, fallbackIconBackground, + isEmptyReceipt = false, + onPress, }: ReceiptImageProps) { const styles = useThemeStyles(); + if (isEmptyReceipt) { + return ( + + ); + } + if (isPDFThumbnail) { return ( { if (isCardTransaction) { @@ -326,6 +326,8 @@ function MoneyRequestPreviewContent({ } }; + const shouldDisableOnPress = isBillSplit && isEmptyObject(transaction); + const childContainer = ( - {hasReceipt && ( - - )} + {isEmptyObject(transaction) && !ReportActionsUtils.isMessageDeleted(action) && action.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE ? ( ) : ( - + @@ -484,8 +485,6 @@ function MoneyRequestPreviewContent({ return childContainer; } - const shouldDisableOnPress = isBillSplit && isEmptyObject(transaction); - return ( void; }; /** @@ -73,12 +78,14 @@ function ReportActionItemImage({ enablePreviewModal = false, transaction, isLocalFile = false, + isEmptyReceipt = false, fileExtension, filename, isSingleImage = true, readonly = false, shouldMapHaveBorderRadius, isFromReviewDuplicates = false, + onPress, }: ReportActionItemImageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -128,6 +135,8 @@ function ReportActionItemImage({ isAuthTokenRequired: false, source: thumbnail ?? image ?? '', shouldUseInitialObjectPosition: isDistanceRequest, + isEmptyReceipt, + onPress, }; } diff --git a/src/components/ReportActionItem/ReportActionItemImages.tsx b/src/components/ReportActionItem/ReportActionItemImages.tsx index f27596556c53..9995b7f77860 100644 --- a/src/components/ReportActionItem/ReportActionItemImages.tsx +++ b/src/components/ReportActionItem/ReportActionItemImages.tsx @@ -27,6 +27,9 @@ type ReportActionItemImagesProps = { /** if the corresponding report action item is hovered */ isHovered?: boolean; + + /** Callback to be called on onPress */ + onPress?: () => void; }; /** @@ -38,7 +41,7 @@ type ReportActionItemImagesProps = { * additional number when subtracted from size. */ -function ReportActionItemImages({images, size, total, isHovered = false}: ReportActionItemImagesProps) { +function ReportActionItemImages({images, size, total, isHovered = false, onPress}: ReportActionItemImagesProps) { const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -67,7 +70,7 @@ function ReportActionItemImages({images, size, total, isHovered = false}: Report - {shownImages.map(({thumbnail, isThumbnail, image, transaction, isLocalFile, fileExtension, filename}, index) => { + {shownImages.map(({thumbnail, isThumbnail, image, isEmptyReceipt, 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 : {}; @@ -81,11 +84,13 @@ function ReportActionItemImages({images, size, total, isHovered = false}: Report fileExtension={fileExtension} image={image} isLocalFile={isLocalFile} + isEmptyReceipt={isEmptyReceipt} filename={filename} transaction={transaction} isThumbnail={isThumbnail} isSingleImage={numberOfShownImages === 1} shouldMapHaveBorderRadius={false} + onPress={onPress} /> ); diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index 63edd29ef4c3..19ab01a27c57 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -173,8 +173,8 @@ function ReportPreview({ ReportUtils.hasWarningTypeViolations(iouReportID, transactionViolations, true) || (ReportUtils.isReportOwner(iouReport) && ReportUtils.hasReportViolations(iouReportID)) || ReportUtils.hasActionsWithErrors(iouReportID); - const lastThreeTransactionsWithReceipts = transactionsWithReceipts.slice(-3); - const lastThreeReceipts = lastThreeTransactionsWithReceipts.map((transaction) => ({...ReceiptUtils.getThumbnailAndImageURIs(transaction), transaction})); + const lastThreeTransactions = allTransactions.slice(-3); + const lastThreeReceipts = lastThreeTransactions.map((transaction) => ({...ReceiptUtils.getThumbnailAndImageURIs(transaction), transaction})); const showRTERViolationMessage = numberOfRequests === 1 && TransactionUtils.hasPendingUI(allTransactions.at(0), TransactionUtils.getTransactionViolations(allTransactions.at(0)?.transactionID ?? '-1', transactionViolations)); @@ -448,6 +448,12 @@ function ReportPreview({ checkMarkScale.set(isPaidAnimationRunning ? withDelay(CONST.ANIMATION_PAID_CHECKMARK_DELAY, withSpring(1, {duration: CONST.ANIMATION_PAID_DURATION})) : 1); }, [isPaidAnimationRunning, iouSettled, checkMarkScale]); + const openReportFromPreview = useCallback(() => { + Performance.markStart(CONST.TIMING.OPEN_REPORT_FROM_PREVIEW); + Timing.start(CONST.TIMING.OPEN_REPORT_FROM_PREVIEW); + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(iouReportID)); + }, [iouReportID]); + return ( { - Performance.markStart(CONST.TIMING.OPEN_REPORT_FROM_PREVIEW); - Timing.start(CONST.TIMING.OPEN_REPORT_FROM_PREVIEW); - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(iouReportID)); - }} + onPress={openReportFromPreview} onPressIn={() => DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()} onPressOut={() => ControlSelection.unblock()} onLongPress={(event) => showContextMenuForReport(event, contextMenuAnchor, chatReportID, action, checkIfContextMenuActive)} @@ -470,13 +472,12 @@ function ReportPreview({ accessibilityLabel={translate('iou.viewDetails')} > - {hasReceipts && ( - - )} + diff --git a/src/libs/ReceiptUtils.ts b/src/libs/ReceiptUtils.ts index ed7def2ed1ec..5085f0e2384e 100644 --- a/src/libs/ReceiptUtils.ts +++ b/src/libs/ReceiptUtils.ts @@ -16,6 +16,7 @@ type ThumbnailAndImageURI = { isThumbnail?: boolean; filename?: string; fileExtension?: string; + isEmptyReceipt?: boolean; }; /** @@ -26,6 +27,9 @@ type ThumbnailAndImageURI = { * @param receiptFileName */ function getThumbnailAndImageURIs(transaction: OnyxEntry, receiptPath: ReceiptSource | null = null, receiptFileName: string | null = null): ThumbnailAndImageURI { + if (!TransactionUtils.hasReceipt(transaction) && !receiptPath && !receiptFileName) { + return {isEmptyReceipt: true}; + } if (TransactionUtils.isFetchingWaypointsFromServer(transaction)) { return {isThumbnail: true, isLocalFile: true}; } diff --git a/src/styles/index.ts b/src/styles/index.ts index 6dbdb715a427..2514d4832b5b 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -4647,6 +4647,21 @@ const styles = (theme: ThemeColors) => borderWidth: 1, }, + moneyRequestAttachReceiptThumbnail: { + backgroundColor: theme.hoverComponentBG, + width: '100%', + borderWidth: 0, + }, + + moneyRequestAttachReceiptThumbnailIcon: { + position: 'absolute', + bottom: -4, + right: -4, + borderColor: theme.highlightBG, + borderWidth: 2, + borderRadius: '50%', + }, + mapViewContainer: { ...flex.flex1, minHeight: 300, diff --git a/src/styles/variables.ts b/src/styles/variables.ts index 8b330ce1dd8d..c8a6f7025912 100644 --- a/src/styles/variables.ts +++ b/src/styles/variables.ts @@ -179,7 +179,7 @@ export default { eReceiptThumbnailCenterReceiptBreakpoint: 200, eReceiptIconHeight: 100, eReceiptIconWidth: 72, - eReceiptEmptyIconWidth: 76, + eReceiptEmptyIconWidth: 64, eReceiptMCCHeightWidth: 40, eReceiptIconHeightSmall: 65, eReceiptIconWidthSmall: 46,