diff --git a/assets/images/eReceipt_background.svg b/assets/images/eReceipt_background.svg new file mode 100644 index 00000000000..5070ed3b2f2 --- /dev/null +++ b/assets/images/eReceipt_background.svg @@ -0,0 +1,1635 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/Attachments/AttachmentCarousel/CarouselItem.js b/src/components/Attachments/AttachmentCarousel/CarouselItem.js index 3aeef8482e2..096b6d60d42 100644 --- a/src/components/Attachments/AttachmentCarousel/CarouselItem.js +++ b/src/components/Attachments/AttachmentCarousel/CarouselItem.js @@ -10,6 +10,7 @@ import Button from '../../Button'; import AttachmentView from '../AttachmentView'; import SafeAreaConsumer from '../../SafeAreaConsumer'; import ReportAttachmentsContext from '../../../pages/home/report/ReportAttachmentsContext'; +import * as AttachmentsPropTypes from '../propTypes'; const propTypes = { /** Attachment required information such as the source and file name */ @@ -20,8 +21,8 @@ const propTypes = { /** Whether source URL requires authentication */ isAuthTokenRequired: PropTypes.bool, - /** The source (URL) of the attachment */ - source: PropTypes.string, + /** URL to full-sized attachment or SVG function */ + source: AttachmentsPropTypes.attachmentSourcePropType.isRequired, /** Additional information about the attachment file */ file: PropTypes.shape({ @@ -31,6 +32,9 @@ const propTypes = { /** Whether the attachment has been flagged */ hasBeenFlagged: PropTypes.bool, + + /** The id of the transaction related to the attachment */ + transactionID: PropTypes.string, }).isRequired, /** Whether the attachment is currently being viewed in the carousel */ @@ -97,6 +101,7 @@ function CarouselItem({item, isFocused, onPress}) { isFocused={isFocused} onPress={onPress} isUsedInCarousel + transactionID={item.transactionID} /> diff --git a/src/components/Attachments/AttachmentView/index.js b/src/components/Attachments/AttachmentView/index.js index f4d3036ff80..a1b07fb99dd 100755 --- a/src/components/Attachments/AttachmentView/index.js +++ b/src/components/Attachments/AttachmentView/index.js @@ -3,6 +3,7 @@ import {View, ActivityIndicator} from 'react-native'; import _ from 'underscore'; import PropTypes from 'prop-types'; import Str from 'expensify-common/lib/str'; +import {withOnyx} from 'react-native-onyx'; import styles from '../../../styles/styles'; import Icon from '../../Icon'; import * as Expensicons from '../../Icon/Expensicons'; @@ -17,7 +18,10 @@ import AttachmentViewPdf from './AttachmentViewPdf'; import addEncryptedAuthTokenToURL from '../../../libs/addEncryptedAuthTokenToURL'; import * as StyleUtils from '../../../styles/StyleUtils'; import {attachmentViewPropTypes, attachmentViewDefaultProps} from './propTypes'; +import * as TransactionUtils from '../../../libs/TransactionUtils'; +import DistanceEReceipt from '../../DistanceEReceipt'; import useNetwork from '../../../hooks/useNetwork'; +import ONYXKEYS from '../../../ONYXKEYS'; const propTypes = { ...attachmentViewPropTypes, @@ -38,6 +42,10 @@ const propTypes = { /** Denotes whether it is a workspace avatar or not */ isWorkspaceAvatar: PropTypes.bool, + + /** The id of the transaction related to the attachment */ + // eslint-disable-next-line react/no-unused-prop-types + transactionID: PropTypes.string, }; const defaultProps = { @@ -47,6 +55,7 @@ const defaultProps = { onToggleKeyboard: () => {}, containerStyles: [], isWorkspaceAvatar: false, + transactionID: '', }; function AttachmentView({ @@ -64,9 +73,9 @@ function AttachmentView({ isFocused, isWorkspaceAvatar, fallbackSource, + transaction, }) { const [loadComplete, setLoadComplete] = useState(false); - const [imageError, setImageError] = useState(false); useNetwork({onReconnect: () => setImageError(false)}); @@ -113,6 +122,10 @@ function AttachmentView({ ); } + if (TransactionUtils.isDistanceRequest(transaction)) { + return ; + } + // For this check we use both source and file.name since temporary file source is a blob // both PDFs and images will appear as images when pasted into the text field. // We also check for numeric source since this is how static images (used for preview) are represented in RN. @@ -168,4 +181,12 @@ AttachmentView.propTypes = propTypes; AttachmentView.defaultProps = defaultProps; AttachmentView.displayName = 'AttachmentView'; -export default compose(memo, withLocalize)(AttachmentView); +export default compose( + memo, + withLocalize, + withOnyx({ + transaction: { + key: ({transactionID}) => `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, + }, + }), +)(AttachmentView); diff --git a/src/components/DistanceEReceipt.js b/src/components/DistanceEReceipt.js new file mode 100644 index 00000000000..7c7837b8413 --- /dev/null +++ b/src/components/DistanceEReceipt.js @@ -0,0 +1,121 @@ +import React, {useMemo} from 'react'; +import {View, ScrollView} from 'react-native'; +import lodashGet from 'lodash/get'; +import _ from 'underscore'; +import Text from './Text'; +import styles from '../styles/styles'; +import transactionPropTypes from './transactionPropTypes'; +import * as ReceiptUtils from '../libs/ReceiptUtils'; +import * as ReportUtils from '../libs/ReportUtils'; +import * as CurrencyUtils from '../libs/CurrencyUtils'; +import * as TransactionUtils from '../libs/TransactionUtils'; +import tryResolveUrlFromApiRoot from '../libs/tryResolveUrlFromApiRoot'; +import ThumbnailImage from './ThumbnailImage'; +import useLocalize from '../hooks/useLocalize'; +import Icon from './Icon'; +import themeColors from '../styles/themes/default'; +import * as Expensicons from './Icon/Expensicons'; +import EReceiptBackground from '../../assets/images/eReceipt_background.svg'; +import useNetwork from '../hooks/useNetwork'; +import PendingMapView from './MapView/PendingMapView'; + +const propTypes = { + /** The transaction for the distance request */ + transaction: transactionPropTypes, +}; + +const defaultProps = { + transaction: {}, +}; + +function DistanceEReceipt({transaction}) { + const {translate} = useLocalize(); + const {isOffline} = useNetwork(); + const {thumbnail} = TransactionUtils.hasReceipt(transaction) ? ReceiptUtils.getThumbnailAndImageURIs(transaction.receipt.source, transaction.filename) : {}; + const {amount: transactionAmount, currency: transactionCurrency, merchant: transactionMerchant, created: transactionDate} = ReportUtils.getTransactionDetails(transaction); + const formattedTransactionAmount = transactionAmount ? CurrencyUtils.convertToDisplayString(transactionAmount, transactionCurrency) : translate('common.tbd'); + const thumbnailSource = tryResolveUrlFromApiRoot(thumbnail || ''); + const waypoints = lodashGet(transaction, 'comment.waypoints', {}); + const sortedWaypoints = useMemo( + () => + // The waypoint keys are sometimes out of order + _.chain(waypoints) + .keys() + .sort((keyA, keyB) => TransactionUtils.getWaypointIndex(keyA) - TransactionUtils.getWaypointIndex(keyB)) + .map((key) => ({[key]: waypoints[key]})) + .reduce((result, obj) => (obj ? _.assign(result, obj) : result), {}) + .value(), + [waypoints], + ); + return ( + + + + + + {isOffline || !thumbnailSource ? ( + + ) : ( + + )} + + + {formattedTransactionAmount} + {transactionMerchant} + + + {_.map(sortedWaypoints, (waypoint, key) => { + const index = TransactionUtils.getWaypointIndex(key); + let descriptionKey = 'distance.waypointDescription.'; + if (index === 0) { + descriptionKey += 'start'; + } else if (index === _.size(waypoints) - 1) { + descriptionKey += 'finish'; + } else { + descriptionKey += 'stop'; + } + return ( + + {translate(descriptionKey)} + {waypoint.address || ''} + + ); + })} + + {translate('common.date')} + {transactionDate} + + + + + {translate('eReceipt.guaranteed')} + + + + + ); +} + +export default DistanceEReceipt; +DistanceEReceipt.displayName = 'DistanceEReceipt'; +DistanceEReceipt.propTypes = propTypes; +DistanceEReceipt.defaultProps = defaultProps; diff --git a/src/components/ReportActionItem/MoneyRequestPreview.js b/src/components/ReportActionItem/MoneyRequestPreview.js index c7ca93e8769..24f8e0b0c0e 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview.js +++ b/src/components/ReportActionItem/MoneyRequestPreview.js @@ -222,7 +222,7 @@ function MoneyRequestPreview(props) { const getDisplayAmountText = () => { if (isDistanceRequest) { - return CurrencyUtils.convertToDisplayString(TransactionUtils.getAmount(props.transaction), props.transaction.currency); + return requestAmount ? CurrencyUtils.convertToDisplayString(requestAmount, props.transaction.currency) : props.translate('common.tbd'); } if (isScanning) { diff --git a/src/components/ReportActionItem/MoneyRequestView.js b/src/components/ReportActionItem/MoneyRequestView.js index 079bc64d96b..988c61f0aad 100644 --- a/src/components/ReportActionItem/MoneyRequestView.js +++ b/src/components/ReportActionItem/MoneyRequestView.js @@ -93,10 +93,15 @@ function MoneyRequestView({report, betas, parentReport, policyCategories, should } = ReportUtils.getTransactionDetails(transaction); const isEmptyMerchant = transactionMerchant === '' || transactionMerchant === CONST.TRANSACTION.UNKNOWN_MERCHANT || transactionMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT; - const formattedTransactionAmount = transactionAmount && transactionCurrency && CurrencyUtils.convertToDisplayString(transactionAmount, transactionCurrency); + const isDistanceRequest = TransactionUtils.isDistanceRequest(transaction); + let formattedTransactionAmount = transactionAmount ? CurrencyUtils.convertToDisplayString(transactionAmount, transactionCurrency) : ''; + if (isDistanceRequest && !formattedTransactionAmount) { + formattedTransactionAmount = translate('common.tbd'); + } const isSettled = ReportUtils.isSettled(moneyRequestReport.reportID); const canEdit = ReportUtils.canEditMoneyRequest(parentReportAction); + // A flag for verifying that the current report is a sub-report of a workspace chat const isPolicyExpenseChat = useMemo(() => ReportUtils.isPolicyExpenseChat(ReportUtils.getRootParentReport(report)), [report]); @@ -109,7 +114,10 @@ function MoneyRequestView({report, betas, parentReport, policyCategories, should const shouldShowTag = isPolicyExpenseChat && Permissions.canUseTags(betas) && (transactionTag || OptionsListUtils.hasEnabledOptions(lodashValues(policyTagsList))); const shouldShowBillable = isPolicyExpenseChat && Permissions.canUseTags(betas) && (transactionBillable || !lodashGet(policy, 'disabledFields.defaultBillable', true)); - let description = `${translate('iou.amount')} • ${translate('iou.cash')}`; + let description = `${translate('iou.amount')}`; + if (!isDistanceRequest) { + description += ` • ${translate('iou.cash')}`; + } if (isSettled) { description += ` • ${translate('iou.settledExpensify')}`; } else if (report.isWaitingOnBankAccount) { @@ -130,7 +138,6 @@ function MoneyRequestView({report, betas, parentReport, policyCategories, should hasErrors = canEdit && TransactionUtils.hasMissingSmartscanFields(transaction); } - const isDistanceRequest = TransactionUtils.isDistanceRequest(transaction); const pendingAction = lodashGet(transaction, 'pendingAction'); const getPendingFieldAction = (fieldPath) => lodashGet(transaction, fieldPath) || pendingAction; diff --git a/src/languages/en.ts b/src/languages/en.ts index 60dae622259..22294e00d0b 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1840,11 +1840,11 @@ export default { selectSuggestedAddress: 'Please select a suggested address or use current location', }, }, - globalNavigationOptions: { - chats: 'Chats', - }, eReceipt: { guaranteed: 'Guaranteed eReceipt', transactionDate: 'Transaction date', }, + globalNavigationOptions: { + chats: 'Chats', + }, } satisfies TranslationBase; diff --git a/src/languages/es.ts b/src/languages/es.ts index 51170426feb..202a8c4c9a6 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2325,11 +2325,11 @@ export default { selectSuggestedAddress: 'Por favor, selecciona una dirección sugerida o usa la ubicación actual.', }, }, - globalNavigationOptions: { - chats: 'Chats', - }, eReceipt: { guaranteed: 'eRecibo garantizado', transactionDate: 'Fecha de transacción', }, + globalNavigationOptions: { + chats: 'Chats', + }, } satisfies EnglishTranslation; diff --git a/src/libs/DistanceRequestUtils.js b/src/libs/DistanceRequestUtils.js index 9b875fb8200..32de571c218 100644 --- a/src/libs/DistanceRequestUtils.js +++ b/src/libs/DistanceRequestUtils.js @@ -89,8 +89,7 @@ const getDistanceMerchant = (hasRoute, distanceInMeters, unit, rate, currency, t const distanceUnit = unit === CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES ? translate('common.miles') : translate('common.kilometers'); const singularDistanceUnit = unit === CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES ? translate('common.mile') : translate('common.kilometer'); const unitString = distanceInUnits === 1 ? singularDistanceUnit : distanceUnit; - - const ratePerUnit = PolicyUtils.getUnitRateValue({rate}, toLocaleDigit); + const ratePerUnit = rate ? PolicyUtils.getUnitRateValue({rate}, toLocaleDigit) : translate('common.tbd'); const currencySymbol = CurrencyUtils.getCurrencySymbol(currency) || `${currency} `; return `${distanceInUnits} ${unitString} @ ${currencySymbol}${ratePerUnit} / ${singularDistanceUnit}`; diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index c469ed02a08..7f5f6d74ed6 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -2819,9 +2819,12 @@ function setMoneyRequestReceipt(receiptPath, receiptFilename) { Onyx.merge(ONYXKEYS.IOU, {receiptPath, receiptFilename, merchant: ''}); } -function createEmptyTransaction() { +function setUpDistanceTransaction() { const transactionID = NumberUtils.rand64(); - Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, {transactionID}); + Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, { + transactionID, + comment: {type: CONST.TRANSACTION.TYPE.CUSTOM_UNIT, customUnit: {name: CONST.CUSTOM_UNITS.NAME_DISTANCE}}, + }); Onyx.merge(ONYXKEYS.IOU, {transactionID}); } @@ -2916,7 +2919,7 @@ export { setMoneyRequestBillable, setMoneyRequestParticipants, setMoneyRequestReceipt, - createEmptyTransaction, + setUpDistanceTransaction, navigateToNextPage, updateDistanceRequest, replaceReceipt, diff --git a/src/pages/iou/NewDistanceRequestPage.js b/src/pages/iou/NewDistanceRequestPage.js index 562ea66453a..c6ac7d72d5f 100644 --- a/src/pages/iou/NewDistanceRequestPage.js +++ b/src/pages/iou/NewDistanceRequestPage.js @@ -49,7 +49,7 @@ function NewDistanceRequestPage({iou, report, route}) { if (iou.transactionID) { return; } - IOU.createEmptyTransaction(); + IOU.setUpDistanceTransaction(); }, [iou.transactionID]); return ( diff --git a/src/styles/styles.js b/src/styles/styles.js index 8fa81cd98b2..ebff49b6a45 100644 --- a/src/styles/styles.js +++ b/src/styles/styles.js @@ -26,6 +26,7 @@ import * as Browser from '../libs/Browser'; import cursor from './utilities/cursor'; import userSelect from './utilities/userSelect'; import textUnderline from './utilities/textUnderline'; +import colors from './colors'; import objectFit from './utilities/objectFit'; // touchCallout is an iOS safari only property that controls the display of the callout information when you touch and hold a target @@ -3252,6 +3253,13 @@ const styles = (theme) => ({ lineHeight: variables.lineHeightXXLarge, }, + eReceiptAmount: { + ...headlineFont, + fontSize: variables.fontSizeXXXLarge, + lineHeight: variables.lineHeightXXXLarge, + color: colors.green400, + }, + eReceiptAmountLarge: { ...headlineFont, fontSize: variables.fontSizeEReceiptLarge, @@ -3278,6 +3286,7 @@ const styles = (theme) => ({ fontFamily: fontFamily.EXP_NEUE, fontSize: variables.fontSizeSmall, lineHeight: variables.lineHeightSmall, + color: colors.green400, }, eReceiptWaypointAddress: { @@ -3294,6 +3303,24 @@ const styles = (theme) => ({ color: theme.textColorfulBackground, }, + eReceiptBackground: { + ...sizing.w100, + borderRadius: 20, + position: 'absolute', + top: 0, + left: 0, + height: 540, + }, + + eReceiptPanel: { + ...spacing.p5, + ...spacing.pb8, + ...spacing.m5, + backgroundColor: colors.green800, + borderRadius: 20, + width: 335, + }, + eReceiptBackgroundThumbnail: { ...sizing.w100, position: 'absolute', diff --git a/src/styles/themes/default.js b/src/styles/themes/default.js index db4719f5548..a1971e9de40 100644 --- a/src/styles/themes/default.js +++ b/src/styles/themes/default.js @@ -1,7 +1,6 @@ /* eslint-disable no-unused-vars */ import colors from '../colors'; import SCREENS from '../../SCREENS'; -import ROUTES from '../../ROUTES'; const darkTheme = { // Figma keys