diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 89f39c8e5510b..a0bfeafc264a5 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -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; }, }, diff --git a/src/components/AttachmentModal.tsx b/src/components/AttachmentModal.tsx index a966d9f33a954..1c1f027a3c366 100644 --- a/src/components/AttachmentModal.tsx +++ b/src/components/AttachmentModal.tsx @@ -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'; @@ -129,6 +130,15 @@ type AttachmentModalProps = { /** A function as a child to pass modal launching methods to */ children?: React.FC; + /** 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; @@ -166,6 +176,9 @@ function AttachmentModal({ type = undefined, accountID = undefined, shouldDisableSendButton = false, + draftTransactionID, + iouAction, + iouType: iouTypeProp, attachmentLink = '', }: AttachmentModalProps) { const styles = useThemeStyles(); @@ -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; @@ -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'), diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index 65353279d3ff1..c3ec5ddb53222 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -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; @@ -197,6 +200,7 @@ function MoneyRequestConfirmationList({ shouldShowSmartScanFields = true, isEditingSplitBill, iouCurrencyCode, + isReceiptEditable, iouMerchant, selectedParticipants: selectedParticipantsProp, payeePersonalDetails: payeePersonalDetailsProp, @@ -1077,6 +1081,7 @@ function MoneyRequestConfirmationList({ unit={unit} onPDFLoadError={onPDFLoadError} onPDFPassword={onPDFPassword} + isReceiptEditable={isReceiptEditable} /> ); diff --git a/src/components/MoneyRequestConfirmationListFooter.tsx b/src/components/MoneyRequestConfirmationListFooter.tsx index e728b7adc494d..cceeb78ae615a 100644 --- a/src/components/MoneyRequestConfirmationListFooter.tsx +++ b/src/components/MoneyRequestConfirmationListFooter.tsx @@ -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; @@ -242,6 +245,7 @@ function MoneyRequestConfirmationListFooter({ unit, onPDFLoadError, onPDFPassword, + isReceiptEditable = false, }: MoneyRequestConfirmationListFooterProps) { const styles = useThemeStyles(); const {translate, toLocaleDigit} = useLocalize(); @@ -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')} @@ -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} @@ -769,7 +781,10 @@ function MoneyRequestConfirmationListFooter({ fileExtension, isDistanceRequest, transactionID, + action, + iouType, reportID, + isReceiptEditable, ], ); diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index b6521a75b9989..8e3900600cc74 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -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; diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 59cd5c5fdc81e..ad197655d367e 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -9993,6 +9993,7 @@ function navigateToStartStepIfScanFileCannotBeRead( transactionID: string, reportID: string, receiptType: string | undefined, + onFailureCallback?: () => void, ) { if (!receiptFilename || !receiptPath) { return; @@ -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; } diff --git a/src/pages/TransactionReceiptPage.tsx b/src/pages/TransactionReceiptPage.tsx index 78a35fcd163bf..85a0397ad5cba 100644 --- a/src/pages/TransactionReceiptPage.tsx +++ b/src/pages/TransactionReceiptPage.tsx @@ -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'; @@ -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; 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; 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}); } }; @@ -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 ( diff --git a/src/styles/index.ts b/src/styles/index.ts index f7e06e6fe4847..74bae618257ef 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -4578,6 +4578,8 @@ const styles = (theme: ThemeColors) => borderRadius: 16, margin: 20, overflow: 'hidden', + borderWidth: 1, + borderColor: theme.border, }, reportPreviewBox: { @@ -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%',