diff --git a/assets/images/eReceipt_background.svg b/assets/images/eReceipt_background.svg
new file mode 100644
index 000000000000..5070ed3b2f24
--- /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 3aeef8482e2d..096b6d60d428 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 f4d3036ff802..a1b07fb99dd8 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 000000000000..7c7837b8413d
--- /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 c7ca93e87694..24f8e0b0c0e5 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 079bc64d96bf..988c61f0aad4 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 60dae6222595..22294e00d0ba 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 51170426feb9..202a8c4c9a64 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 9b875fb82004..32de571c218c 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 c469ed02a084..7f5f6d74ed67 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 562ea66453a1..c6ac7d72d5f8 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 8fa81cd98b21..ebff49b6a45c 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 db4719f5548a..a1971e9de40c 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