Skip to content
8 changes: 5 additions & 3 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1822,15 +1822,17 @@ const ROUTES = {
MIGRATED_USER_WELCOME_MODAL: 'onboarding/migrated-user-welcome',

TRANSACTION_RECEIPT: {
route: 'r/:reportID/transaction/:transactionID/receipt',
getRoute: (reportID: string | undefined, transactionID: string | undefined, readonly = false, isFromReviewDuplicates = false) => {
route: 'r/:reportID/transaction/:transactionID/receipt/:action?/:iouType?',
getRoute: (reportID: string | undefined, transactionID: string | undefined, readonly = false, isFromReviewDuplicates = false, action?: IOUAction, iouType?: IOUType) => {
if (!reportID) {
Log.warn('Invalid reportID is used to build the TRANSACTION_RECEIPT route');
}
if (!transactionID) {
Log.warn('Invalid transactionID is used to build the TRANSACTION_RECEIPT route');
}
return `r/${reportID}/transaction/${transactionID}/receipt?readonly=${readonly}${isFromReviewDuplicates ? '&isFromReviewDuplicates=true' : ''}` as const;
return `r/${reportID}/transaction/${transactionID}/receipt${action ? `/${action}` : ''}${iouType ? `/${iouType}` : ''}?readonly=${readonly}${
isFromReviewDuplicates ? '&isFromReviewDuplicates=true' : ''
}` as const;
},
},

Expand Down
25 changes: 22 additions & 3 deletions src/components/AttachmentModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {hasEReceipt, hasMissingSmartscanFields, hasReceipt, hasReceiptSource, is
import type {AvatarSource} from '@libs/UserUtils';
import variables from '@styles/variables';
import {detachReceipt} from '@userActions/IOU';
import type {IOUAction, IOUType} from '@src/CONST';
import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
import ONYXKEYS from '@src/ONYXKEYS';
Expand Down Expand Up @@ -129,6 +130,15 @@ type AttachmentModalProps = {
/** A function as a child to pass modal launching methods to */
children?: React.FC<ChildrenProps>;

/** The iou action of the expense creation flow of which we are displaying the receipt for. */
iouAction?: IOUAction;

/** The iou type of the expense creation flow of which we are displaying the receipt for. */
iouType?: IOUType;

/** The id of the draft transaction linked to the receipt. */
draftTransactionID?: string;

fallbackSource?: AvatarSource;

canEditReceipt?: boolean;
Expand Down Expand Up @@ -166,6 +176,9 @@ function AttachmentModal({
type = undefined,
accountID = undefined,
shouldDisableSendButton = false,
draftTransactionID,
iouAction,
iouType: iouTypeProp,
attachmentLink = '',
}: AttachmentModalProps) {
const styles = useThemeStyles();
Expand All @@ -185,7 +198,7 @@ function AttachmentModal({
const {shouldUseNarrowLayout} = useResponsiveLayout();
const nope = useSharedValue(false);
const isOverlayModalVisible = (isReceiptAttachment && isDeleteReceiptConfirmModalVisible) || (!isReceiptAttachment && isAttachmentInvalid);
const iouType = useMemo(() => (isTrackExpenseAction ? CONST.IOU.TYPE.TRACK : CONST.IOU.TYPE.SUBMIT), [isTrackExpenseAction]);
const iouType = useMemo(() => iouTypeProp ?? (isTrackExpenseAction ? CONST.IOU.TYPE.TRACK : CONST.IOU.TYPE.SUBMIT), [isTrackExpenseAction, iouTypeProp]);
const parentReportAction = getReportAction(report?.parentReportID, report?.parentReportActionID);
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const transactionID = (isMoneyRequestAction(parentReportAction) && getOriginalMessage(parentReportAction)?.IOUTransactionID) || CONST.DEFAULT_NUMBER_ID;
Expand Down Expand Up @@ -437,13 +450,19 @@ function AttachmentModal({
closeModal(true);
InteractionManager.runAfterInteractions(() => {
Navigation.navigate(
ROUTES.MONEY_REQUEST_STEP_SCAN.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID, report?.reportID, Navigation.getActiveRouteWithoutParams()),
ROUTES.MONEY_REQUEST_STEP_SCAN.getRoute(
iouAction ?? CONST.IOU.ACTION.EDIT,
iouType,
draftTransactionID ?? transaction?.transactionID,
report?.reportID,
Navigation.getActiveRouteWithoutParams(),
),
);
});
},
});
}
if (!isOffline && allowDownload && !isLocalSource) {
if ((!isOffline && allowDownload && !isLocalSource) || !!draftTransactionID) {
menuItems.push({
icon: Expensicons.Download,
text: translate('common.download'),
Expand Down
5 changes: 5 additions & 0 deletions src/components/MoneyRequestConfirmationList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,9 @@ type MoneyRequestConfirmationListProps = {
/** Whether the expense is in the process of being confirmed */
isConfirming?: boolean;

/** Whether the receipt can be replaced */
isReceiptEditable?: boolean;

/** The PDF load error callback */
onPDFLoadError?: () => void;

Expand All @@ -197,6 +200,7 @@ function MoneyRequestConfirmationList({
shouldShowSmartScanFields = true,
isEditingSplitBill,
iouCurrencyCode,
isReceiptEditable,
iouMerchant,
selectedParticipants: selectedParticipantsProp,
payeePersonalDetails: payeePersonalDetailsProp,
Expand Down Expand Up @@ -1077,6 +1081,7 @@ function MoneyRequestConfirmationList({
unit={unit}
onPDFLoadError={onPDFLoadError}
onPDFPassword={onPDFPassword}
isReceiptEditable={isReceiptEditable}
/>
);

Expand Down
19 changes: 17 additions & 2 deletions src/components/MoneyRequestConfirmationListFooter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,9 @@ type MoneyRequestConfirmationListFooterProps = {
/** The transaction ID */
transactionID: string | undefined;

/** Whether the receipt can be replaced */
isReceiptEditable?: boolean;

/** The unit */
unit: Unit | undefined;

Expand Down Expand Up @@ -242,6 +245,7 @@ function MoneyRequestConfirmationListFooter({
unit,
onPDFLoadError,
onPDFPassword,
isReceiptEditable = false,
}: MoneyRequestConfirmationListFooterProps) {
const styles = useThemeStyles();
const {translate, toLocaleDigit} = useLocalize();
Expand Down Expand Up @@ -706,7 +710,11 @@ function MoneyRequestConfirmationListFooter({
return;
}

Navigation.navigate(ROUTES.TRANSACTION_RECEIPT.getRoute(reportID, transactionID));
Navigation.navigate(
isReceiptEditable
? ROUTES.TRANSACTION_RECEIPT.getRoute(reportID, transactionID, undefined, undefined, action, iouType)
: ROUTES.TRANSACTION_RECEIPT.getRoute(reportID, transactionID),
);
}}
accessibilityRole={CONST.ROLE.BUTTON}
accessibilityLabel={translate('accessibilityHints.viewAttachment')}
Expand All @@ -727,7 +735,11 @@ function MoneyRequestConfirmationListFooter({
return;
}

Navigation.navigate(ROUTES.TRANSACTION_RECEIPT.getRoute(reportID, transactionID));
Navigation.navigate(
isReceiptEditable
? ROUTES.TRANSACTION_RECEIPT.getRoute(reportID, transactionID, undefined, undefined, action, iouType)
: ROUTES.TRANSACTION_RECEIPT.getRoute(reportID, transactionID),
);
}}
disabled={!shouldDisplayReceipt || isThumbnail}
accessibilityRole={CONST.ROLE.BUTTON}
Expand Down Expand Up @@ -769,7 +781,10 @@ function MoneyRequestConfirmationListFooter({
fileExtension,
isDistanceRequest,
transactionID,
action,
iouType,
reportID,
isReceiptEditable,
],
);

Expand Down
2 changes: 2 additions & 0 deletions src/libs/Navigation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1866,6 +1866,8 @@ type AuthScreensParamList = SharedScreensParamList & {
transactionID: string;
readonly?: string;
isFromReviewDuplicates?: string;
action?: IOUAction;
iouType?: IOUType;
};
[SCREENS.CONNECTION_COMPLETE]: undefined;
[NAVIGATORS.SHARE_MODAL_NAVIGATOR]: NavigatorScreenParams<ShareNavigatorParamList>;
Expand Down
5 changes: 5 additions & 0 deletions src/libs/actions/IOU.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9993,6 +9993,7 @@ function navigateToStartStepIfScanFileCannotBeRead(
transactionID: string,
reportID: string,
receiptType: string | undefined,
onFailureCallback?: () => void,
) {
if (!receiptFilename || !receiptPath) {
return;
Expand All @@ -10001,6 +10002,10 @@ function navigateToStartStepIfScanFileCannotBeRead(
const onFailure = () => {
setMoneyRequestReceipt(transactionID, '', '', true);
if (requestType === CONST.IOU.REQUEST_TYPE.MANUAL) {
if (onFailureCallback) {
onFailureCallback();
return;
}
Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_SCAN.getRoute(CONST.IOU.ACTION.CREATE, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()));
return;
}
Expand Down
84 changes: 66 additions & 18 deletions src/pages/TransactionReceiptPage.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, {useEffect} from 'react';
import {useOnyx} from 'react-native-onyx';
import AttachmentModal from '@components/AttachmentModal';
import {navigateToStartStepIfScanFileCannotBeRead} from '@libs/actions/IOU';
import {openReport} from '@libs/actions/Report';
import Navigation from '@libs/Navigation/Navigation';
import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
Expand All @@ -13,58 +14,100 @@ import {
isOneTransactionThread as isOneTransactionThreadReportUtils,
isTrackExpenseReport as isTrackExpenseReportReportUtils,
} from '@libs/ReportUtils';
import {hasEReceipt, hasReceiptSource} from '@libs/TransactionUtils';
import {getRequestType, hasEReceipt, hasReceiptSource} from '@libs/TransactionUtils';
import tryResolveUrlFromApiRoot from '@libs/tryResolveUrlFromApiRoot';
import navigationRef from '@navigation/navigationRef';
import CONST from '@src/CONST';
import NAVIGATORS from '@src/NAVIGATORS';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type SCREENS from '@src/SCREENS';

type TransactionReceiptProps = PlatformStackScreenProps<AuthScreensParamList, typeof SCREENS.TRANSACTION_RECEIPT>;

function TransactionReceipt({route}: TransactionReceiptProps) {
const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${route.params.reportID ?? CONST.DEFAULT_NUMBER_ID}`);
const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${route.params.transactionID ?? CONST.DEFAULT_NUMBER_ID}`);
const [reportMetadata = {isLoadingInitialReportActions: true}] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${route.params.reportID ?? CONST.DEFAULT_NUMBER_ID}`);
const receiptURIs = getThumbnailAndImageURIs(transaction);

const imageSource = tryResolveUrlFromApiRoot(receiptURIs.image ?? '');
const reportID = route.params.reportID;
const transactionID = route.params.transactionID;
const action = route.params.action;
const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID ?? CONST.DEFAULT_NUMBER_ID}`);
const [transactionMain] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID ?? CONST.DEFAULT_NUMBER_ID}`);
const [transactionDraft] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID ?? CONST.DEFAULT_NUMBER_ID}`);
const [reportMetadata = {isLoadingInitialReportActions: true}] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID ?? CONST.DEFAULT_NUMBER_ID}`);

const isDraftTransaction = !!action;
const transaction = isDraftTransaction ? transactionDraft : transactionMain;
const receiptURIs = getThumbnailAndImageURIs(transaction);
const isLocalFile = receiptURIs.isLocalFile;
const readonly = route.params.readonly === 'true';
const isFromReviewDuplicates = route.params.isFromReviewDuplicates === 'true';
const imageSource = isDraftTransaction ? transactionDraft?.receipt?.source : tryResolveUrlFromApiRoot(receiptURIs.image ?? '');

const parentReportAction = getReportAction(report?.parentReportID, report?.parentReportActionID);
const canEditReceipt = canEditFieldOfMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.RECEIPT);
const canDeleteReceipt = canEditFieldOfMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.RECEIPT, true);
const isEReceipt = transaction && !hasReceiptSource(transaction) && hasEReceipt(transaction);
const isTrackExpenseAction = isTrackExpenseReportReportActionsUtils(parentReportAction);
const iouType = route.params.iouType;

useEffect(() => {
if (report && transaction) {
if ((!!report && !!transaction) || isDraftTransaction) {
return;
}
openReport(route.params.reportID);
openReport(reportID);
// I'm disabling the warning, as it expects to use exhaustive deps, even though we want this useEffect to run only on the first render.
// eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, []);

const receiptPath = transaction?.receipt?.source;

useEffect(() => {
if (!isDraftTransaction || !iouType || !transaction) {
return;
}

const requestType = getRequestType(transaction);
const receiptFilename = transaction?.filename;
const receiptType = transaction?.receipt?.type;
navigateToStartStepIfScanFileCannotBeRead(
receiptFilename,
receiptPath,
() => {},
requestType,
iouType,
transactionID,
reportID,
receiptType,
() =>
Navigation.goBack(
ROUTES.MONEY_REQUEST_STEP_SCAN.getRoute(
CONST.IOU.ACTION.CREATE,
iouType,
transactionID,
reportID,
ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(action, iouType, transactionID, reportID),
),
),
);

// eslint-disable-next-line react-compiler/react-compiler
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [receiptPath]);

const onModalClose = () => {
// Receipt Page can be opened either from Reports or from Search RHP view
// We have to handle going back to correct screens, if it was opened from RHP just close the modal, otherwise go to Report Page
const rootState = navigationRef.getRootState() as State<RootNavigatorParamList>;
const secondToLastRoute = rootState.routes.at(-2);
if (secondToLastRoute?.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR) {
if (secondToLastRoute?.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR || isDraftTransaction) {
Navigation.dismissModal();
} else {
const isOneTransactionThread = isOneTransactionThreadReportUtils(report?.reportID, report?.parentReportID, parentReportAction);
const reportID = isOneTransactionThread ? report?.parentReportID : report?.reportID;
if (!reportID) {
const dismissModalReportID = isOneTransactionThread ? report?.parentReportID : report?.reportID;
if (!dismissModalReportID) {
Navigation.dismissModal();
return;
}
Navigation.dismissModalWithReport({reportID});
Navigation.dismissModalWithReport({reportID: dismissModalReportID});
}
};

Expand All @@ -73,20 +116,25 @@ function TransactionReceipt({route}: TransactionReceiptProps) {

// eslint-disable-next-line rulesdir/no-negated-variables
const shouldShowNotFoundPage =
isTrackExpenseReport || transaction?.reportID === CONST.REPORT.SPLIT_REPORTID || isFromReviewDuplicates ? !transaction : moneyRequestReportID !== transaction?.reportID;
isTrackExpenseReport || isDraftTransaction || transaction?.reportID === CONST.REPORT.SPLIT_REPORTID || isFromReviewDuplicates
? !transaction
: moneyRequestReportID !== transaction?.reportID;

return (
<AttachmentModal
source={imageSource}
isAuthTokenRequired={!isLocalFile}
isAuthTokenRequired={!isLocalFile && !isDraftTransaction}
report={report}
isReceiptAttachment
canEditReceipt={canEditReceipt && !readonly}
canDeleteReceipt={canDeleteReceipt && !readonly}
canEditReceipt={(canEditReceipt && !readonly) || isDraftTransaction}
canDeleteReceipt={canDeleteReceipt && !readonly && !isDraftTransaction}
allowDownload={!isEReceipt}
isTrackExpenseAction={isTrackExpenseAction}
originalFileName={receiptURIs?.filename}
originalFileName={isDraftTransaction ? transaction?.filename : receiptURIs?.filename}
defaultOpen
iouAction={action}
iouType={iouType}
draftTransactionID={isDraftTransaction ? transactionID : undefined}
onModalClose={onModalClose}
isLoading={!transaction && reportMetadata?.isLoadingInitialReportActions}
shouldShowNotFoundPage={shouldShowNotFoundPage}
Expand Down
2 changes: 2 additions & 0 deletions src/pages/iou/request/step/IOURequestStepConfirmation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -939,6 +939,7 @@ function IOURequestStepConfirmation({
receiptFilename={receiptFilename}
iouType={iouType}
reportID={reportID}
shouldDisplayReceipt={!isMovingTransactionFromTrackExpense && !isDistanceRequest && !isPerDiemRequest}
isPolicyExpenseChat={isPolicyExpenseChat}
policyID={getIOURequestPolicyID(transaction, report)}
bankAccountRoute={getBankAccountRoute(report)}
Expand All @@ -952,6 +953,7 @@ function IOURequestStepConfirmation({
shouldPlaySound={iouType === CONST.IOU.TYPE.PAY}
isConfirmed={isConfirmed}
isConfirming={isConfirming}
isReceiptEditable
/>
</View>
</DragAndDropProvider>
Expand Down
6 changes: 4 additions & 2 deletions src/styles/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4578,6 +4578,8 @@ const styles = (theme: ThemeColors) =>
borderRadius: 16,
margin: 20,
overflow: 'hidden',
borderWidth: 1,
borderColor: theme.border,
},

reportPreviewBox: {
Expand Down Expand Up @@ -4750,8 +4752,8 @@ const styles = (theme: ThemeColors) =>
...spacing.mh5,
...spacing.mv3,
overflow: 'hidden',
borderWidth: 2,
borderColor: theme.cardBG,
borderWidth: 1,
borderColor: theme.border,
borderRadius: variables.componentBorderRadiusLarge,
height: 180,
maxWidth: '100%',
Expand Down