Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Show eReceipts for distance requests #27204

Merged
merged 60 commits into from
Oct 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
fabe55f
WIP render an eReceipt for distance requests
neil-marcellini Sep 11, 2023
a6b83fe
Render the receipt image
neil-marcellini Sep 11, 2023
9d7538c
Style and clean up
neil-marcellini Sep 11, 2023
bffd20b
Merge branch 'main' into neil-distance-receipts
neil-marcellini Sep 21, 2023
71689bd
Add amount and thumbnail image
neil-marcellini Sep 21, 2023
8095338
Add merchant
neil-marcellini Sep 21, 2023
d9ddb6f
Display waypoints
neil-marcellini Sep 21, 2023
71ff545
Remove unused import
neil-marcellini Sep 21, 2023
215c03e
Style e receipt merchant text
neil-marcellini Sep 21, 2023
90ad229
Style eReceipt waypoint titles
neil-marcellini Sep 21, 2023
c1aa7b3
Style eReceipt waypoint address
neil-marcellini Sep 21, 2023
5bb7b9e
Add date text
neil-marcellini Sep 21, 2023
dd0fe7e
WIP add logo
neil-marcellini Sep 21, 2023
f69bf99
Add logo and guaranteed eReceipt
neil-marcellini Sep 22, 2023
3a67ed4
Set up wrapping views with padding
neil-marcellini Sep 22, 2023
74664ce
Fix lint issues / clean up
neil-marcellini Sep 22, 2023
6e1440e
Add the eReceipt background
neil-marcellini Sep 22, 2023
f8ef144
The eReceipt is specific to distance requests
neil-marcellini Sep 22, 2023
ab4db97
Set up gap between main sections
neil-marcellini Sep 22, 2023
0954367
Spacing and layout within sections
neil-marcellini Sep 22, 2023
8b8afe9
Set width on eReceipt for large screens
neil-marcellini Sep 22, 2023
8c28a58
Center eReceipt vertically on page
neil-marcellini Sep 25, 2023
3382e90
Use colors directly since it's a specific purpose
neil-marcellini Sep 25, 2023
16f3d79
Fix missing colors import
neil-marcellini Sep 25, 2023
7aef125
Fix background color
neil-marcellini Sep 25, 2023
00108d1
Make large eReceipts scrollable
neil-marcellini Sep 25, 2023
d2305c2
Fix center eReceipt vertically with scroll view
neil-marcellini Sep 25, 2023
327109e
Add spanish translation
neil-marcellini Sep 26, 2023
001799d
Align background with top of eReceipt
neil-marcellini Sep 26, 2023
87c8457
Remove receipt image border for prettier loading
neil-marcellini Sep 26, 2023
6b1983e
Merge branch 'main' into neil-distance-receipts
neil-marcellini Sep 27, 2023
d891e79
Fix distance request next button while offline
neil-marcellini Sep 27, 2023
32d4091
Fix isDistanceRequest for optimistic transactions
neil-marcellini Sep 28, 2023
54a25c4
Use placeholders for offline distance eReceipts
neil-marcellini Sep 28, 2023
bb6aa08
Merge branch 'main' into neil-distance-receipts
neil-marcellini Sep 28, 2023
5c5f717
Use withOnyx for transaction vs deprecated utils
neil-marcellini Sep 28, 2023
db6e4da
Update comment
neil-marcellini Sep 28, 2023
7877d5e
Fix and simplify transaction access
neil-marcellini Sep 29, 2023
b2c3f9e
Fix prop type
neil-marcellini Sep 29, 2023
6963757
Fix online and offline transaction amount
neil-marcellini Sep 29, 2023
1d3a8b9
Load thumbnail only when image source is ready
neil-marcellini Sep 29, 2023
5545564
Ensure waypoints are ordered properly for eReceipt
neil-marcellini Sep 29, 2023
b62df64
Set the rate to TBD when it's unavailable
neil-marcellini Sep 29, 2023
25d66cc
Fix crash / prop types with default receipt source
neil-marcellini Oct 3, 2023
2d653b8
Fix eReceipt background on Android
neil-marcellini Oct 3, 2023
1e86e51
Fix linter
neil-marcellini Oct 3, 2023
058f38e
Prevent top of merchant text being cut off
neil-marcellini Oct 4, 2023
0a04398
Merge branch 'main' into neil-distance-receipts
neil-marcellini Oct 4, 2023
3bd0c26
Line height can be larger by convention
neil-marcellini Oct 4, 2023
50349b2
Show scroll bar at the full width of the modal
neil-marcellini Oct 4, 2023
0ae77b6
Merge branch 'main' into neil-distance-receipts
neil-marcellini Oct 5, 2023
d28fcfc
Clean up after review feedback
neil-marcellini Oct 5, 2023
93687c9
Show distance amount as TBD on request preview
neil-marcellini Oct 5, 2023
94fc6d7
Use TBD amount for distance request view
neil-marcellini Oct 5, 2023
d374539
Remove cash from distance amount field description
neil-marcellini Oct 5, 2023
70ead50
Remove possibly undefined waypoints
neil-marcellini Oct 10, 2023
712dc86
Merge branch 'main' into neil-distance-receipts
neil-marcellini Oct 10, 2023
4d49405
Merge branch 'main' into neil-distance-receipts
neil-marcellini Oct 11, 2023
8dedc7a
Merge branch 'main' into neil-distance-receipts
neil-marcellini Oct 12, 2023
4e23ab4
Fix duplicate translation key after merge
neil-marcellini Oct 12, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
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,
Comment on lines +35 to +37
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where is this being passed in? (Trying to understand patterns for ECard transactions eReceipts).

I'm having trouble finding where the URL is coming from, do you save it to the receipt key in the transaction object in the BE?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The AttachmentCarousel renders items here

const renderItem = useCallback(
({item}) => (
<CarouselItem
item={item}

renderItem is passed data from attachments

data={attachments}
CellRendererComponent={AttachmentCarouselCellRenderer}
renderItem={renderItem}

attachments are set up here

const attachmentsFromReport = extractAttachmentsFromReport(report, reportActions);

Here's where the transactionID is set

I'm having trouble finding where the URL is coming from, do you save it to the receipt key in the transaction object in the BE?

Yeah it's from transaction.filename here

const {thumbnail} = TransactionUtils.hasReceipt(transaction) ? ReceiptUtils.getThumbnailAndImageURIs(transaction.receipt.source, transaction.filename) : {};

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you! This was super helpful

}).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}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, we had a bug here - App crashes when tapping on the receipt thumbnail while receipt is generating

On native, the pending attachment uses a static image, and it has the source: 1 (that's a standard RN, see this), which is not a string so it breaks the Str.isPDF() and Str.isImage() checks.

More details: #28814

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>
neil-marcellini marked this conversation as resolved.
Show resolved Hide resolved
</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 @@ -1840,11 +1840,11 @@ export default {
selectSuggestedAddress: 'Please select a suggested address or use current location',
},
},
globalNavigationOptions: {
chats: 'Chats',
},
eReceipt: {
neil-marcellini marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -2325,11 +2325,11 @@ export default {
selectSuggestedAddress: 'Por favor, selecciona una dirección sugerida o usa la ubicación actual.',
},
},
globalNavigationOptions: {
chats: 'Chats',
},
eReceipt: {
neil-marcellini marked this conversation as resolved.
Show resolved Hide resolved
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
Loading