diff --git a/src/CONST.js b/src/CONST.js index e7d2fa6f4d03..e8c95b83f57e 100755 --- a/src/CONST.js +++ b/src/CONST.js @@ -1029,6 +1029,9 @@ const CONST = { AMOUNT_MAX_LENGTH: 10, RECEIPT_STATE: { SCANREADY: 'SCANREADY', + SCANNING: 'SCANNING', + SCANCOMPLETE: 'SCANCOMPLETE', + SCANFAILED: 'SCANFAILED', }, FILE_TYPES: { HTML: 'html', diff --git a/src/ONYXKEYS.js b/src/ONYXKEYS.js index 5a00c49db0a0..27e7f9f0ecf3 100755 --- a/src/ONYXKEYS.js +++ b/src/ONYXKEYS.js @@ -33,7 +33,7 @@ export default { // Credentials to authenticate the user CREDENTIALS: 'credentials', - // Contains loading data for the IOU feature (MoneyRequestModal, IOUDetail, & IOUPreview Components) + // Contains loading data for the IOU feature (MoneyRequestModal, IOUDetail, & MoneyRequestPreview Components) IOU: 'iou', // Keeps track if there is modal currently visible or not diff --git a/src/components/AttachmentModal.js b/src/components/AttachmentModal.js index 57bf3218abbc..c07a4474a68b 100755 --- a/src/components/AttachmentModal.js +++ b/src/components/AttachmentModal.js @@ -98,6 +98,7 @@ function AttachmentModal(props) { const [shouldLoadAttachment, setShouldLoadAttachment] = useState(false); const [isAttachmentInvalid, setIsAttachmentInvalid] = useState(false); const [isAuthTokenRequired, setIsAuthTokenRequired] = useState(props.isAuthTokenRequired); + const [isAttachmentReceipt, setIsAttachmentReceipt] = useState(false); const [attachmentInvalidReasonTitle, setAttachmentInvalidReasonTitle] = useState(''); const [attachmentInvalidReason, setAttachmentInvalidReason] = useState(null); const [source, setSource] = useState(props.source); @@ -118,12 +119,13 @@ function AttachmentModal(props) { /** * Keeps the attachment source in sync with the attachment displayed currently in the carousel. - * @param {{ source: String, isAuthTokenRequired: Boolean, file: { name: string } }} attachment + * @param {{ source: String, isAuthTokenRequired: Boolean, file: { name: string }, isReceipt: Boolean }} attachment */ const onNavigate = useCallback( (attachment) => { setSource(attachment.source); setFile(attachment.file); + setIsAttachmentReceipt(attachment.isReceipt); setIsAuthTokenRequired(attachment.isAuthTokenRequired); onCarouselAttachmentChange(attachment); }, @@ -314,6 +316,7 @@ function AttachmentModal(props) { }, []); const sourceForAttachmentView = props.source || source; + return ( <> {props.isSmallScreenWidth && } downloadAttachment(source)} diff --git a/src/components/Attachments/AttachmentCarousel/extractAttachmentsFromReport.js b/src/components/Attachments/AttachmentCarousel/extractAttachmentsFromReport.js index f4df0fc0c3e2..b967d5ab0066 100644 --- a/src/components/Attachments/AttachmentCarousel/extractAttachmentsFromReport.js +++ b/src/components/Attachments/AttachmentCarousel/extractAttachmentsFromReport.js @@ -1,6 +1,9 @@ import {Parser as HtmlParser} from 'htmlparser2'; import _ from 'underscore'; +import lodashGet from 'lodash/get'; import * as ReportActionsUtils from '../../../libs/ReportActionsUtils'; +import * as TransactionUtils from '../../../libs/TransactionUtils'; +import * as ReceiptUtils from '../../../libs/ReceiptUtils'; import CONST from '../../../CONST'; import tryResolveUrlFromApiRoot from '../../../libs/tryResolveUrlFromApiRoot'; @@ -28,6 +31,7 @@ function extractAttachmentsFromReport(report, reportActions) { source: tryResolveUrlFromApiRoot(expensifySource || attribs.src), isAuthTokenRequired: Boolean(expensifySource), file: {name: attribs[CONST.ATTACHMENT_ORIGINAL_FILENAME_ATTRIBUTE]}, + isReceipt: false, }); }, }); @@ -36,6 +40,28 @@ function extractAttachmentsFromReport(report, reportActions) { if (!ReportActionsUtils.shouldReportActionBeVisible(action, key)) { return; } + + // We're handling receipts differently here because receipt images are not + // part of the report action message, the images are constructed client-side + if (ReportActionsUtils.isMoneyRequestAction(action)) { + const transactionID = lodashGet(action, ['originalMessage', 'IOUTransactionID']); + if (!transactionID) { + return; + } + + const transaction = TransactionUtils.getTransaction(transactionID); + if (TransactionUtils.hasReceipt(transaction)) { + const {image} = ReceiptUtils.getThumbnailAndImageURIs(transaction.receipt.source, transaction.filename); + attachments.unshift({ + source: tryResolveUrlFromApiRoot(image), + isAuthTokenRequired: true, + file: {name: transaction.filename}, + isReceipt: true, + }); + return; + } + } + htmlParser.write(_.get(action, ['message', 0, 'html'])); }); htmlParser.end(); diff --git a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.js b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.js index 5417f7af6820..e156c8bda3f4 100755 --- a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.js +++ b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.js @@ -48,7 +48,7 @@ const customHTMLElementModels = { 'mention-here': defaultHTMLElementModels.span.extend({tagName: 'mention-here'}), }; -const defaultViewProps = {style: [styles.alignItemsStart, styles.userSelectText]}; +const defaultViewProps = {style: [styles.alignItemsStart, styles.userSelectText, styles.w100, styles.h100]}; // We are using the explicit composite architecture for performance gains. // Configuration for RenderHTML is handled in a top-level component providing diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.js b/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.js index 103e653d7d88..2f4ee7780346 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.js +++ b/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.js @@ -33,29 +33,33 @@ function ImageRenderer(props) { // Concierge responder attachments are uploaded to S3 without any access // control and thus require no authToken to verify access. // - const isAttachment = Boolean(htmlAttribs[CONST.ATTACHMENT_SOURCE_ATTRIBUTE]); + const isAttachmentOrReceipt = Boolean(htmlAttribs[CONST.ATTACHMENT_SOURCE_ATTRIBUTE]); // Files created/uploaded/hosted by App should resolve from API ROOT. Other URLs aren't modified const previewSource = tryResolveUrlFromApiRoot(htmlAttribs.src); - const source = tryResolveUrlFromApiRoot(isAttachment ? htmlAttribs[CONST.ATTACHMENT_SOURCE_ATTRIBUTE] : htmlAttribs.src); + const source = tryResolveUrlFromApiRoot(isAttachmentOrReceipt ? htmlAttribs[CONST.ATTACHMENT_SOURCE_ATTRIBUTE] : htmlAttribs.src); const imageWidth = htmlAttribs['data-expensify-width'] ? parseInt(htmlAttribs['data-expensify-width'], 10) : undefined; const imageHeight = htmlAttribs['data-expensify-height'] ? parseInt(htmlAttribs['data-expensify-height'], 10) : undefined; const imagePreviewModalDisabled = htmlAttribs['data-expensify-preview-modal-disabled'] === 'true'; + const shouldFitContainer = htmlAttribs['data-expensify-fit-container'] === 'true'; + const sizingStyle = shouldFitContainer ? [styles.w100, styles.h100] : []; + return imagePreviewModalDisabled ? ( ) : ( {({anchor, report, action, checkIfContextMenuActive}) => ( { const route = ROUTES.getReportAttachmentRoute(report.reportID, source); Navigation.navigate(route); @@ -66,10 +70,11 @@ function ImageRenderer(props) { > )} diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js index 832be75c1317..e8b792a620c0 100755 --- a/src/components/MoneyRequestConfirmationList.js +++ b/src/components/MoneyRequestConfirmationList.js @@ -4,7 +4,6 @@ import {withOnyx} from 'react-native-onyx'; import {format} from 'date-fns'; import _ from 'underscore'; import {View} from 'react-native'; -import Str from 'expensify-common/lib/str'; import styles from '../styles/styles'; import * as ReportUtils from '../libs/ReportUtils'; import * as OptionsListUtils from '../libs/OptionsListUtils'; @@ -26,12 +25,8 @@ import Button from './Button'; import * as Expensicons from './Icon/Expensicons'; import themeColors from '../styles/themes/default'; import Image from './Image'; -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 * as FileUtils from '../libs/fileDownload/FileUtils'; import useLocalize from '../hooks/useLocalize'; +import * as ReceiptUtils from '../libs/ReceiptUtils'; const propTypes = { /** Callback to inform parent modal of success */ @@ -319,36 +314,6 @@ function MoneyRequestConfirmationList(props) { ); }, [confirm, props.selectedParticipants, props.bankAccountRoute, props.iouCurrencyCode, props.iouType, props.isReadOnly, props.policyID, selectedParticipants, splitOrRequestOptions]); - /** - * Grab the appropriate image URI based on file type - * - * @param {String} receiptPath - * @param {String} receiptSource - * @returns {*} - */ - const getImageURI = (receiptPath, receiptSource) => { - const {fileExtension} = FileUtils.splitExtensionFromFileName(receiptSource); - const isReceiptImage = Str.isImage(props.receiptSource); - - if (isReceiptImage) { - return receiptPath; - } - - if (fileExtension === CONST.IOU.FILE_TYPES.HTML) { - return ReceiptHTML; - } - - if (fileExtension === CONST.IOU.FILE_TYPES.DOC || fileExtension === CONST.IOU.FILE_TYPES.DOCX) { - return ReceiptDoc; - } - - if (fileExtension === CONST.IOU.FILE_TYPES.SVG) { - return ReceiptSVG; - } - - return ReceiptGeneric; - }; - return ( ) : ( @@ -90,8 +95,8 @@ function MoneyRequestHeader(props) { personalDetails={props.personalDetails} shouldShowBackButton={props.isSmallScreenWidth} onBackButtonPress={() => Navigation.goBack(ROUTES.HOME, false, true)} - shouldShowBorderBottom /> + {isScanning && } + + {translate('iou.receiptStatusTitle')} + + + {translate('iou.receiptStatusText')} + + + ); +} + +MoneyRequestHeaderStatusBar.displayName = 'MoneyRequestHeaderStatusBar'; + +export default MoneyRequestHeaderStatusBar; diff --git a/src/components/ReportActionItem/MoneyRequestAction.js b/src/components/ReportActionItem/MoneyRequestAction.js index 228558d38e9e..da361b5540db 100644 --- a/src/components/ReportActionItem/MoneyRequestAction.js +++ b/src/components/ReportActionItem/MoneyRequestAction.js @@ -10,7 +10,7 @@ import compose from '../../libs/compose'; import reportActionPropTypes from '../../pages/home/report/reportActionPropTypes'; import networkPropTypes from '../networkPropTypes'; import iouReportPropTypes from '../../pages/iouReportPropTypes'; -import IOUPreview from './IOUPreview'; +import MoneyRequestPreview from './MoneyRequestPreview'; import Navigation from '../../libs/Navigation/Navigation'; import ROUTES from '../../ROUTES'; import styles from '../../styles/styles'; @@ -87,7 +87,7 @@ const defaultProps = { function MoneyRequestAction(props) { const isSplitBillAction = lodashGet(props.action, 'originalMessage.type', '') === CONST.IOU.REPORT_ACTION_TYPE.SPLIT; - const onIOUPreviewPressed = () => { + const onMoneyRequestPreviewPressed = () => { if (isSplitBillAction) { const reportActionID = lodashGet(props.action, 'reportActionID', '0'); Navigation.navigate(ROUTES.getSplitBillDetailsRoute(props.chatReportID, reportActionID)); @@ -138,16 +138,16 @@ function MoneyRequestAction(props) { return isDeletedParentAction ? ( ${props.translate('parentReportAction.deletedRequest')}`} /> ) : ( - ); diff --git a/src/components/ReportActionItem/IOUPreview.js b/src/components/ReportActionItem/MoneyRequestPreview.js similarity index 63% rename from src/components/ReportActionItem/IOUPreview.js rename to src/components/ReportActionItem/MoneyRequestPreview.js index 95c303667469..65372c259955 100644 --- a/src/components/ReportActionItem/IOUPreview.js +++ b/src/components/ReportActionItem/MoneyRequestPreview.js @@ -29,6 +29,8 @@ import * as ReportUtils from '../../libs/ReportUtils'; import * as TransactionUtils from '../../libs/TransactionUtils'; import refPropTypes from '../refPropTypes'; import PressableWithFeedback from '../Pressable/PressableWithoutFeedback'; +import * as ReceiptUtils from '../../libs/ReceiptUtils'; +import ReportActionItemImages from './ReportActionItemImages'; const propTypes = { /** The active IOUReport, used for Onyx subscription */ @@ -125,7 +127,7 @@ const defaultProps = { shouldShowPendingConversionMessage: false, }; -function IOUPreview(props) { +function MoneyRequestPreview(props) { if (_.isEmpty(props.iouReport) && !props.isBillSplit) { return null; } @@ -144,7 +146,9 @@ function IOUPreview(props) { const isCurrentUserManager = managerID === sessionAccountID; const transaction = TransactionUtils.getLinkedTransaction(props.action); - const {amount: requestAmount, currency: requestCurrency, comment: requestComment} = ReportUtils.getTransactionDetails(transaction); + const {amount: requestAmount, currency: requestCurrency, comment: requestComment, merchant: requestMerchant} = ReportUtils.getTransactionDetails(transaction); + const hasReceipt = TransactionUtils.hasReceipt(transaction); + const isScanning = hasReceipt && TransactionUtils.isReceiptBeingScanned(transaction); const getSettledMessage = () => { switch (lodashGet(props.action, 'originalMessage.paymentType', '')) { @@ -164,6 +168,10 @@ function IOUPreview(props) { }; const getPreviewHeaderText = () => { + if (isScanning) { + return props.translate('common.receipt'); + } + if (props.isBillSplit) { return props.translate('iou.split'); } @@ -177,6 +185,14 @@ function IOUPreview(props) { return message; }; + const getDisplayAmountText = () => { + if (isScanning) { + return props.translate('iou.receiptScanning'); + } + + return CurrencyUtils.convertToDisplayString(requestAmount, requestCurrency); + }; + const childContainer = ( - - - - {getPreviewHeaderText()} - {Boolean(getSettledMessage()) && ( - <> - - {getSettledMessage()} - - )} + + {hasReceipt && ( + + )} + + + + {getPreviewHeaderText()} + {Boolean(getSettledMessage()) && ( + <> + + {getSettledMessage()} + + )} + + - - - - {CurrencyUtils.convertToDisplayString(requestAmount, requestCurrency)} - {ReportUtils.isSettled(props.iouReport.reportID) && !props.isBillSplit && ( - - + + {getDisplayAmountText()} + {ReportUtils.isSettled(props.iouReport.reportID) && !props.isBillSplit && ( + + + + )} + + {props.isBillSplit && ( + + )} - {props.isBillSplit && ( - - + {requestMerchant && ( + + {requestMerchant} )} - - - - {!isCurrentUserManager && props.shouldShowPendingConversionMessage && ( - {props.translate('iou.pendingConversionMessage')} + + + {!isCurrentUserManager && props.shouldShowPendingConversionMessage && ( + {props.translate('iou.pendingConversionMessage')} + )} + {!_.isEmpty(requestComment) && {requestComment}} + + {props.isBillSplit && !_.isEmpty(participantAccountIDs) && ( + + {props.translate('iou.amountEach', { + amount: CurrencyUtils.convertToDisplayString( + IOUUtils.calculateAmount(isPolicyExpenseChat ? 1 : participantAccountIDs.length - 1, requestAmount, requestCurrency), + requestCurrency, + ), + })} + )} - {!_.isEmpty(requestComment) && {requestComment}} - {props.isBillSplit && !_.isEmpty(participantAccountIDs) && ( - - {props.translate('iou.amountEach', { - amount: CurrencyUtils.convertToDisplayString( - IOUUtils.calculateAmount(isPolicyExpenseChat ? 1 : participantAccountIDs.length - 1, requestAmount, requestCurrency), - requestCurrency, - ), - })} - - )} @@ -271,9 +301,9 @@ function IOUPreview(props) { ); } -IOUPreview.propTypes = propTypes; -IOUPreview.defaultProps = defaultProps; -IOUPreview.displayName = 'IOUPreview'; +MoneyRequestPreview.propTypes = propTypes; +MoneyRequestPreview.defaultProps = defaultProps; +MoneyRequestPreview.displayName = 'MoneyRequestPreview'; export default compose( withLocalize, @@ -294,4 +324,4 @@ export default compose( key: ONYXKEYS.WALLET_TERMS, }, }), -)(IOUPreview); +)(MoneyRequestPreview); diff --git a/src/components/ReportActionItem/MoneyRequestView.js b/src/components/ReportActionItem/MoneyRequestView.js index c36eaf5db368..1b5d3676e30d 100644 --- a/src/components/ReportActionItem/MoneyRequestView.js +++ b/src/components/ReportActionItem/MoneyRequestView.js @@ -1,5 +1,5 @@ import React from 'react'; -import {View, Image} from 'react-native'; +import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; @@ -22,7 +22,10 @@ import iouReportPropTypes from '../../pages/iouReportPropTypes'; import * as CurrencyUtils from '../../libs/CurrencyUtils'; import EmptyStateBackgroundImage from '../../../assets/images/empty-state_background-fade.png'; import useLocalize from '../../hooks/useLocalize'; +import * as ReceiptUtils from '../../libs/ReceiptUtils'; import useWindowDimensions from '../../hooks/useWindowDimensions'; +import Image from '../Image'; +import ReportActionItemImage from './ReportActionItemImage'; import OfflineWithFeedback from '../OfflineWithFeedback'; const propTypes = { @@ -95,6 +98,12 @@ function MoneyRequestView({report, parentReport, shouldShowHorizontalRule, polic return null; } + const hasReceipt = TransactionUtils.hasReceipt(transaction); + let receiptURIs; + if (hasReceipt) { + receiptURIs = ReceiptUtils.getThumbnailAndImageURIs(transaction.receipt.source, transaction.filename); + } + return ( @@ -104,6 +113,15 @@ function MoneyRequestView({report, parentReport, shouldShowHorizontalRule, polic style={[StyleUtils.getReportWelcomeBackgroundImageStyle(true)]} /> + {hasReceipt && ( + + + + )} + `} + /> + ); + } + + return ( + + ); +} + +ReportActionItemImage.propTypes = propTypes; +ReportActionItemImage.defaultProps = defaultProps; +ReportActionItemImage.displayName = 'ReportActionItemImage'; + +export default ReportActionItemImage; diff --git a/src/components/ReportActionItem/ReportActionItemImages.js b/src/components/ReportActionItem/ReportActionItemImages.js new file mode 100644 index 000000000000..a5dcc0ccaea2 --- /dev/null +++ b/src/components/ReportActionItem/ReportActionItemImages.js @@ -0,0 +1,73 @@ +import React from 'react'; +import {View} from 'react-native'; +import PropTypes from 'prop-types'; +import _ from 'underscore'; +import styles from '../../styles/styles'; +import Text from '../Text'; +import ReportActionItemImage from './ReportActionItemImage'; + +const propTypes = { + /** array of image and thumbnail URIs */ + images: PropTypes.arrayOf( + PropTypes.shape({ + thumbnail: PropTypes.string, + image: PropTypes.string, + }), + ).isRequired, + + // We're not providing default values for size and total and disabling the ESLint rule + // because we want them to default to the length of images, but we can't set default props + // to be computed from another prop + + /** max number of images to show in the row if different than images length */ + // eslint-disable-next-line react/require-default-props + size: PropTypes.number, + + /** total number of images if different than images length */ + // eslint-disable-next-line react/require-default-props + total: PropTypes.number, + + /** if the corresponding report action item is hovered */ + isHovered: PropTypes.bool, +}; + +const defaultProps = { + isHovered: false, +}; + +function ReportActionItemImages({images, size, total, isHovered}) { + const numberOfShownImages = size || images.length; + const shownImages = images.slice(0, size); + const remaining = (total || images.length) - size; + + const hoverStyle = isHovered ? styles.reportPreviewBoxHoverBorder : undefined; + return ( + + {_.map(shownImages, ({thumbnail, image}, index) => { + const isLastImage = index === numberOfShownImages - 1; + return ( + + + {isLastImage && remaining > 0 && ( + + +{remaining} + + )} + + ); + })} + + ); +} + +ReportActionItemImages.propTypes = propTypes; +ReportActionItemImages.defaultProps = defaultProps; +ReportActionItemImages.displayName = 'ReportActionItemImages'; + +export default ReportActionItemImages; diff --git a/src/components/ReportActionItem/ReportPreview.js b/src/components/ReportActionItem/ReportPreview.js index 0ba0a1c4099f..21d45c10c8a2 100644 --- a/src/components/ReportActionItem/ReportPreview.js +++ b/src/components/ReportActionItem/ReportPreview.js @@ -25,6 +25,10 @@ import refPropTypes from '../refPropTypes'; import PressableWithoutFeedback from '../Pressable/PressableWithoutFeedback'; import themeColors from '../../styles/themes/default'; import reportPropTypes from '../../pages/reportPropTypes'; +import * as ReceiptUtils from '../../libs/ReceiptUtils'; +import * as ReportActionUtils from '../../libs/ReportActionsUtils'; +import * as TransactionUtils from '../../libs/TransactionUtils'; +import ReportActionItemImages from './ReportActionItemImages'; const propTypes = { /** All the data of the action */ @@ -95,16 +99,36 @@ const defaultProps = { function ReportPreview(props) { const managerID = props.iouReport.managerID || props.action.actorAccountID || 0; const isCurrentUserManager = managerID === lodashGet(props.session, 'accountID'); - const moneyRequestCount = lodashGet(props.action, 'childMoneyRequestCount', 0); - const moneyRequestComment = lodashGet(props.action, 'childLastMoneyRequestComment', ''); - const showComment = moneyRequestComment || moneyRequestCount > 1; const reportTotal = ReportUtils.getMoneyRequestTotal(props.iouReport); - let displayAmount; - if (reportTotal) { - displayAmount = CurrencyUtils.convertToDisplayString(reportTotal, props.iouReport.currency); - } else { + + const iouSettled = ReportUtils.isSettled(props.iouReportID); + const numberOfRequests = ReportActionUtils.getNumberOfMoneyRequests(props.action); + const moneyRequestComment = lodashGet(props.action, 'childLastMoneyRequestComment', ''); + + const transactionsWithReceipts = ReportUtils.getTransactionsWithReceipts(props.iouReport); + const numberOfScanningReceipts = _.filter(transactionsWithReceipts, (transaction) => TransactionUtils.isReceiptBeingScanned(transaction)).length; + const hasReceipts = transactionsWithReceipts.length > 0; + const isScanning = hasReceipts && ReportUtils.areAllRequestsBeingSmartScanned(props.iouReport, props.action); + const lastThreeTransactionsWithReceipts = ReportUtils.getReportPreviewDisplayTransactions(props.action); + + const hasOnlyOneReceiptRequest = numberOfRequests === 1 && hasReceipts; + const previewSubtitle = hasOnlyOneReceiptRequest + ? transactionsWithReceipts[0].merchant + : props.translate('iou.requestCount', { + count: numberOfRequests, + scanningReceipts: numberOfScanningReceipts, + }); + + const getDisplayAmount = () => { + if (reportTotal) { + return CurrencyUtils.convertToDisplayString(reportTotal, props.iouReport.currency); + } + if (isScanning) { + return props.translate('iou.receiptScanning'); + } + // If iouReport is not available, get amount from the action message (Ex: "Domain20821's Workspace owes $33.00" or "paid ₫60" or "paid -₫60 elsewhere") - displayAmount = ''; + let displayAmount = ''; const actionMessage = lodashGet(props.action, ['message', 0, 'text'], ''); const splits = actionMessage.split(' '); for (let i = 0; i < splits.length; i++) { @@ -112,13 +136,20 @@ function ReportPreview(props) { displayAmount = splits[i]; } } - } + return displayAmount; + }; + + const getPreviewMessage = () => { + const managerName = ReportUtils.isPolicyExpenseChat(props.chatReport) ? ReportUtils.getPolicyName(props.chatReport) : ReportUtils.getDisplayNameForParticipant(managerID, true); + if (isScanning) { + return props.translate('common.receipt'); + } + return props.translate(iouSettled || props.iouReport.isWaitingOnBankAccount ? 'iou.payerPaid' : 'iou.payerOwes', {payer: managerName}); + }; - const managerName = ReportUtils.isPolicyExpenseChat(props.chatReport) ? ReportUtils.getPolicyName(props.chatReport) : ReportUtils.getDisplayNameForParticipant(managerID, true); const bankAccountRoute = ReportUtils.getBankAccountRoute(props.chatReport); - const previewMessage = props.translate(ReportUtils.isSettled(props.iouReportID) || props.iouReport.isWaitingOnBankAccount ? 'iou.payerPaid' : 'iou.payerOwes', {payer: managerName}); - const shouldShowSettlementButton = - !_.isEmpty(props.iouReport) && isCurrentUserManager && !ReportUtils.isSettled(props.iouReportID) && !props.iouReport.isWaitingOnBankAccount && reportTotal !== 0; + const shouldShowSettlementButton = !_.isEmpty(props.iouReport) && isCurrentUserManager && !iouSettled && !props.iouReport.isWaitingOnBankAccount && reportTotal !== 0; + return ( - - - - {previewMessage} - - - - - {displayAmount} - {ReportUtils.isSettled(props.iouReportID) && ( - - - - )} + + {hasReceipts && ( + ReceiptUtils.getThumbnailAndImageURIs(receipt.source, filename))} + size={3} + total={transactionsWithReceipts.length} + isHovered={props.isHovered || isScanning} + /> + )} + + + + {getPreviewMessage()} + + - - {showComment && ( - - - - {moneyRequestCount > 1 ? props.translate('iou.requestCount', {count: moneyRequestCount}) : moneyRequestComment} - + + + {getDisplayAmount()} + {ReportUtils.isSettled(props.iouReportID) && ( + + + + )} - )} - {shouldShowSettlementButton && ( - IOU.payMoneyRequest(paymentType, props.chatReport, props.iouReport)} - enablePaymentsRoute={ROUTES.BANK_ACCOUNT_NEW} - addBankAccountRoute={bankAccountRoute} - style={[styles.requestPreviewBox]} - /> - )} + {hasReceipts && !isScanning && ( + + + {previewSubtitle || moneyRequestComment} + + + )} + {shouldShowSettlementButton && ( + IOU.payMoneyRequest(paymentType, props.chatReport, props.iouReport)} + enablePaymentsRoute={ROUTES.BANK_ACCOUNT_NEW} + addBankAccountRoute={bankAccountRoute} + style={[styles.requestPreviewBox]} + /> + )} + diff --git a/src/components/ThumbnailImage.js b/src/components/ThumbnailImage.js index d68d7530839b..983f806bb2e2 100644 --- a/src/components/ThumbnailImage.js +++ b/src/components/ThumbnailImage.js @@ -24,12 +24,16 @@ const propTypes = { /** Height of the thumbnail image */ imageHeight: PropTypes.number, + + /** Should the image be resized on load or just fit container */ + shouldDynamicallyResize: PropTypes.bool, }; const defaultProps = { style: {}, imageWidth: 200, imageHeight: 200, + shouldDynamicallyResize: true, }; /** @@ -85,9 +89,12 @@ function ThumbnailImage(props) { }, [windowHeight], ); + + const sizeStyles = props.shouldDynamicallyResize ? [StyleUtils.getWidthAndHeightStyle(imageWidth, imageHeight)] : [styles.w100, styles.h100]; + return ( - + `${count} requests${scanningReceipts > 0 ? `, ${scanningReceipts} scanning` : ''}`, deleteRequest: 'Delete request', deleteConfirmation: 'Are you sure that you want to delete this request?', settledExpensify: 'Paid', @@ -424,7 +431,6 @@ export default { pendingConversionMessage: "Total will update when you're back online", threadRequestReportName: ({formattedAmount, comment}) => `${formattedAmount} request${comment ? ` for ${comment}` : ''}`, threadSentMoneyReportName: ({formattedAmount, comment}) => `${formattedAmount} sent${comment ? ` for ${comment}` : ''}`, - requestCount: ({count}) => `${count} requests`, error: { invalidSplit: 'Split amounts do not equal total amount', other: 'Unexpected error, please try again later', diff --git a/src/languages/es.js b/src/languages/es.js index bb3306417e17..c3c950b04a8c 100644 --- a/src/languages/es.js +++ b/src/languages/es.js @@ -154,6 +154,8 @@ export default { edit: 'Editar', showMore: 'Mostrar más', merchant: 'Comerciante', + receipt: 'Recibo', + replace: 'Sustituir', }, anonymousReportFooter: { logoTagline: 'Únete a la discussion.', @@ -398,6 +400,11 @@ export default { pay: 'Pagar', viewDetails: 'Ver detalles', pending: 'Pendiente', + deleteReceipt: 'Eliminar recibo', + receiptScanning: 'Escaneo de recibo en curso…', + receiptStatusTitle: 'Escaneando…', + receiptStatusText: 'Solo tú puedes ver este recibo cuando se está escaneando. Vuelve más tarde o introduce los detalles ahora.', + requestCount: ({count, scanningReceipts = 0}) => `${count} solicitudes${scanningReceipts > 0 ? `, ${scanningReceipts} escaneando` : ''}`, deleteRequest: 'Eliminar pedido', deleteConfirmation: '¿Estás seguro de que quieres eliminar este pedido?', settledExpensify: 'Pagado', @@ -423,7 +430,6 @@ export default { pendingConversionMessage: 'El total se actualizará cuando estés online', threadRequestReportName: ({formattedAmount, comment}) => `Solicitud de ${formattedAmount}${comment ? ` para ${comment}` : ''}`, threadSentMoneyReportName: ({formattedAmount, comment}) => `${formattedAmount} enviado${comment ? ` para ${comment}` : ''}`, - requestCount: ({count}) => `${count} solicitudes`, error: { invalidSplit: 'La suma de las partes no equivale al monto total', other: 'Error inesperado, por favor inténtalo más tarde', diff --git a/src/libs/ReceiptUtils.js b/src/libs/ReceiptUtils.js index b8ec54c0e899..c1c028073690 100644 --- a/src/libs/ReceiptUtils.js +++ b/src/libs/ReceiptUtils.js @@ -1,11 +1,16 @@ import lodashGet from 'lodash/get'; import _ from 'underscore'; +import Str from 'expensify-common/lib/str'; import * as FileUtils from './fileDownload/FileUtils'; import CONST from '../CONST'; import Receipt from './actions/Receipt'; import * as Localize from './Localize'; +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'; -const validateReceipt = (file) => { +function validateReceipt(file) { const {fileExtension} = FileUtils.splitExtensionFromFileName(lodashGet(file, 'name', '')); if (_.contains(CONST.API_ATTACHMENT_VALIDATIONS.UNALLOWED_EXTENSIONS, fileExtension.toLowerCase())) { Receipt.setUploadReceiptError(true, Localize.translateLocal('attachmentPicker.wrongFileType'), Localize.translateLocal('attachmentPicker.notAllowedExtension')); @@ -23,6 +28,41 @@ const validateReceipt = (file) => { } return true; -}; +} -export default {validateReceipt}; +/** + * Grab the appropriate receipt image and thumbnail URIs based on file type + * + * @param {String} path URI to image, i.e. blob:new.expensify.com/9ef3a018-4067-47c6-b29f-5f1bd35f213d or expensify.com/receipts/w_e616108497ef940b7210ec6beb5a462d01a878f4.jpg + * @param {String} filename of uploaded image or last part of remote URI + * @returns {Object} + */ +function getThumbnailAndImageURIs(path, filename) { + // For local files, we won't have a thumbnail yet + if (path.startsWith('blob:') || path.startsWith('file:')) { + return {thumbnail: null, image: path}; + } + + const {fileExtension} = FileUtils.splitExtensionFromFileName(filename); + const isReceiptImage = Str.isImage(filename); + + if (isReceiptImage) { + return {thumbnail: `${path}.1024.jpg`, image: path}; + } + + 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; + } + return {thumbnail: null, image}; +} + +export {validateReceipt, getThumbnailAndImageURIs}; diff --git a/src/libs/ReportActionsUtils.js b/src/libs/ReportActionsUtils.js index f68cfe6adeba..ca78dcc62a21 100644 --- a/src/libs/ReportActionsUtils.js +++ b/src/libs/ReportActionsUtils.js @@ -566,6 +566,16 @@ function isMessageDeleted(reportAction) { return lodashGet(reportAction, ['message', 0, 'isDeletedParentAction'], false); } +/** + * Returns the number of money requests associated with a report preview + * + * @param {Object|null} reportPreviewAction + * @returns {Number} + */ +function getNumberOfMoneyRequests(reportPreviewAction) { + return lodashGet(reportPreviewAction, 'childMoneyRequestCount', 0); +} + /** * @param {*} reportAction * @returns {Boolean} @@ -574,6 +584,14 @@ function isSplitBillAction(reportAction) { return lodashGet(reportAction, 'originalMessage.type', '') === CONST.IOU.REPORT_ACTION_TYPE.SPLIT; } +/** + * @param {*} reportID + * @returns {[Object]} + */ +function getAllReportActions(reportID) { + return lodashGet(allReportActions, reportID, []); +} + export { getSortedReportActions, getLastVisibleAction, @@ -607,5 +625,7 @@ export { isWhisperAction, isPendingRemove, getReportAction, + getNumberOfMoneyRequests, isSplitBillAction, + getAllReportActions, }; diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 1f571bb7c33e..224b195c3d99 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -1249,6 +1249,50 @@ function getTransactionDetails(transaction) { }; } +/** + * Gets all transactions on an IOU report with a receipt + * + * @param {Object|null} iouReport + * @returns {[Object]} + */ +function getTransactionsWithReceipts(iouReport) { + const reportID = lodashGet(iouReport, 'reportID'); + const reportActions = ReportActionsUtils.getAllReportActions(reportID); + return _.reduce( + reportActions, + (transactions, action) => { + if (ReportActionsUtils.isMoneyRequestAction(action)) { + const transaction = TransactionUtils.getLinkedTransaction(action); + if (TransactionUtils.hasReceipt(transaction)) { + transactions.push(transaction); + } + } + return transactions; + }, + [], + ); +} + +/** + * For report previews, we display a "Receipt scan in progress" indicator + * instead of the report total only when we have no report total ready to show. This is the case when + * all requests are receipts that are being SmartScanned. As soon as we have a non-receipt request, + * or as soon as one receipt request is done scanning, we have at least one + * "ready" money request, and we remove this indicator to show the partial report total. + * + * @param {Object|null} iouReport + * @param {Object|null} reportPreviewAction the preview action associated with the IOU report + * @returns {Boolean} + */ +function areAllRequestsBeingSmartScanned(iouReport, reportPreviewAction) { + const transactions = getTransactionsWithReceipts(iouReport); + // If we have more requests than requests with receipts, we have some manual requests + if (ReportActionsUtils.getNumberOfMoneyRequests(reportPreviewAction) > transactions.length) { + return false; + } + return _.all(transactions, (transaction) => TransactionUtils.isReceiptBeingScanned(transaction)); +} + /** * Given a parent IOU report action get report name for the LHN. * @@ -1261,6 +1305,10 @@ function getTransactionReportName(reportAction) { } const transaction = TransactionUtils.getLinkedTransaction(reportAction); + if (TransactionUtils.hasReceipt(transaction) && TransactionUtils.isReceiptBeingScanned(transaction)) { + return Localize.translateLocal('iou.receiptScanning'); + } + const {amount, currency, comment} = getTransactionDetails(transaction); return Localize.translateLocal(ReportActionsUtils.isSentMoneyReportAction(reportAction) ? 'iou.threadSentMoneyReportName' : 'iou.threadRequestReportName', { @@ -1973,6 +2021,7 @@ function buildOptimisticIOUReportAction( created: DateUtils.getDBTime(), pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, receipt, + whisperedToAccountIDs: !_.isEmpty(receipt) ? [currentUserAccountID] : [], }; } @@ -1982,10 +2031,12 @@ function buildOptimisticIOUReportAction( * @param {Object} chatReport * @param {Object} iouReport * @param {String} [comment] - User comment for the IOU. + * @param {Object} [transaction] - optimistic first transaction of preview * * @returns {Object} */ -function buildOptimisticReportPreview(chatReport, iouReport, comment = '') { +function buildOptimisticReportPreview(chatReport, iouReport, comment = '', transaction = undefined) { + const hasReceipt = TransactionUtils.hasReceipt(transaction); const message = getReportPreviewMessage(iouReport); return { reportActionID: NumberUtils.rand64(), @@ -2005,9 +2056,12 @@ function buildOptimisticReportPreview(chatReport, iouReport, comment = '') { ], created: DateUtils.getDBTime(), accountID: iouReport.managerID || 0, - actorAccountID: iouReport.managerID || 0, + // The preview is initially whispered if created with a receipt, so the actor is the current user as well + actorAccountID: hasReceipt ? currentUserAccountID : iouReport.managerID || 0, childMoneyRequestCount: 1, childLastMoneyRequestComment: comment, + childLastReceiptTransactionIDs: hasReceipt ? transaction.transactionID : '', + whisperedToAccountIDs: hasReceipt ? [currentUserAccountID] : [], }; } @@ -2058,10 +2112,15 @@ function buildOptimisticModifiedExpenseReportAction(transactionThread, oldTransa * @param {Object} iouReport * @param {Object} reportPreviewAction * @param {String} [comment] - User comment for the IOU. + * @param {Object} [transaction] - optimistic newest transaction of a report preview * * @returns {Object} */ -function updateReportPreview(iouReport, reportPreviewAction, comment = '') { +function updateReportPreview(iouReport, reportPreviewAction, comment = '', transaction = undefined) { + const hasReceipt = TransactionUtils.hasReceipt(transaction); + const lastReceiptTransactionIDs = lodashGet(reportPreviewAction, 'childLastReceiptTransactionIDs', ''); + const previousTransactionIDs = lastReceiptTransactionIDs.split(',').slice(0, 2); + const message = getReportPreviewMessage(iouReport, reportPreviewAction); return { ...reportPreviewAction, @@ -2076,6 +2135,10 @@ function updateReportPreview(iouReport, reportPreviewAction, comment = '') { ], childLastMoneyRequestComment: comment || reportPreviewAction.childLastMoneyRequestComment, childMoneyRequestCount: reportPreviewAction.childMoneyRequestCount + 1, + childLastReceiptTransactionIDs: hasReceipt ? [transaction.transactionID, ...previousTransactionIDs].join(',') : lastReceiptTransactionIDs, + // As soon as we add a transaction without a receipt to the report, it will have ready money requests, + // so we remove the whisper + whisperedToAccountIDs: hasReceipt ? reportPreviewAction.whisperedToAccountIDs : [], }; } @@ -3187,6 +3250,27 @@ function getTaskAssigneeChatOnyxData(accountID, assigneeEmail, assigneeAccountID }; } +/** + * 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, ['childLastReceiptTransactionIDs'], '').split(','); + return _.reduce( + transactionIDs, + (transactions, transactionID) => { + const transaction = TransactionUtils.getTransaction(transactionID); + if (TransactionUtils.hasReceipt(transaction)) { + transactions.push(transaction); + } + return transactions; + }, + [], + ); +} + export { getReportParticipantsTitle, isReportMessageAttachment, @@ -3315,4 +3399,7 @@ export { getTransactionReportName, getTransactionDetails, getTaskAssigneeChatOnyxData, + areAllRequestsBeingSmartScanned, + getReportPreviewDisplayTransactions, + getTransactionsWithReceipts, }; diff --git a/src/libs/TransactionUtils.js b/src/libs/TransactionUtils.js index f7e3e644d7b8..a995769eebe0 100644 --- a/src/libs/TransactionUtils.js +++ b/src/libs/TransactionUtils.js @@ -31,6 +31,7 @@ Onyx.connect({ * @param {String} [originalTransactionID] * @param {String} [merchant] * @param {Object} [receipt] + * @param {String} [filename] * @param {String} [existingTransactionID] When creating a distance request, an empty transaction has already been created with a transactionID. In that case, the transaction here needs to have it's transactionID match what was already generated. * @returns {Object} */ @@ -44,6 +45,7 @@ function buildOptimisticTransaction( originalTransactionID = '', merchant = CONST.REPORT.TYPE.IOU, receipt = {}, + filename = '', existingTransactionID = null, ) { // transactionIDs are random, positive, 64-bit numeric strings. @@ -68,9 +70,18 @@ function buildOptimisticTransaction( created: created || DateUtils.getDBTime(), pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, receipt, + filename, }; } +/** + * @param {Object|null} transaction + * @returns {Boolean} + */ +function hasReceipt(transaction) { + return !_.isEmpty(lodashGet(transaction, 'receipt')); +} + /** * Given the edit made to the money request, return an updated transaction object. * @@ -215,4 +226,21 @@ function getAllReportTransactions(reportID) { return _.filter(allTransactions, (transaction) => transaction.reportID === reportID); } -export {buildOptimisticTransaction, getUpdatedTransaction, getTransaction, getDescription, getAmount, getCurrency, getMerchant, getCreated, getLinkedTransaction, getAllReportTransactions}; +function isReceiptBeingScanned(transaction) { + return transaction.receipt.state === CONST.IOU.RECEIPT_STATE.SCANREADY || transaction.receipt.state === CONST.IOU.RECEIPT_STATE.SCANNING; +} + +export { + buildOptimisticTransaction, + getUpdatedTransaction, + getTransaction, + getDescription, + getAmount, + getCurrency, + getMerchant, + getCreated, + getLinkedTransaction, + getAllReportTransactions, + hasReceipt, + isReceiptBeingScanned, +}; diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 7244d14fe273..e1071a2ad5a1 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -376,9 +376,11 @@ function getMoneyRequestInformation(report, participant, comment, amount, curren // STEP 3: Build optimistic receipt and transaction const receiptObject = {}; + let filename; if (receipt && receipt.source) { receiptObject.source = receipt.source; receiptObject.state = CONST.IOU.RECEIPT_STATE.SCANREADY; + filename = receipt.name; } let optimisticTransaction = TransactionUtils.buildOptimisticTransaction( ReportUtils.isExpenseReport(iouReport) ? -amount : amount, @@ -390,6 +392,7 @@ function getMoneyRequestInformation(report, participant, comment, amount, curren '', merchant, receiptObject, + filename, existingTransactionID, ); @@ -428,9 +431,9 @@ function getMoneyRequestInformation(report, participant, comment, amount, curren let reportPreviewAction = isNewIOUReport ? null : ReportActionsUtils.getReportPreviewAction(chatReport.reportID, iouReport.reportID); if (reportPreviewAction) { - reportPreviewAction = ReportUtils.updateReportPreview(iouReport, reportPreviewAction, comment); + reportPreviewAction = ReportUtils.updateReportPreview(iouReport, reportPreviewAction, comment, optimisticTransaction); } else { - reportPreviewAction = ReportUtils.buildOptimisticReportPreview(chatReport, iouReport, comment); + reportPreviewAction = ReportUtils.buildOptimisticReportPreview(chatReport, iouReport, comment, optimisticTransaction); } // Add optimistic personal details for participant @@ -1061,7 +1064,7 @@ function deleteMoneyRequest(transactionID, reportAction, isSingleTransactionView // STEP 2: Decide if we need to: // 1. Delete the transactionThread - delete if there are no visible comments in the thread - // 2. Update the iouPreview to show [Deleted request] - update if the transactionThread exists AND it isn't being deleted + // 2. Update the moneyRequestPreview to show [Deleted request] - update if the transactionThread exists AND it isn't being deleted const shouldDeleteTransactionThread = transactionThreadID ? ReportActionsUtils.getLastVisibleMessage(transactionThreadID).lastMessageText.length === 0 : false; const shouldShowDeletedRequestMessage = transactionThreadID && !shouldDeleteTransactionThread; diff --git a/src/libs/fileDownload/FileUtils.js b/src/libs/fileDownload/FileUtils.js index 4d714fff4f24..a98a33b2934f 100644 --- a/src/libs/fileDownload/FileUtils.js +++ b/src/libs/fileDownload/FileUtils.js @@ -1,4 +1,4 @@ -import {Alert, Linking} from 'react-native'; +import {Alert, Linking, Platform} from 'react-native'; import CONST from '../../CONST'; import * as Localize from '../Localize'; import DateUtils from '../DateUtils'; @@ -146,7 +146,10 @@ const readFileAsync = (path, fileName) => return fetch(path) .then((res) => { - if (!res.ok) { + // For some reason, fetch is "Unable to read uploaded file" + // on Android even though the blob is returned, so we'll ignore + // in that case + if (!res.ok && Platform.OS !== 'android') { throw Error(res.statusText); } return res.blob(); @@ -154,6 +157,9 @@ const readFileAsync = (path, fileName) => .then((blob) => { const file = new File([blob], cleanFileName(fileName)); file.source = path; + // For some reason, the File object on iOS does not have a uri property + // so images aren't uploaded correctly to the backend + file.uri = path; resolve(file); }) .catch((e) => { diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.js b/src/pages/home/report/ContextMenu/ContextMenuActions.js index e66c61b1122f..898f5103eac5 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.js +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.js @@ -176,6 +176,7 @@ export default [ shouldShow: (type, reportAction) => type === CONTEXT_MENU_TYPES.REPORT_ACTION && reportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.IOU && + reportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && reportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.TASKCANCELLED && reportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.TASKCOMPLETED && reportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.TASKREOPENED && diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index 870d2fd6ee93..9f0c2d8cda47 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -252,7 +252,7 @@ function ReportActionItem(props) { // IOUDetails only exists when we are sending money const isSendingMoney = originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.PAY && _.has(originalMessage, 'IOUDetails'); - // Show the IOUPreview for when request was created, bill was split or money was sent + // Show the MoneyRequestPreview for when request was created, bill was split or money was sent if ( props.action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && originalMessage && @@ -469,10 +469,21 @@ function ReportActionItem(props) { const parentReportAction = ReportActionsUtils.getParentReportAction(props.report); if (ReportActionsUtils.isTransactionThread(parentReportAction)) { return ( - + + + + + ); } if (ReportUtils.isTaskReport(props.report)) { diff --git a/src/pages/iou/ReceiptSelector/index.js b/src/pages/iou/ReceiptSelector/index.js index c674878c2c73..392e96887f8b 100644 --- a/src/pages/iou/ReceiptSelector/index.js +++ b/src/pages/iou/ReceiptSelector/index.js @@ -19,7 +19,7 @@ import Receipt from '../../../libs/actions/Receipt'; import useWindowDimensions from '../../../hooks/useWindowDimensions'; import useLocalize from '../../../hooks/useLocalize'; import {DragAndDropContext} from '../../../components/DragAndDrop/Provider'; -import ReceiptUtils from '../../../libs/ReceiptUtils'; +import * as ReceiptUtils from '../../../libs/ReceiptUtils'; const propTypes = { /** Information shown to the user when a receipt is not valid */ diff --git a/src/styles/styles.js b/src/styles/styles.js index 4217148bd35b..350df210dc18 100644 --- a/src/styles/styles.js +++ b/src/styles/styles.js @@ -2653,19 +2653,18 @@ const styles = { maxWidth: variables.sideBarWidth, }, - iouPreviewBox: { + moneyRequestPreviewBox: { backgroundColor: themeColors.cardBG, borderRadius: variables.componentBorderRadiusLarge, - padding: 16, maxWidth: variables.sideBarWidth, width: '100%', }, - iouPreviewBoxHover: { - backgroundColor: themeColors.border, + moneyRequestPreviewBoxText: { + padding: 16, }, - iouPreviewBoxLoading: { + moneyRequestPreviewBoxLoading: { // When a new IOU request arrives it is very briefly in a loading state, so set the minimum height of the container to 94 to match the rendered height after loading. // Otherwise, the IOU request pay button will not be fully visible and the user will have to scroll up to reveal the entire IOU request container. // See https://github.com/Expensify/App/issues/10283. @@ -2673,7 +2672,7 @@ const styles = { width: '100%', }, - iouPreviewBoxAvatar: { + moneyRequestPreviewBoxAvatar: { marginRight: -10, marginBottom: 0, }, @@ -3719,6 +3718,61 @@ const styles = { margin: 20, }, + reportPreviewBox: { + backgroundColor: themeColors.cardBG, + borderRadius: variables.componentBorderRadiusLarge, + maxWidth: variables.sideBarWidth, + width: '100%', + }, + + reportPreviewBoxHoverBorder: { + borderColor: themeColors.border, + backgroundColor: themeColors.border, + }, + + reportPreviewBoxBody: { + padding: 16, + }, + + reportActionItemImages: { + flexDirection: 'row', + borderWidth: 2, + borderColor: themeColors.cardBG, + borderTopLeftRadius: variables.componentBorderRadiusLarge, + borderTopRightRadius: variables.componentBorderRadiusLarge, + overflow: 'hidden', + height: 200, + }, + + reportActionItemImage: { + borderWidth: 1, + borderColor: themeColors.cardBG, + flex: 1, + width: '100%', + height: '100%', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + }, + + reportActionItemImagesMore: { + position: 'absolute', + borderRadius: 18, + backgroundColor: themeColors.cardBG, + width: 36, + height: 36, + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + }, + + moneyRequestHeaderStatusBarBadge: { + padding: 8, + borderRadius: variables.componentBorderRadiusMedium, + marginRight: 16, + backgroundColor: themeColors.border, + }, + staticHeaderImage: { minHeight: 240, }, @@ -3732,6 +3786,17 @@ const styles = { transform: [{rotate: '90deg'}], }, + moneyRequestViewImage: { + ...spacing.mh5, + ...spacing.mv3, + overflow: 'hidden', + borderWidth: 2, + borderColor: themeColors.cardBG, + borderRadius: variables.componentBorderRadiusLarge, + height: 200, + maxWidth: 400, + }, + distanceRequestContainer: (maxHeight) => ({ ...flex.flexShrink2, minHeight: variables.baseMenuItemHeight,