Skip to content

Commit

Permalink
Merge pull request #26508 from Expensify/cmartins-replaceReceipt
Browse files Browse the repository at this point in the history
Replace receipt
  • Loading branch information
luacmartins authored Sep 20, 2023
2 parents 8000576 + 35d51ab commit a29650f
Show file tree
Hide file tree
Showing 10 changed files with 193 additions and 13 deletions.
1 change: 1 addition & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1352,6 +1352,7 @@ const CONST = {
DATE: 'date',
DESCRIPTION: 'description',
MERCHANT: 'merchant',
RECEIPT: 'receipt',
},
FOOTER: {
EXPENSE_MANAGEMENT_URL: `${USE_EXPENSIFY_URL}/expense-management`,
Expand Down
33 changes: 31 additions & 2 deletions src/components/AttachmentModal.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, {useState, useCallback} from 'react';
import React, {useState, useCallback, useRef} from 'react';
import PropTypes from 'prop-types';
import {View, Animated, Keyboard} from 'react-native';
import Str from 'expensify-common/lib/str';
Expand All @@ -25,6 +25,10 @@ import HeaderGap from './HeaderGap';
import SafeAreaConsumer from './SafeAreaConsumer';
import addEncryptedAuthTokenToURL from '../libs/addEncryptedAuthTokenToURL';
import reportPropTypes from '../pages/reportPropTypes';
import * as Expensicons from './Icon/Expensicons';
import useWindowDimensions from '../hooks/useWindowDimensions';
import Navigation from '../libs/Navigation/Navigation';
import ROUTES from '../ROUTES';
import useNativeDriver from '../libs/useNativeDriver';

/**
Expand Down Expand Up @@ -94,6 +98,7 @@ const defaultProps = {
};

function AttachmentModal(props) {
const onModalHideCallbackRef = useRef(null);
const [isModalOpen, setIsModalOpen] = useState(props.defaultOpen);
const [shouldLoadAttachment, setShouldLoadAttachment] = useState(false);
const [isAttachmentInvalid, setIsAttachmentInvalid] = useState(false);
Expand All @@ -106,6 +111,8 @@ function AttachmentModal(props) {
const [isConfirmButtonDisabled, setIsConfirmButtonDisabled] = useState(false);
const [confirmButtonFadeAnimation] = useState(new Animated.Value(1));
const [shouldShowDownloadButton, setShouldShowDownloadButton] = React.useState(true);
const {windowWidth} = useWindowDimensions();

const [file, setFile] = useState(
props.originalFileName
? {
Expand Down Expand Up @@ -331,6 +338,10 @@ function AttachmentModal(props) {
}}
onModalHide={(e) => {
props.onModalHide(e);
if (onModalHideCallbackRef.current) {
onModalHideCallbackRef.current();
}

setShouldLoadAttachment(false);
}}
propagateSwipe
Expand All @@ -339,12 +350,30 @@ function AttachmentModal(props) {
<HeaderWithBackButton
title={props.headerTitle || translate(isAttachmentReceipt ? 'common.receipt' : 'common.attachment')}
shouldShowBorderBottom
shouldShowDownloadButton={props.allowDownload && shouldShowDownloadButton}
shouldShowDownloadButton={props.allowDownload && shouldShowDownloadButton && !isAttachmentReceipt}
onDownloadButtonPress={() => downloadAttachment(source)}
shouldShowCloseButton={!props.isSmallScreenWidth}
shouldShowBackButton={props.isSmallScreenWidth}
onBackButtonPress={closeModal}
onCloseButtonPress={closeModal}
shouldShowThreeDotsButton={isAttachmentReceipt}
threeDotsAnchorPosition={styles.threeDotsPopoverOffsetAttachmentModal(windowWidth)}
threeDotsMenuItems={[
{
icon: Expensicons.Camera,
text: props.translate('common.replace'),
onSelected: () => {
onModalHideCallbackRef.current = () => Navigation.navigate(ROUTES.getEditRequestRoute(props.report.reportID, CONST.EDIT_REQUEST_FIELD.RECEIPT));
closeModal();
},
},
{
icon: Expensicons.Download,
text: props.translate('common.download'),
onSelected: () => downloadAttachment(source),
},
]}
shouldOverlay
/>
<View style={styles.imageModalImageCenterContainer}>
{!_.isEmpty(props.report) ? (
Expand Down
2 changes: 2 additions & 0 deletions src/components/HeaderWithBackButton/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ function HeaderWithBackButton({
},
threeDotsMenuItems = [],
children = null,
shouldOverlay = false,
}) {
const [isDownloadButtonActive, temporarilyDisableDownloadButton] = useThrottledButtonState();
const {translate} = useLocalize();
Expand Down Expand Up @@ -137,6 +138,7 @@ function HeaderWithBackButton({
menuItems={threeDotsMenuItems}
onIconPress={onThreeDotsButtonPress}
anchorPosition={threeDotsAnchorPosition}
shouldOverlay={shouldOverlay}
/>
)}
{shouldShowCloseButton && (
Expand Down
8 changes: 6 additions & 2 deletions src/components/ThreeDotsMenu/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ const propTypes = {
horizontal: PropTypes.oneOf(_.values(CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL)),
vertical: PropTypes.oneOf(_.values(CONST.MODAL.ANCHOR_ORIGIN_VERTICAL)),
}),

/** Whether the popover menu should overlay the current view */
shouldOverlay: PropTypes.bool,
};

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

function ThreeDotsMenu({iconTooltip, icon, iconFill, iconStyles, onIconPress, menuItems, anchorPosition, anchorAlignment}) {
function ThreeDotsMenu({iconTooltip, icon, iconFill, iconStyles, onIconPress, menuItems, anchorPosition, anchorAlignment, shouldOverlay}) {
const [isPopupMenuVisible, setPopupMenuVisible] = useState(false);
const buttonRef = useRef(null);
const {translate} = useLocalize();
Expand Down Expand Up @@ -106,7 +110,7 @@ function ThreeDotsMenu({iconTooltip, icon, iconFill, iconStyles, onIconPress, me
anchorAlignment={anchorAlignment}
onItemSelected={hidePopoverMenu}
menuItems={menuItems}
withoutOverlay
withoutOverlay={!shouldOverlay}
anchorRef={buttonRef}
/>
</>
Expand Down
38 changes: 38 additions & 0 deletions src/libs/actions/IOU.js
Original file line number Diff line number Diff line change
Expand Up @@ -1859,6 +1859,43 @@ function payMoneyRequest(paymentType, chatReport, iouReport) {
Navigation.dismissModal(chatReport.reportID);
}

/**
* @param {String} transactionID
* @param {Object} receipt
* @param {String} filePath
*/
function replaceReceipt(transactionID, receipt, filePath) {
const transaction = lodashGet(allTransactions, 'transactionID', {});
const oldReceipt = lodashGet(transaction, 'receipt', {});

const optimisticData = [
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`,
value: {
receipt: {
source: filePath,
state: CONST.IOU.RECEIPT_STATE.OPEN,
},
filename: receipt.name,
},
},
];

const failureData = [
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`,
value: {
receipt: oldReceipt,
filename: transaction.filename,
},
},
];

API.write('ReplaceReceipt', {transactionID, receipt}, {optimisticData, failureData});
}

/**
* Initialize money request info and navigate to the MoneyRequest page
* @param {String} iouType
Expand Down Expand Up @@ -2017,4 +2054,5 @@ export {
setMoneyRequestReceipt,
createEmptyTransaction,
navigateToNextPage,
replaceReceipt,
};
10 changes: 10 additions & 0 deletions src/pages/EditRequestPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import EditRequestDescriptionPage from './EditRequestDescriptionPage';
import EditRequestMerchantPage from './EditRequestMerchantPage';
import EditRequestCreatedPage from './EditRequestCreatedPage';
import EditRequestAmountPage from './EditRequestAmountPage';
import EditRequestReceiptPage from './EditRequestReceiptPage';
import reportPropTypes from './reportPropTypes';
import * as IOU from '../libs/actions/IOU';
import * as CurrencyUtils from '../libs/CurrencyUtils';
Expand Down Expand Up @@ -171,6 +172,15 @@ function EditRequestPage({report, route, parentReport, policy, session}) {
);
}

if (fieldToEdit === CONST.EDIT_REQUEST_FIELD.RECEIPT) {
return (
<EditRequestReceiptPage
route={route}
transactionID={transaction.transactionID}
/>
);
}

return <FullPageNotFoundView shouldShow />;
}

Expand Down
52 changes: 52 additions & 0 deletions src/pages/EditRequestReceiptPage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import React from 'react';
import PropTypes from 'prop-types';
import ScreenWrapper from '../components/ScreenWrapper';
import HeaderWithBackButton from '../components/HeaderWithBackButton';
import Navigation from '../libs/Navigation/Navigation';
import useLocalize from '../hooks/useLocalize';
import ReceiptSelector from './iou/ReceiptSelector';
import DragAndDropProvider from '../components/DragAndDrop/Provider';

const propTypes = {
/** React Navigation route */
route: PropTypes.shape({
/** Params from the route */
params: PropTypes.shape({
/** The type of IOU report, i.e. bill, request, send */
iouType: PropTypes.string,

/** The report ID of the IOU */
reportID: PropTypes.string,
}),
}).isRequired,

/** The id of the transaction we're editing */
transactionID: PropTypes.string.isRequired,
};

function EditRequestReceiptPage({route, transactionID}) {
const {translate} = useLocalize();

return (
<ScreenWrapper
includeSafeAreaPaddingBottom={false}
shouldEnableMaxHeight
>
<HeaderWithBackButton
title={translate('common.receipt')}
onBackButtonPress={Navigation.goBack}
/>
<DragAndDropProvider>
<ReceiptSelector
route={route}
transactionID={transactionID}
/>
</DragAndDropProvider>
</ScreenWrapper>
);
}

EditRequestReceiptPage.propTypes = propTypes;
EditRequestReceiptPage.displayName = 'EditRequestReceiptPage';

export default EditRequestReceiptPage;
12 changes: 12 additions & 0 deletions src/pages/iou/ReceiptSelector/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import useLocalize from '../../../hooks/useLocalize';
import {DragAndDropContext} from '../../../components/DragAndDrop/Provider';
import * as ReceiptUtils from '../../../libs/ReceiptUtils';
import {iouPropTypes, iouDefaultProps} from '../propTypes';
import Navigation from '../../../libs/Navigation/Navigation';

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

/** Holds data related to Money Request view state, rather than the underlying Money Request data. */
iou: iouPropTypes,

/** The id of the transaction we're editing */
transactionID: PropTypes.string,
};

const defaultProps = {
Expand All @@ -57,6 +61,7 @@ const defaultProps = {
},
report: {},
iou: iouDefaultProps,
transactionID: '',
};

function ReceiptSelector(props) {
Expand All @@ -83,6 +88,13 @@ function ReceiptSelector(props) {

const filePath = URL.createObjectURL(file);
IOU.setMoneyRequestReceipt(filePath, file.name);

if (props.transactionID) {
IOU.replaceReceipt(props.transactionID, file, filePath);
Navigation.dismissModal();
return;
}

IOU.navigateToNextPage(iou, iouType, reportID, report);
};

Expand Down
45 changes: 36 additions & 9 deletions src/pages/iou/ReceiptSelector/index.native.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import Log from '../../../libs/Log';
import * as CameraPermission from './CameraPermission';
import {iouPropTypes, iouDefaultProps} from '../propTypes';
import NavigationAwareCamera from './NavigationAwareCamera';
import Navigation from '../../../libs/Navigation/Navigation';
import * as FileUtils from '../../../libs/fileDownload/FileUtils';

const propTypes = {
/** React Navigation route */
Expand All @@ -42,11 +44,15 @@ const propTypes = {

/** Holds data related to Money Request view state, rather than the underlying Money Request data. */
iou: iouPropTypes,

/** The id of the transaction we're editing */
transactionID: PropTypes.string,
};

const defaultProps = {
report: {},
iou: iouDefaultProps,
transactionID: '',
};

/**
Expand Down Expand Up @@ -74,7 +80,7 @@ function getImagePickerOptions(type) {
};
}

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

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

const iouType = lodashGet(props.route, 'params.iouType', '');
const reportID = lodashGet(props.route, 'params.reportID', '');
const pageIndex = lodashGet(props.route, 'params.pageIndex', 1);
const iouType = lodashGet(route, 'params.iouType', '');
const reportID = lodashGet(route, 'params.reportID', '');
const pageIndex = lodashGet(route, 'params.pageIndex', 1);

const {translate} = useLocalize();

Expand Down Expand Up @@ -195,14 +201,25 @@ function ReceiptSelector(props) {
flash: flash ? 'on' : 'off',
})
.then((photo) => {
IOU.setMoneyRequestReceipt(`file://${photo.path}`, photo.path);
IOU.navigateToNextPage(props.iou, iouType, reportID, props.report);
const filePath = `file://${photo.path}`;
IOU.setMoneyRequestReceipt(filePath, photo.path);

if (transactionID) {
FileUtils.readFileAsync(filePath, photo.path).then((receipt) => {
IOU.replaceReceipt(transactionID, receipt, filePath);
});

Navigation.dismissModal();
return;
}

IOU.navigateToNextPage(iou, iouType, reportID, report);
})
.catch((error) => {
showCameraAlert();
Log.warn('Error taking photo', error);
});
}, [flash, iouType, props.iou, props.report, reportID, translate]);
}, [flash, iouType, iou, report, reportID, translate, transactionID]);

CameraPermission.getCameraPermissionStatus().then((permissionStatus) => {
setPermissions(permissionStatus);
Expand Down Expand Up @@ -260,8 +277,18 @@ function ReceiptSelector(props) {
onPress={() => {
showImagePicker(launchImageLibrary)
.then((receiptImage) => {
IOU.setMoneyRequestReceipt(receiptImage[0].uri, receiptImage[0].fileName);
IOU.navigateToNextPage(props.iou, iouType, reportID, props.report);
const filePath = receiptImage[0].uri;
IOU.setMoneyRequestReceipt(filePath, receiptImage[0].fileName);

if (transactionID) {
FileUtils.readFileAsync(filePath, receiptImage[0].fileName).then((receipt) => {
IOU.replaceReceipt(transactionID, receipt, filePath);
});
Navigation.dismissModal();
return;
}

IOU.navigateToNextPage(iou, iouType, reportID, report);
})
.catch(() => {
Log.info('User did not select an image from gallery');
Expand Down
Loading

0 comments on commit a29650f

Please sign in to comment.