diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 398b9ac6ba4f..e4505af81721 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -286,6 +286,11 @@ export default { I_AM_A_TEACHER: 'teachersunite/i-am-a-teacher', INTRO_SCHOOL_PRINCIPAL: 'teachersunite/intro-school-principal', + ERECEIPT: { + route: 'eReceipt/:transactionID', + getRoute: (transactionID: string) => `eReceipt/${transactionID}`, + }, + WORKSPACE_NEW: 'workspace/new', WORKSPACE_NEW_ROOM: 'workspace/new-room', WORKSPACE_INITIAL: { diff --git a/src/components/Attachments/AttachmentCarousel/extractAttachmentsFromReport.js b/src/components/Attachments/AttachmentCarousel/extractAttachmentsFromReport.js index 8a623a44709f..dae0191b2158 100644 --- a/src/components/Attachments/AttachmentCarousel/extractAttachmentsFromReport.js +++ b/src/components/Attachments/AttachmentCarousel/extractAttachmentsFromReport.js @@ -53,7 +53,7 @@ function extractAttachmentsFromReport(report, reportActions) { const transaction = TransactionUtils.getTransaction(transactionID); if (TransactionUtils.hasReceipt(transaction)) { - const {image} = ReceiptUtils.getThumbnailAndImageURIs(transaction.receipt.source, transaction.filename); + const {image} = ReceiptUtils.getThumbnailAndImageURIs(transaction); attachments.unshift({ source: tryResolveUrlFromApiRoot(image), isAuthTokenRequired: true, diff --git a/src/components/Attachments/AttachmentView/index.js b/src/components/Attachments/AttachmentView/index.js index a1b07fb99dd8..34ff45160ce9 100755 --- a/src/components/Attachments/AttachmentView/index.js +++ b/src/components/Attachments/AttachmentView/index.js @@ -1,5 +1,5 @@ import React, {memo, useState} from 'react'; -import {View, ActivityIndicator} from 'react-native'; +import {View, ScrollView, ActivityIndicator} from 'react-native'; import _ from 'underscore'; import PropTypes from 'prop-types'; import Str from 'expensify-common/lib/str'; @@ -22,6 +22,7 @@ import * as TransactionUtils from '../../../libs/TransactionUtils'; import DistanceEReceipt from '../../DistanceEReceipt'; import useNetwork from '../../../hooks/useNetwork'; import ONYXKEYS from '../../../ONYXKEYS'; +import EReceipt from '../../EReceipt'; const propTypes = { ...attachmentViewPropTypes, @@ -101,6 +102,19 @@ function AttachmentView({ ); } + if (TransactionUtils.hasEReceipt(transaction)) { + return ( + + + + + + ); + } + // Check both source and file.name since PDFs dragged into the text field // will appear with a source that is a blob if ((_.isString(source) && Str.isPDF(source)) || (file && Str.isPDF(file.name || translate('attachmentView.unknownFilename')))) { diff --git a/src/components/EReceipt.js b/src/components/EReceipt.js index e6b3a9809c7e..84daabb96c9b 100644 --- a/src/components/EReceipt.js +++ b/src/components/EReceipt.js @@ -59,7 +59,7 @@ function EReceipt({transaction, transactionID}) { - + {currency} diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js index 42fa1db48220..fefacc385116 100755 --- a/src/components/MoneyRequestConfirmationList.js +++ b/src/components/MoneyRequestConfirmationList.js @@ -536,8 +536,7 @@ function MoneyRequestConfirmationList(props) { ); }, [confirm, props.bankAccountRoute, props.iouCurrencyCode, props.iouType, props.isReadOnly, props.policyID, selectedParticipants, splitOrRequestOptions, translate, formError]); - const {image: receiptImage, thumbnail: receiptThumbnail} = - props.receiptPath && props.receiptFilename ? ReceiptUtils.getThumbnailAndImageURIs(props.receiptPath, props.receiptFilename) : {}; + const {image: receiptImage, thumbnail: receiptThumbnail} = props.receiptPath && props.receiptFilename ? ReceiptUtils.getThumbnailAndImageURIs(transaction) : {}; return ( { if (isExpensifyCardTransaction) { diff --git a/src/components/ReportActionItem/MoneyRequestView.js b/src/components/ReportActionItem/MoneyRequestView.js index 289cd70c3332..707ef419d8b3 100644 --- a/src/components/ReportActionItem/MoneyRequestView.js +++ b/src/components/ReportActionItem/MoneyRequestView.js @@ -151,7 +151,7 @@ function MoneyRequestView({report, betas, parentReport, policyCategories, should let receiptURIs; let hasErrors = false; if (hasReceipt) { - receiptURIs = ReceiptUtils.getThumbnailAndImageURIs(transaction.receipt.source, transaction.filename); + receiptURIs = ReceiptUtils.getThumbnailAndImageURIs(transaction); hasErrors = canEdit && TransactionUtils.hasMissingSmartscanFields(transaction); } @@ -170,6 +170,7 @@ function MoneyRequestView({report, betas, parentReport, policyCategories, should diff --git a/src/components/ReportActionItem/ReportActionItemImage.js b/src/components/ReportActionItem/ReportActionItemImage.js index 98bdede0fe26..f17a1f1929fe 100644 --- a/src/components/ReportActionItem/ReportActionItemImage.js +++ b/src/components/ReportActionItem/ReportActionItemImage.js @@ -1,5 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; +import {View} from 'react-native'; +import _ from 'underscore'; import styles from '../../styles/styles'; import Image from '../Image'; import ThumbnailImage from '../ThumbnailImage'; @@ -10,6 +12,9 @@ import {ShowContextMenuContext} from '../ShowContextMenuContext'; import Navigation from '../../libs/Navigation/Navigation'; import PressableWithoutFocus from '../Pressable/PressableWithoutFocus'; import useLocalize from '../../hooks/useLocalize'; +import EReceiptThumbnail from '../EReceiptThumbnail'; +import transactionPropTypes from '../transactionPropTypes'; +import * as TransactionUtils from '../../libs/TransactionUtils'; const propTypes = { /** thumbnail URI for the image */ @@ -20,10 +25,14 @@ const propTypes = { /** whether or not to enable the image preview modal */ enablePreviewModal: PropTypes.bool, + + /* The transaction associated with this image, if any. Passed for handling eReceipts. */ + transaction: transactionPropTypes, }; const defaultProps = { thumbnail: null, + transaction: {}, enablePreviewModal: false, }; @@ -33,24 +42,37 @@ const defaultProps = { * and optional preview modal as well. */ -function ReportActionItemImage({thumbnail, image, enablePreviewModal}) { +function ReportActionItemImage({thumbnail, image, enablePreviewModal, transaction}) { const {translate} = useLocalize(); const imageSource = tryResolveUrlFromApiRoot(image || ''); const thumbnailSource = tryResolveUrlFromApiRoot(thumbnail || ''); + const isEReceipt = !_.isEmpty(transaction) && TransactionUtils.hasEReceipt(transaction); + + let receiptImageComponent; - const receiptImageComponent = thumbnail ? ( - - ) : ( - - ); + if (isEReceipt) { + receiptImageComponent = ( + + + + ); + } else if (thumbnail) { + receiptImageComponent = ( + + ); + } else { + receiptImageComponent = ( + + ); + } if (enablePreviewModal) { return ( diff --git a/src/components/ReportActionItem/ReportActionItemImages.js b/src/components/ReportActionItem/ReportActionItemImages.js index 773c66d6e7b6..bd1ee6d45a07 100644 --- a/src/components/ReportActionItem/ReportActionItemImages.js +++ b/src/components/ReportActionItem/ReportActionItemImages.js @@ -7,6 +7,7 @@ import Text from '../Text'; import ReportActionItemImage from './ReportActionItemImage'; import * as StyleUtils from '../../styles/StyleUtils'; import variables from '../../styles/variables'; +import transactionPropTypes from '../transactionPropTypes'; const propTypes = { /** array of image and thumbnail URIs */ @@ -14,6 +15,7 @@ const propTypes = { PropTypes.shape({ thumbnail: PropTypes.string, image: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + transaction: transactionPropTypes, }), ).isRequired, @@ -68,7 +70,7 @@ function ReportActionItemImages({images, size, total, isHovered}) { return ( - {_.map(shownImages, ({thumbnail, image}, index) => { + {_.map(shownImages, ({thumbnail, image, transaction}, index) => { const isLastImage = index === numberOfShownImages - 1; // Show a border to separate multiple images. Shown to the right for each except the last. @@ -82,6 +84,7 @@ function ReportActionItemImages({images, size, total, isHovered}) { {isLastImage && remaining > 0 && ( diff --git a/src/components/ReportActionItem/ReportPreview.js b/src/components/ReportActionItem/ReportPreview.js index d4d839183e07..82fbf0cb9195 100644 --- a/src/components/ReportActionItem/ReportPreview.js +++ b/src/components/ReportActionItem/ReportPreview.js @@ -125,8 +125,8 @@ function ReportPreview(props) { const hasReceipts = transactionsWithReceipts.length > 0; const isScanning = hasReceipts && ReportUtils.areAllRequestsBeingSmartScanned(props.iouReportID, props.action); const hasErrors = hasReceipts && ReportUtils.hasMissingSmartscanFields(props.iouReportID); - const lastThreeTransactionsWithReceipts = ReportUtils.getReportPreviewDisplayTransactions(props.action); - const lastThreeReceipts = _.map(lastThreeTransactionsWithReceipts, ({receipt, filename}) => ReceiptUtils.getThumbnailAndImageURIs(receipt.source, filename || '')); + const lastThreeTransactionsWithReceipts = transactionsWithReceipts.slice(-3); + const lastThreeReceipts = _.map(lastThreeTransactionsWithReceipts, (transaction) => ReceiptUtils.getThumbnailAndImageURIs(transaction)); const hasNonReimbursableTransactions = ReportUtils.hasNonReimbursableTransactions(props.iouReportID); const hasOnlyOneReceiptRequest = numberOfRequests === 1 && hasReceipts; const previewSubtitle = hasOnlyOneReceiptRequest diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index 8df554dd4dbf..9ac1362e3e47 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -47,7 +47,7 @@ function getCardDescription(cardID: number) { return ''; } const cardDescriptor = card.state === CONST.EXPENSIFY_CARD.STATE.NOT_ACTIVATED ? Localize.translateLocal('cardTransactions.notActivated') : card.lastFourPAN; - return `${card.bank} - ${cardDescriptor}`; + return cardDescriptor ? `${card.bank} - ${cardDescriptor}` : `${card.bank}`; } /** diff --git a/src/libs/ReceiptUtils.ts b/src/libs/ReceiptUtils.ts index cdc45cb119d5..9fa7ebdc6559 100644 --- a/src/libs/ReceiptUtils.ts +++ b/src/libs/ReceiptUtils.ts @@ -6,10 +6,13 @@ import ReceiptHTML from '../../assets/images/receipt-html.png'; import ReceiptDoc from '../../assets/images/receipt-doc.png'; import ReceiptGeneric from '../../assets/images/receipt-generic.png'; import ReceiptSVG from '../../assets/images/receipt-svg.png'; +import {Transaction} from '../types/onyx'; +import ROUTES from '../ROUTES'; type ThumbnailAndImageURI = { image: ImageSourcePropType | string; thumbnail: string | null; + transaction?: Transaction; }; type FileNameAndExtension = { @@ -20,12 +23,21 @@ type FileNameAndExtension = { /** * Grab the appropriate receipt image and thumbnail URIs based on file type * - * @param path URI to image, i.e. blob:new.expensify.com/9ef3a018-4067-47c6-b29f-5f1bd35f213d or expensify.com/receipts/w_e616108497ef940b7210ec6beb5a462d01a878f4.jpg - * @param filename of uploaded image or last part of remote URI + * @param transaction */ -function getThumbnailAndImageURIs(path: string, filename: string): ThumbnailAndImageURI { +function getThumbnailAndImageURIs(transaction: Transaction): ThumbnailAndImageURI { + // URI to image, i.e. blob:new.expensify.com/9ef3a018-4067-47c6-b29f-5f1bd35f213d or expensify.com/receipts/w_e616108497ef940b7210ec6beb5a462d01a878f4.jpg + const path = transaction?.receipt?.source ?? ''; + // filename of uploaded image or last part of remote URI + const filename = transaction?.filename ?? ''; const isReceiptImage = Str.isImage(filename); + const hasEReceipt = transaction?.hasEReceipt; + + if (hasEReceipt) { + return {thumbnail: null, image: ROUTES.ERECEIPT.getRoute(transaction.transactionID), transaction}; + } + // For local files, we won't have a thumbnail yet if (isReceiptImage && (path.startsWith('blob:') || path.startsWith('file:'))) { return {thumbnail: null, image: path}; diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 0b7bbfd61461..e5994b4e0c94 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -3800,29 +3800,6 @@ function getParticipantsIDs(report) { return participants; } -/** - * Get the last 3 transactions with receipts of an IOU report that will be displayed on the report preview - * - * @param {Object} reportPreviewAction - * @returns {Object} - */ -function getReportPreviewDisplayTransactions(reportPreviewAction) { - const transactionIDs = lodashGet(reportPreviewAction, ['childRecentReceiptTransactionIDs']); - return _.reduce( - _.keys(transactionIDs), - (transactions, transactionID) => { - if (transactionIDs[transactionID] !== null) { - const transaction = TransactionUtils.getTransaction(transactionID); - if (TransactionUtils.hasReceipt(transaction)) { - transactions.push(transaction); - } - } - return transactions; - }, - [], - ); -} - /** * Return iou report action display message * @@ -4010,7 +3987,6 @@ export { canEditMoneyRequest, buildTransactionThread, areAllRequestsBeingSmartScanned, - getReportPreviewDisplayTransactions, getTransactionsWithReceipts, hasNonReimbursableTransactions, hasMissingSmartscanFields, diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index 77fc4f04f99d..6a45bef5780b 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -76,8 +76,15 @@ function buildOptimisticTransaction( }; } +/** + * Check if the transaction has an Ereceipt + */ +function hasEReceipt(transaction: Transaction | undefined | null): boolean { + return !!transaction?.hasEReceipt; +} + function hasReceipt(transaction: Transaction | undefined | null): boolean { - return !!transaction?.receipt?.state; + return !!transaction?.receipt?.state || hasEReceipt(transaction); } function isMerchantMissing(transaction: Transaction) { @@ -365,13 +372,6 @@ function hasRoute(transaction: Transaction): boolean { return !!transaction?.routes?.route0?.geometry?.coordinates; } -/** - * Check if the transaction has an Ereceipt - */ -function hasEreceipt(transaction: Transaction): boolean { - return !!transaction?.hasEReceipt; -} - /** * Get the transactions related to a report preview with receipts * Get the details linked to the IOU reportAction @@ -472,7 +472,7 @@ export { getLinkedTransaction, getAllReportTransactions, hasReceipt, - hasEreceipt, + hasEReceipt, hasRoute, isReceiptBeingScanned, getValidWaypoints, diff --git a/src/stories/EReceipt.stories.js b/src/stories/EReceipt.stories.js index 3099e0f4a128..56a79e30980b 100644 --- a/src/stories/EReceipt.stories.js +++ b/src/stories/EReceipt.stories.js @@ -13,6 +13,7 @@ const transactionData = { merchant: 'United Airlines', mccGroup: 'Goods', created: '2023-07-24 13:46:20', + hasEReceipt: true, }, [`${ONYXKEYS.COLLECTION.TRANSACTION}FAKE_2`]: { transactionID: 'FAKE_2', @@ -22,6 +23,7 @@ const transactionData = { merchant: 'United Airlines', mccGroup: 'Airlines', created: '2023-07-24 13:46:20', + hasEReceipt: true, }, [`${ONYXKEYS.COLLECTION.TRANSACTION}FAKE_3`]: { transactionID: 'FAKE_3', @@ -31,6 +33,7 @@ const transactionData = { merchant: 'United Airlines', mccGroup: 'Commuter', created: '2023-07-24 13:46:20', + hasEReceipt: true, }, [`${ONYXKEYS.COLLECTION.TRANSACTION}FAKE_4`]: {transactionID: 'FAKE_4', amount: 444444, currency: 'USD', cardID: 4, merchant: 'Chevron', mccGroup: 'Gas', created: '2023-07-24 13:46:20'}, [`${ONYXKEYS.COLLECTION.TRANSACTION}FAKE_5`]: { @@ -41,6 +44,7 @@ const transactionData = { merchant: 'Barnes and Noble', mccGroup: 'Goods', created: '2022-03-21 13:46:20', + hasEReceipt: true, }, [`${ONYXKEYS.COLLECTION.TRANSACTION}FAKE_6`]: { transactionID: 'FAKE_6', @@ -50,6 +54,7 @@ const transactionData = { merchant: 'Trader Joes', mccGroup: 'Groceries', created: '2023-12-24 13:46:20', + hasEReceipt: true, }, [`${ONYXKEYS.COLLECTION.TRANSACTION}FAKE_7`]: { transactionID: 'FAKE_7', @@ -59,6 +64,7 @@ const transactionData = { merchant: "Linda's Place", mccGroup: 'Hotel', created: '2023-03-24 13:46:20', + hasEReceipt: true, }, [`${ONYXKEYS.COLLECTION.TRANSACTION}FAKE_8`]: { transactionID: 'FAKE_8', @@ -68,6 +74,7 @@ const transactionData = { merchant: 'United Post Office', mccGroup: 'Mail', created: '2023-09-24 13:46:20', + hasEReceipt: true, }, [`${ONYXKEYS.COLLECTION.TRANSACTION}FAKE_9`]: { transactionID: 'FAKE_9', @@ -77,6 +84,7 @@ const transactionData = { merchant: 'Dishoom', mccGroup: 'Meals', created: '2023-07-24 13:46:20', + hasEReceipt: true, }, [`${ONYXKEYS.COLLECTION.TRANSACTION}FAKE_10`]: { transactionID: 'FAKE_10', @@ -86,6 +94,7 @@ const transactionData = { merchant: 'Hertz', mccGroup: 'Rental', created: '2023-07-24 13:46:20', + hasEReceipt: true, }, [`${ONYXKEYS.COLLECTION.TRANSACTION}FAKE_11`]: { transactionID: 'FAKE_11', @@ -95,6 +104,7 @@ const transactionData = { merchant: 'Laundromat', mccGroup: 'Services', created: '2023-07-24 13:46:20', + hasEReceipt: true, }, [`${ONYXKEYS.COLLECTION.TRANSACTION}FAKE_12`]: {transactionID: 'FAKE_12', amount: 1000, currency: 'USD', cardID: 4, merchant: 'Uber', mccGroup: 'Taxi', created: '2023-07-24 13:46:20'}, [`${ONYXKEYS.COLLECTION.TRANSACTION}FAKE_13`]: { @@ -105,6 +115,7 @@ const transactionData = { merchant: 'Pirate Party Store', mccGroup: 'Miscellaneous', created: '2023-10-31 13:46:20', + hasEReceipt: true, }, [`${ONYXKEYS.COLLECTION.TRANSACTION}FAKE_14`]: { transactionID: 'FAKE_14', @@ -123,6 +134,7 @@ const transactionData = { merchant: 'Invalid MCC', mccGroup: 'invalidMCC', created: '2023-01-11 13:46:20', + hasEReceipt: true, }, [`${ONYXKEYS.COLLECTION.TRANSACTION}FAKE_16`]: { transactionID: 'FAKE_16', @@ -132,6 +144,7 @@ const transactionData = { merchant: 'This is a very very very very very very very very long merchant name, why would you ever shop at a store with a sign this long?', mccGroup: 'invalidMCC', created: '2023-01-11 13:46:20', + hasEReceipt: true, }, }; diff --git a/src/stories/ReportActionItemImages.stories.js b/src/stories/ReportActionItemImages.stories.js index e619cc2ee143..b776d9261e60 100644 --- a/src/stories/ReportActionItemImages.stories.js +++ b/src/stories/ReportActionItemImages.stories.js @@ -38,6 +38,78 @@ Default.args = { total: 1, }; +const DisplayEReceipt = Template.bind({}); +DisplayEReceipt.args = { + images: [ + { + image: 'eReceipt/FAKE_3', + thumbnail: '', + transaction: { + transactionID: 'FAKE_3', + amount: 1000, + currency: 'USD', + cardID: 5, + merchant: 'United Airlines', + mccGroup: 'Commuter', + created: '2023-07-24 13:46:20', + hasEReceipt: true, + }, + }, + ], + size: 1, + total: 1, +}; + +const DisplayMultipleEReceipts = Template.bind({}); +DisplayMultipleEReceipts.args = { + images: [ + { + image: 'eReceipt/FAKE_3', + thumbnail: '', + transaction: { + transactionID: 'FAKE_3', + amount: 1000, + currency: 'USD', + cardID: 5, + merchant: 'United Airlines', + mccGroup: 'Commuter', + created: '2023-07-24 13:46:20', + hasEReceipt: true, + }, + }, + { + image: 'eReceipt/FAKE_5', + thumbnail: '', + transaction: { + transactionID: 'FAKE_5', + amount: 230440, + currency: 'USD', + cardID: 4, + merchant: 'Barnes and Noble', + mccGroup: 'Goods', + created: '2022-03-21 13:46:20', + hasEReceipt: true, + }, + }, + { + image: 'eReceipt/FAKE_2', + thumbnail: '', + transaction: { + transactionID: 'FAKE_2', + amount: 1000, + currency: 'USD', + cardID: 4, + merchant: 'United Airlines', + mccGroup: 'Airlines', + created: '2023-07-24 13:46:20', + hasEReceipt: true, + }, + }, + ], + size: 3, + total: 3, +}; + const TwoImages = Template.bind({}); TwoImages.args = { images: [ @@ -139,4 +211,4 @@ ThreePlusTenImages.args = { }; export default story; -export {Default, TwoImages, ThreeImages, FourImages, ThreePlusTwoImages, ThreePlusTenImages}; +export {Default, TwoImages, ThreeImages, FourImages, ThreePlusTwoImages, ThreePlusTenImages, DisplayEReceipt, DisplayMultipleEReceipts}; diff --git a/src/styles/styles.ts b/src/styles/styles.ts index 749a9e7aedcf..6346cc42e03f 100644 --- a/src/styles/styles.ts +++ b/src/styles/styles.ts @@ -3339,16 +3339,12 @@ const styles = (theme: ThemeDefault) => eReceiptAmountLarge: { ...headlineFont, fontSize: variables.fontSizeEReceiptLarge, - lineHeight: variables.lineHeightXXLarge, - wordBreak: 'break-word', textAlign: 'center', }, eReceiptCurrency: { ...headlineFont, fontSize: variables.fontSizeXXLarge, - lineHeight: variables.lineHeightXXLarge, - wordBreak: 'break-all', }, eReceiptMerchant: { @@ -3406,7 +3402,6 @@ const styles = (theme: ThemeDefault) => }, eReceiptContainer: { - flex: 1, width: 335, minHeight: 540, borderRadius: 20, diff --git a/src/styles/variables.ts b/src/styles/variables.ts index e7efcf4052d4..ea0af11d1b7a 100644 --- a/src/styles/variables.ts +++ b/src/styles/variables.ts @@ -167,6 +167,7 @@ export default { eReceiptWordmarkWidth: 86, eReceiptBGHeight: 540, eReceiptBGHWidth: 335, + eReceiptTextContainerWidth: 263, reportPreviewMaxWidth: 335, reportActionImagesSingleImageHeight: 147, reportActionImagesDoubleImageHeight: 138,