Skip to content

Commit

Permalink
Merge pull request #27204 from Expensify/neil-distance-receipts
Browse files Browse the repository at this point in the history
Show eReceipts for distance requests
  • Loading branch information
grgia authored Oct 13, 2023
2 parents ae0ac14 + 4e23ab4 commit 873a250
Show file tree
Hide file tree
Showing 13 changed files with 1,838 additions and 21 deletions.
1,635 changes: 1,635 additions & 0 deletions assets/images/eReceipt_background.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 7 additions & 2 deletions src/components/Attachments/AttachmentCarousel/CarouselItem.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand All @@ -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({
Expand All @@ -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 */
Expand Down Expand Up @@ -97,6 +101,7 @@ function CarouselItem({item, isFocused, onPress}) {
isFocused={isFocused}
onPress={onPress}
isUsedInCarousel
transactionID={item.transactionID}
/>
</View>

Expand Down
25 changes: 23 additions & 2 deletions src/components/Attachments/AttachmentView/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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,
Expand All @@ -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 = {
Expand All @@ -47,6 +55,7 @@ const defaultProps = {
onToggleKeyboard: () => {},
containerStyles: [],
isWorkspaceAvatar: false,
transactionID: '',
};

function AttachmentView({
Expand All @@ -64,9 +73,9 @@ function AttachmentView({
isFocused,
isWorkspaceAvatar,
fallbackSource,
transaction,
}) {
const [loadComplete, setLoadComplete] = useState(false);

const [imageError, setImageError] = useState(false);

useNetwork({onReconnect: () => setImageError(false)});
Expand Down Expand Up @@ -113,6 +122,10 @@ function AttachmentView({
);
}

if (TransactionUtils.isDistanceRequest(transaction)) {
return <DistanceEReceipt transaction={transaction} />;
}

// 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.
Expand Down Expand Up @@ -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);
121 changes: 121 additions & 0 deletions src/components/DistanceEReceipt.js
Original file line number Diff line number Diff line change
@@ -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 (
<View style={[styles.flex1, styles.alignItemsCenter]}>
<ScrollView
style={styles.w100}
contentContainerStyle={[styles.flexGrow1, styles.justifyContentCenter, styles.alignItemsCenter]}
>
<View style={styles.eReceiptPanel}>
<EReceiptBackground
style={styles.eReceiptBackground}
pointerEvents="none"
/>
<View style={[styles.moneyRequestViewImage, styles.mh0, styles.mt0, styles.mb5, styles.borderNone]}>
{isOffline || !thumbnailSource ? (
<PendingMapView />
) : (
<ThumbnailImage
previewSourceURL={thumbnailSource}
style={[styles.w100, styles.h100]}
isAuthTokenRequired
shouldDynamicallyResize={false}
/>
)}
</View>
<View style={[styles.mb10, styles.gap5, styles.ph2, styles.flexColumn, styles.alignItemsCenter]}>
<Text style={styles.eReceiptAmount}>{formattedTransactionAmount}</Text>
<Text style={styles.eReceiptMerchant}>{transactionMerchant}</Text>
</View>
<View style={[styles.mb10, styles.gap5, styles.ph2]}>
{_.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 (
<View
style={styles.gap1}
key={key}
>
<Text style={styles.eReceiptWaypointTitle}>{translate(descriptionKey)}</Text>
<Text style={styles.eReceiptWaypointAddress}>{waypoint.address || ''}</Text>
</View>
);
})}
<View style={styles.gap1}>
<Text style={styles.eReceiptWaypointTitle}>{translate('common.date')}</Text>
<Text style={styles.eReceiptWaypointAddress}>{transactionDate}</Text>
</View>
</View>
<View style={[styles.ph2, styles.flexRow, styles.justifyContentBetween, styles.alignItemsCenter]}>
<Icon
width={86}
height={19.25}
fill={themeColors.textBrand}
src={Expensicons.ExpensifyWordmark}
/>
<Text style={styles.eReceiptGuaranteed}>{translate('eReceipt.guaranteed')}</Text>
</View>
</View>
</ScrollView>
</View>
);
}

export default DistanceEReceipt;
DistanceEReceipt.displayName = 'DistanceEReceipt';
DistanceEReceipt.propTypes = propTypes;
DistanceEReceipt.defaultProps = defaultProps;
2 changes: 1 addition & 1 deletion src/components/ReportActionItem/MoneyRequestPreview.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
13 changes: 10 additions & 3 deletions src/components/ReportActionItem/MoneyRequestView.js
Original file line number Diff line number Diff line change
Expand Up @@ -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]);

Expand All @@ -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) {
Expand All @@ -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;

Expand Down
6 changes: 3 additions & 3 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1857,11 +1857,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;
6 changes: 3 additions & 3 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2342,11 +2342,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;
3 changes: 1 addition & 2 deletions src/libs/DistanceRequestUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}`;
Expand Down
9 changes: 6 additions & 3 deletions src/libs/actions/IOU.js
Original file line number Diff line number Diff line change
Expand Up @@ -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});
}

Expand Down Expand Up @@ -2916,7 +2919,7 @@ export {
setMoneyRequestBillable,
setMoneyRequestParticipants,
setMoneyRequestReceipt,
createEmptyTransaction,
setUpDistanceTransaction,
navigateToNextPage,
updateDistanceRequest,
replaceReceipt,
Expand Down
2 changes: 1 addition & 1 deletion src/pages/iou/NewDistanceRequestPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ function NewDistanceRequestPage({iou, report, route}) {
if (iou.transactionID) {
return;
}
IOU.createEmptyTransaction();
IOU.setUpDistanceTransaction();
}, [iou.transactionID]);

return (
Expand Down
Loading

0 comments on commit 873a250

Please sign in to comment.