Skip to content

Commit a29650f

Browse files
authored
Merge pull request #26508 from Expensify/cmartins-replaceReceipt
Replace receipt
2 parents 8000576 + 35d51ab commit a29650f

File tree

10 files changed

+193
-13
lines changed

10 files changed

+193
-13
lines changed

src/CONST.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1352,6 +1352,7 @@ const CONST = {
13521352
DATE: 'date',
13531353
DESCRIPTION: 'description',
13541354
MERCHANT: 'merchant',
1355+
RECEIPT: 'receipt',
13551356
},
13561357
FOOTER: {
13571358
EXPENSE_MANAGEMENT_URL: `${USE_EXPENSIFY_URL}/expense-management`,

src/components/AttachmentModal.js

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, {useState, useCallback} from 'react';
1+
import React, {useState, useCallback, useRef} from 'react';
22
import PropTypes from 'prop-types';
33
import {View, Animated, Keyboard} from 'react-native';
44
import Str from 'expensify-common/lib/str';
@@ -25,6 +25,10 @@ import HeaderGap from './HeaderGap';
2525
import SafeAreaConsumer from './SafeAreaConsumer';
2626
import addEncryptedAuthTokenToURL from '../libs/addEncryptedAuthTokenToURL';
2727
import reportPropTypes from '../pages/reportPropTypes';
28+
import * as Expensicons from './Icon/Expensicons';
29+
import useWindowDimensions from '../hooks/useWindowDimensions';
30+
import Navigation from '../libs/Navigation/Navigation';
31+
import ROUTES from '../ROUTES';
2832
import useNativeDriver from '../libs/useNativeDriver';
2933

3034
/**
@@ -94,6 +98,7 @@ const defaultProps = {
9498
};
9599

96100
function AttachmentModal(props) {
101+
const onModalHideCallbackRef = useRef(null);
97102
const [isModalOpen, setIsModalOpen] = useState(props.defaultOpen);
98103
const [shouldLoadAttachment, setShouldLoadAttachment] = useState(false);
99104
const [isAttachmentInvalid, setIsAttachmentInvalid] = useState(false);
@@ -106,6 +111,8 @@ function AttachmentModal(props) {
106111
const [isConfirmButtonDisabled, setIsConfirmButtonDisabled] = useState(false);
107112
const [confirmButtonFadeAnimation] = useState(new Animated.Value(1));
108113
const [shouldShowDownloadButton, setShouldShowDownloadButton] = React.useState(true);
114+
const {windowWidth} = useWindowDimensions();
115+
109116
const [file, setFile] = useState(
110117
props.originalFileName
111118
? {
@@ -331,6 +338,10 @@ function AttachmentModal(props) {
331338
}}
332339
onModalHide={(e) => {
333340
props.onModalHide(e);
341+
if (onModalHideCallbackRef.current) {
342+
onModalHideCallbackRef.current();
343+
}
344+
334345
setShouldLoadAttachment(false);
335346
}}
336347
propagateSwipe
@@ -339,12 +350,30 @@ function AttachmentModal(props) {
339350
<HeaderWithBackButton
340351
title={props.headerTitle || translate(isAttachmentReceipt ? 'common.receipt' : 'common.attachment')}
341352
shouldShowBorderBottom
342-
shouldShowDownloadButton={props.allowDownload && shouldShowDownloadButton}
353+
shouldShowDownloadButton={props.allowDownload && shouldShowDownloadButton && !isAttachmentReceipt}
343354
onDownloadButtonPress={() => downloadAttachment(source)}
344355
shouldShowCloseButton={!props.isSmallScreenWidth}
345356
shouldShowBackButton={props.isSmallScreenWidth}
346357
onBackButtonPress={closeModal}
347358
onCloseButtonPress={closeModal}
359+
shouldShowThreeDotsButton={isAttachmentReceipt}
360+
threeDotsAnchorPosition={styles.threeDotsPopoverOffsetAttachmentModal(windowWidth)}
361+
threeDotsMenuItems={[
362+
{
363+
icon: Expensicons.Camera,
364+
text: props.translate('common.replace'),
365+
onSelected: () => {
366+
onModalHideCallbackRef.current = () => Navigation.navigate(ROUTES.getEditRequestRoute(props.report.reportID, CONST.EDIT_REQUEST_FIELD.RECEIPT));
367+
closeModal();
368+
},
369+
},
370+
{
371+
icon: Expensicons.Download,
372+
text: props.translate('common.download'),
373+
onSelected: () => downloadAttachment(source),
374+
},
375+
]}
376+
shouldOverlay
348377
/>
349378
<View style={styles.imageModalImageCenterContainer}>
350379
{!_.isEmpty(props.report) ? (

src/components/HeaderWithBackButton/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ function HeaderWithBackButton({
4747
},
4848
threeDotsMenuItems = [],
4949
children = null,
50+
shouldOverlay = false,
5051
}) {
5152
const [isDownloadButtonActive, temporarilyDisableDownloadButton] = useThrottledButtonState();
5253
const {translate} = useLocalize();
@@ -137,6 +138,7 @@ function HeaderWithBackButton({
137138
menuItems={threeDotsMenuItems}
138139
onIconPress={onThreeDotsButtonPress}
139140
anchorPosition={threeDotsAnchorPosition}
141+
shouldOverlay={shouldOverlay}
140142
/>
141143
)}
142144
{shouldShowCloseButton && (

src/components/ThreeDotsMenu/index.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ const propTypes = {
4545
horizontal: PropTypes.oneOf(_.values(CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL)),
4646
vertical: PropTypes.oneOf(_.values(CONST.MODAL.ANCHOR_ORIGIN_VERTICAL)),
4747
}),
48+
49+
/** Whether the popover menu should overlay the current view */
50+
shouldOverlay: PropTypes.bool,
4851
};
4952

5053
const defaultProps = {
@@ -57,9 +60,10 @@ const defaultProps = {
5760
horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT,
5861
vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP, // we assume that popover menu opens below the button, anchor is at TOP
5962
},
63+
shouldOverlay: false,
6064
};
6165

62-
function ThreeDotsMenu({iconTooltip, icon, iconFill, iconStyles, onIconPress, menuItems, anchorPosition, anchorAlignment}) {
66+
function ThreeDotsMenu({iconTooltip, icon, iconFill, iconStyles, onIconPress, menuItems, anchorPosition, anchorAlignment, shouldOverlay}) {
6367
const [isPopupMenuVisible, setPopupMenuVisible] = useState(false);
6468
const buttonRef = useRef(null);
6569
const {translate} = useLocalize();
@@ -106,7 +110,7 @@ function ThreeDotsMenu({iconTooltip, icon, iconFill, iconStyles, onIconPress, me
106110
anchorAlignment={anchorAlignment}
107111
onItemSelected={hidePopoverMenu}
108112
menuItems={menuItems}
109-
withoutOverlay
113+
withoutOverlay={!shouldOverlay}
110114
anchorRef={buttonRef}
111115
/>
112116
</>

src/libs/actions/IOU.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1859,6 +1859,43 @@ function payMoneyRequest(paymentType, chatReport, iouReport) {
18591859
Navigation.dismissModal(chatReport.reportID);
18601860
}
18611861

1862+
/**
1863+
* @param {String} transactionID
1864+
* @param {Object} receipt
1865+
* @param {String} filePath
1866+
*/
1867+
function replaceReceipt(transactionID, receipt, filePath) {
1868+
const transaction = lodashGet(allTransactions, 'transactionID', {});
1869+
const oldReceipt = lodashGet(transaction, 'receipt', {});
1870+
1871+
const optimisticData = [
1872+
{
1873+
onyxMethod: Onyx.METHOD.MERGE,
1874+
key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`,
1875+
value: {
1876+
receipt: {
1877+
source: filePath,
1878+
state: CONST.IOU.RECEIPT_STATE.OPEN,
1879+
},
1880+
filename: receipt.name,
1881+
},
1882+
},
1883+
];
1884+
1885+
const failureData = [
1886+
{
1887+
onyxMethod: Onyx.METHOD.MERGE,
1888+
key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`,
1889+
value: {
1890+
receipt: oldReceipt,
1891+
filename: transaction.filename,
1892+
},
1893+
},
1894+
];
1895+
1896+
API.write('ReplaceReceipt', {transactionID, receipt}, {optimisticData, failureData});
1897+
}
1898+
18621899
/**
18631900
* Initialize money request info and navigate to the MoneyRequest page
18641901
* @param {String} iouType
@@ -2017,4 +2054,5 @@ export {
20172054
setMoneyRequestReceipt,
20182055
createEmptyTransaction,
20192056
navigateToNextPage,
2057+
replaceReceipt,
20202058
};

src/pages/EditRequestPage.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import EditRequestDescriptionPage from './EditRequestDescriptionPage';
1515
import EditRequestMerchantPage from './EditRequestMerchantPage';
1616
import EditRequestCreatedPage from './EditRequestCreatedPage';
1717
import EditRequestAmountPage from './EditRequestAmountPage';
18+
import EditRequestReceiptPage from './EditRequestReceiptPage';
1819
import reportPropTypes from './reportPropTypes';
1920
import * as IOU from '../libs/actions/IOU';
2021
import * as CurrencyUtils from '../libs/CurrencyUtils';
@@ -171,6 +172,15 @@ function EditRequestPage({report, route, parentReport, policy, session}) {
171172
);
172173
}
173174

175+
if (fieldToEdit === CONST.EDIT_REQUEST_FIELD.RECEIPT) {
176+
return (
177+
<EditRequestReceiptPage
178+
route={route}
179+
transactionID={transaction.transactionID}
180+
/>
181+
);
182+
}
183+
174184
return <FullPageNotFoundView shouldShow />;
175185
}
176186

src/pages/EditRequestReceiptPage.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import React from 'react';
2+
import PropTypes from 'prop-types';
3+
import ScreenWrapper from '../components/ScreenWrapper';
4+
import HeaderWithBackButton from '../components/HeaderWithBackButton';
5+
import Navigation from '../libs/Navigation/Navigation';
6+
import useLocalize from '../hooks/useLocalize';
7+
import ReceiptSelector from './iou/ReceiptSelector';
8+
import DragAndDropProvider from '../components/DragAndDrop/Provider';
9+
10+
const propTypes = {
11+
/** React Navigation route */
12+
route: PropTypes.shape({
13+
/** Params from the route */
14+
params: PropTypes.shape({
15+
/** The type of IOU report, i.e. bill, request, send */
16+
iouType: PropTypes.string,
17+
18+
/** The report ID of the IOU */
19+
reportID: PropTypes.string,
20+
}),
21+
}).isRequired,
22+
23+
/** The id of the transaction we're editing */
24+
transactionID: PropTypes.string.isRequired,
25+
};
26+
27+
function EditRequestReceiptPage({route, transactionID}) {
28+
const {translate} = useLocalize();
29+
30+
return (
31+
<ScreenWrapper
32+
includeSafeAreaPaddingBottom={false}
33+
shouldEnableMaxHeight
34+
>
35+
<HeaderWithBackButton
36+
title={translate('common.receipt')}
37+
onBackButtonPress={Navigation.goBack}
38+
/>
39+
<DragAndDropProvider>
40+
<ReceiptSelector
41+
route={route}
42+
transactionID={transactionID}
43+
/>
44+
</DragAndDropProvider>
45+
</ScreenWrapper>
46+
);
47+
}
48+
49+
EditRequestReceiptPage.propTypes = propTypes;
50+
EditRequestReceiptPage.displayName = 'EditRequestReceiptPage';
51+
52+
export default EditRequestReceiptPage;

src/pages/iou/ReceiptSelector/index.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import useLocalize from '../../../hooks/useLocalize';
2121
import {DragAndDropContext} from '../../../components/DragAndDrop/Provider';
2222
import * as ReceiptUtils from '../../../libs/ReceiptUtils';
2323
import {iouPropTypes, iouDefaultProps} from '../propTypes';
24+
import Navigation from '../../../libs/Navigation/Navigation';
2425

2526
const propTypes = {
2627
/** Information shown to the user when a receipt is not valid */
@@ -47,6 +48,9 @@ const propTypes = {
4748

4849
/** Holds data related to Money Request view state, rather than the underlying Money Request data. */
4950
iou: iouPropTypes,
51+
52+
/** The id of the transaction we're editing */
53+
transactionID: PropTypes.string,
5054
};
5155

5256
const defaultProps = {
@@ -57,6 +61,7 @@ const defaultProps = {
5761
},
5862
report: {},
5963
iou: iouDefaultProps,
64+
transactionID: '',
6065
};
6166

6267
function ReceiptSelector(props) {
@@ -83,6 +88,13 @@ function ReceiptSelector(props) {
8388

8489
const filePath = URL.createObjectURL(file);
8590
IOU.setMoneyRequestReceipt(filePath, file.name);
91+
92+
if (props.transactionID) {
93+
IOU.replaceReceipt(props.transactionID, file, filePath);
94+
Navigation.dismissModal();
95+
return;
96+
}
97+
8698
IOU.navigateToNextPage(iou, iouType, reportID, report);
8799
};
88100

src/pages/iou/ReceiptSelector/index.native.js

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import Log from '../../../libs/Log';
2323
import * as CameraPermission from './CameraPermission';
2424
import {iouPropTypes, iouDefaultProps} from '../propTypes';
2525
import NavigationAwareCamera from './NavigationAwareCamera';
26+
import Navigation from '../../../libs/Navigation/Navigation';
27+
import * as FileUtils from '../../../libs/fileDownload/FileUtils';
2628

2729
const propTypes = {
2830
/** React Navigation route */
@@ -42,11 +44,15 @@ const propTypes = {
4244

4345
/** Holds data related to Money Request view state, rather than the underlying Money Request data. */
4446
iou: iouPropTypes,
47+
48+
/** The id of the transaction we're editing */
49+
transactionID: PropTypes.string,
4550
};
4651

4752
const defaultProps = {
4853
report: {},
4954
iou: iouDefaultProps,
55+
transactionID: '',
5056
};
5157

5258
/**
@@ -74,7 +80,7 @@ function getImagePickerOptions(type) {
7480
};
7581
}
7682

77-
function ReceiptSelector(props) {
83+
function ReceiptSelector({route, report, iou, transactionID}) {
7884
const devices = useCameraDevices('wide-angle-camera');
7985
const device = devices.back;
8086

@@ -84,9 +90,9 @@ function ReceiptSelector(props) {
8490
const isAndroidBlockedPermissionRef = useRef(false);
8591
const appState = useRef(AppState.currentState);
8692

87-
const iouType = lodashGet(props.route, 'params.iouType', '');
88-
const reportID = lodashGet(props.route, 'params.reportID', '');
89-
const pageIndex = lodashGet(props.route, 'params.pageIndex', 1);
93+
const iouType = lodashGet(route, 'params.iouType', '');
94+
const reportID = lodashGet(route, 'params.reportID', '');
95+
const pageIndex = lodashGet(route, 'params.pageIndex', 1);
9096

9197
const {translate} = useLocalize();
9298

@@ -195,14 +201,25 @@ function ReceiptSelector(props) {
195201
flash: flash ? 'on' : 'off',
196202
})
197203
.then((photo) => {
198-
IOU.setMoneyRequestReceipt(`file://${photo.path}`, photo.path);
199-
IOU.navigateToNextPage(props.iou, iouType, reportID, props.report);
204+
const filePath = `file://${photo.path}`;
205+
IOU.setMoneyRequestReceipt(filePath, photo.path);
206+
207+
if (transactionID) {
208+
FileUtils.readFileAsync(filePath, photo.path).then((receipt) => {
209+
IOU.replaceReceipt(transactionID, receipt, filePath);
210+
});
211+
212+
Navigation.dismissModal();
213+
return;
214+
}
215+
216+
IOU.navigateToNextPage(iou, iouType, reportID, report);
200217
})
201218
.catch((error) => {
202219
showCameraAlert();
203220
Log.warn('Error taking photo', error);
204221
});
205-
}, [flash, iouType, props.iou, props.report, reportID, translate]);
222+
}, [flash, iouType, iou, report, reportID, translate, transactionID]);
206223

207224
CameraPermission.getCameraPermissionStatus().then((permissionStatus) => {
208225
setPermissions(permissionStatus);
@@ -260,8 +277,18 @@ function ReceiptSelector(props) {
260277
onPress={() => {
261278
showImagePicker(launchImageLibrary)
262279
.then((receiptImage) => {
263-
IOU.setMoneyRequestReceipt(receiptImage[0].uri, receiptImage[0].fileName);
264-
IOU.navigateToNextPage(props.iou, iouType, reportID, props.report);
280+
const filePath = receiptImage[0].uri;
281+
IOU.setMoneyRequestReceipt(filePath, receiptImage[0].fileName);
282+
283+
if (transactionID) {
284+
FileUtils.readFileAsync(filePath, receiptImage[0].fileName).then((receipt) => {
285+
IOU.replaceReceipt(transactionID, receipt, filePath);
286+
});
287+
Navigation.dismissModal();
288+
return;
289+
}
290+
291+
IOU.navigateToNextPage(iou, iouType, reportID, report);
265292
})
266293
.catch(() => {
267294
Log.info('User did not select an image from gallery');

0 commit comments

Comments
 (0)