diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 4f22f01b50d4..d067bc5529ec 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -1312,13 +1312,22 @@ const ROUTES = { }, MONEY_REQUEST_STEP_MERCHANT: { route: ':action/:iouType/merchant/:transactionID/:reportID/:reportActionID?', - getRoute: (action: IOUAction, iouType: IOUType, transactionID: string | undefined, reportID: string | undefined, backTo = '', reportActionID?: string) => { + getRoute: (action: IOUAction, iouType: IOUType, transactionID: string | undefined, reportID: string | undefined, backTo = '', reportActionID?: string, backToReport?: string) => { if (!transactionID || !reportID) { Log.warn('Invalid transactionID or reportID is used to build the MONEY_REQUEST_STEP_MERCHANT route'); } + let optionalRoutePart = ''; + + if (reportActionID) { + optionalRoutePart += `/${reportActionID}`; + } + if (backToReport) { + optionalRoutePart += `?backToReport=${backToReport}`; + } + // eslint-disable-next-line no-restricted-syntax -- Legacy route generation - return getUrlWithBackToParam(`${action as string}/${iouType as string}/merchant/${transactionID}/${reportID}${reportActionID ? `/${reportActionID}` : ''}`, backTo); + return getUrlWithBackToParam(`${action as string}/${iouType as string}/merchant/${transactionID}/${reportID}${optionalRoutePart}`, backTo); }, }, MONEY_REQUEST_STEP_PARTICIPANTS: { diff --git a/src/libs/IOUUtils.ts b/src/libs/IOUUtils.ts index d50b15e56eb9..fcc754008199 100644 --- a/src/libs/IOUUtils.ts +++ b/src/libs/IOUUtils.ts @@ -5,14 +5,15 @@ import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import type {OnyxInputOrEntry, PersonalDetails, Policy, Report} from '@src/types/onyx'; import type {Attendee, Participant} from '@src/types/onyx/IOU'; +import type Transaction from '@src/types/onyx/Transaction'; import SafeString from '@src/utils/SafeString'; import type {IOURequestType} from './actions/IOU'; import {getCurrencyUnit} from './CurrencyUtils'; import Navigation from './Navigation/Navigation'; import Performance from './Performance'; import {isPaidGroupPolicy} from './PolicyUtils'; -import {getReportTransactions} from './ReportUtils'; -import {getCurrency, getTagArrayFromName} from './TransactionUtils'; +import {getReportTransactions, isExpenseReport, isPolicyExpenseChat as isPolicyExpenseChatUtils} from './ReportUtils'; +import {getCurrency, getTagArrayFromName, isMerchantMissing, isScanRequest} from './TransactionUtils'; function navigateToStartMoneyRequestStep(requestType: IOURequestType, iouType: IOUType, transactionID: string, reportID: string, iouAction?: IOUAction): void { if (iouAction === CONST.IOU.ACTION.CATEGORIZE || iouAction === CONST.IOU.ACTION.SUBMIT || iouAction === CONST.IOU.ACTION.SHARE) { @@ -324,6 +325,34 @@ function formatCurrentUserToAttendee(currentUser?: PersonalDetails, reportID?: s return [initialAttendee]; } +/** + * Checks if merchant is required and missing for a transaction. + * Merchant is required for policy expense chats, expense requests, or when any participant is a policy expense chat. + * For scan requests, merchant is not required unless it's a split bill being edited. + * + * @param transaction - The transaction to check + * @param report - The report associated with the transaction + * @param isEditingSplitBill - Whether this is editing a split bill + * @returns true if merchant is required and missing, false otherwise + */ +function shouldRequireMerchant(transaction: OnyxInputOrEntry | undefined, report: OnyxInputOrEntry | undefined, isEditingSplitBill = false): boolean { + if (!transaction) { + return false; + } + + if (!isMerchantMissing(transaction)) { + return false; + } + + // For scan requests, merchant is not required unless it's a split bill being edited + if (isScanRequest(transaction) && !isEditingSplitBill) { + return false; + } + + // Check if merchant is required based on report type and participants + return !!(isPolicyExpenseChatUtils(report) || isExpenseReport(report) || transaction?.participants?.some((participant) => !!participant.isPolicyExpenseChat)); +} + function navigateToConfirmationPage( iouType: IOUType, transactionID: string, @@ -392,6 +421,7 @@ export { formatCurrentUserToAttendee, navigateToParticipantPage, shouldShowReceiptEmptyState, + shouldRequireMerchant, navigateToConfirmationPage, calculateDefaultReimbursable, }; diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 38ea1b1f4047..8da0c0a6638a 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -1846,6 +1846,7 @@ type MoneyRequestNavigatorParamList = { // eslint-disable-next-line no-restricted-syntax -- `backTo` usages in this file are legacy. Do not add new `backTo` params to screens. See contributingGuides/NAVIGATION.md backTo: Routes; reportActionID?: string; + backToReport?: string; }; [SCREENS.IOU_SEND.ENABLE_PAYMENTS]: undefined; [SCREENS.IOU_SEND.ADD_BANK_ACCOUNT]: undefined; diff --git a/src/pages/iou/request/step/IOURequestStepAmount.tsx b/src/pages/iou/request/step/IOURequestStepAmount.tsx index af510159aebb..a35c9bc87b14 100644 --- a/src/pages/iou/request/step/IOURequestStepAmount.tsx +++ b/src/pages/iou/request/step/IOURequestStepAmount.tsx @@ -21,7 +21,7 @@ import useShowNotFoundPageInIOUStep from '@hooks/useShowNotFoundPageInIOUStep'; import {setTransactionReport} from '@libs/actions/Transaction'; import {convertToBackendAmount} from '@libs/CurrencyUtils'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; -import {calculateDefaultReimbursable, isMovingTransactionFromTrackExpense, navigateToConfirmationPage, navigateToParticipantPage} from '@libs/IOUUtils'; +import {calculateDefaultReimbursable, isMovingTransactionFromTrackExpense, navigateToConfirmationPage, navigateToParticipantPage, shouldRequireMerchant} from '@libs/IOUUtils'; import Navigation from '@libs/Navigation/Navigation'; import {getParticipantsOption, getReportOption} from '@libs/OptionsListUtils'; import {getPolicyExpenseChat, getReportOrDraftReport, getTransactionDetails, isMoneyRequestReport, isPolicyExpenseChat, isSelfDM, shouldEnableNegative} from '@libs/ReportUtils'; @@ -307,8 +307,14 @@ function IOURequestStepAmount({ setSplitShares(transaction, amountInSmallestCurrencyUnits, selectedCurrency || CONST.CURRENCY.USD, participantAccountIDs); } setMoneyRequestParticipantsFromReport(transactionID, report, currentUserPersonalDetails.accountID).then(() => { + // If merchant is required and missing, navigate to merchant step first + if (shouldRequireMerchant(transaction, report, isEditingSplitBill)) { + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_MERCHANT.getRoute(action, CONST.IOU.TYPE.SUBMIT, transactionID, reportID, undefined, reportActionID, backToReport)); + return; + } navigateToConfirmationPage(iouType, transactionID, reportID, backToReport); }); + return; } @@ -324,7 +330,11 @@ function IOURequestStepAmount({ const resetToDefaultWorkspace = () => { setTransactionReport(transactionID, {reportID: transactionReportID}, true); setMoneyRequestParticipantsFromReport(transactionID, targetReport, currentUserPersonalDetails.accountID).then(() => { - Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(CONST.IOU.ACTION.CREATE, iouTypeTrackOrSubmit, transactionID, targetReport?.reportID)); + if (transactionReportID === CONST.REPORT.UNREPORTED_REPORT_ID) { + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(CONST.IOU.ACTION.CREATE, iouTypeTrackOrSubmit, transactionID, targetReport?.reportID)); + return; + } + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_MERCHANT.getRoute(action, CONST.IOU.TYPE.SUBMIT, transactionID, targetReport?.reportID, undefined, reportActionID)); }); }; @@ -346,6 +356,10 @@ function IOURequestStepAmount({ const chatReportID = selectedReport?.chatReportID ?? selectedReport?.reportID; Navigation.setNavigationActionToMicrotaskQueue(() => { + if (shouldRequireMerchant(transaction, selectedReport, isEditingSplitBill)) { + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_MERCHANT.getRoute(CONST.IOU.ACTION.CREATE, navigationIOUType, transactionID, chatReportID, undefined, reportActionID)); + return; + } Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(CONST.IOU.ACTION.CREATE, navigationIOUType, transactionID, chatReportID)); }); } else { diff --git a/src/pages/iou/request/step/IOURequestStepMerchant.tsx b/src/pages/iou/request/step/IOURequestStepMerchant.tsx index eaf970aae95d..bda0b982b561 100644 --- a/src/pages/iou/request/step/IOURequestStepMerchant.tsx +++ b/src/pages/iou/request/step/IOURequestStepMerchant.tsx @@ -1,4 +1,5 @@ -import React, {useCallback, useEffect, useRef, useState} from 'react'; +import {useFocusEffect} from '@react-navigation/native'; +import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {InteractionManager, View} from 'react-native'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; @@ -21,6 +22,7 @@ import {setMoneyRequestMerchant, updateMoneyRequestMerchant} from '@userActions/ import {setDraftSplitTransaction} from '@userActions/IOU/Split'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import INPUT_IDS from '@src/types/form/MoneyRequestMerchantForm'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; @@ -36,7 +38,7 @@ type IOURequestStepMerchantProps = WithWritableReportOrNotFoundProps !!participant.isPolicyExpenseChat); + const isMerchantRequired = useMemo(() => { + if (isEditing) { + return isPolicyExpenseChat(report) || isExpenseRequest(report); + } + return transaction?.participants?.some((participant) => !!participant.isPolicyExpenseChat); + }, [isEditing, report, transaction?.participants]); const navigateBack = useCallback(() => { Navigation.goBack(backTo); }, [backTo]); + useFocusEffect( + useCallback(() => { + setIsSaved(false); + setCurrentMerchant(initialMerchant); + }, [initialMerchant]), + ); + useEffect(() => { if (!isSaved || !shouldNavigateAfterSaveRef.current) { return; } shouldNavigateAfterSaveRef.current = false; + if (!isEditing && !backTo) { + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(action, iouType, transactionID, reportID, backToReport, undefined, Navigation.getActiveRoute())); + return; + } navigateBack(); - }, [isSaved, navigateBack]); + }, [isSaved, navigateBack, action, iouType, transactionID, reportID, backTo, isEditing, backToReport]); const validate = useCallback( (value: FormOnyxValues) => { @@ -165,6 +183,7 @@ function IOURequestStepMerchant({ inputID={INPUT_IDS.MONEY_REQUEST_MERCHANT} name={INPUT_IDS.MONEY_REQUEST_MERCHANT} defaultValue={initialMerchant} + value={currentMerchant} onValueChange={updateMerchantRef} label={translate('common.merchant')} accessibilityLabel={translate('common.merchant')} diff --git a/src/pages/iou/request/step/IOURequestStepParticipants.tsx b/src/pages/iou/request/step/IOURequestStepParticipants.tsx index ac8f1795c272..5db7907ebde5 100644 --- a/src/pages/iou/request/step/IOURequestStepParticipants.tsx +++ b/src/pages/iou/request/step/IOURequestStepParticipants.tsx @@ -22,7 +22,7 @@ import {isPaidGroupPolicy} from '@libs/PolicyUtils'; import {findSelfDMReportID, generateReportID, isInvoiceRoomWithID} from '@libs/ReportUtils'; import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils'; import {endSpan} from '@libs/telemetry/activeSpans'; -import {getRequestType, hasRoute, isCorporateCardTransaction, isDistanceRequest, isPerDiemRequest} from '@libs/TransactionUtils'; +import {getRequestType, hasRoute, isCorporateCardTransaction, isDistanceRequest, isMerchantMissing, isPerDiemRequest} from '@libs/TransactionUtils'; import MoneyRequestParticipantsSelector from '@pages/iou/request/MoneyRequestParticipantsSelector'; import { navigateToStartStepIfScanFileCannotBeRead, @@ -38,6 +38,7 @@ import {createDraftWorkspace} from '@userActions/Policy/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type {Route} from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import type {Policy} from '@src/types/onyx'; import type {Participant} from '@src/types/onyx/IOU'; @@ -88,6 +89,7 @@ function IOURequestStepParticipants({ // We need to set selectedReportID if user has navigated back from confirmation page and navigates to confirmation page with already selected participant const selectedReportID = useRef(participants?.length === 1 ? (participants.at(0)?.reportID ?? reportID) : reportID); + const selectedParticipants = useRef(participants); const numberOfParticipants = useRef(participants?.length ?? 0); const iouRequestType = getRequestType(initialTransaction); const isSplitRequest = iouType === CONST.IOU.TYPE.SPLIT; @@ -221,6 +223,7 @@ function IOURequestStepParticipants({ (val: Participant[]) => { HttpUtils.cancelPendingRequests(READ_COMMANDS.SEARCH_FOR_REPORTS); + selectedParticipants.current = val; const firstParticipant = val.at(0); if (firstParticipant?.isSelfDM && !isSplitRequest) { @@ -335,6 +338,12 @@ function IOURequestStepParticipants({ return; } + const firstParticipant = selectedParticipants.current?.at(0); + const isMerchantRequired = + !!firstParticipant?.isPolicyExpenseChat && + isMerchantMissing(initialTransaction) && + (iouRequestType === CONST.IOU.REQUEST_TYPE.MANUAL || (isMovingTransactionFromTrackExpense && iouRequestType === CONST.IOU.REQUEST_TYPE.TIME)); + const iouConfirmationPageRoute = ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute( action, iouType === CONST.IOU.TYPE.CREATE || iouType === CONST.IOU.TYPE.TRACK ? CONST.IOU.TYPE.SUBMIT : iouType, @@ -345,9 +354,18 @@ function IOURequestStepParticipants({ action === CONST.IOU.ACTION.SHARE ? Navigation.getActiveRoute() : undefined, ); - const route = isCategorizing - ? ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(action, iouType, initialTransactionID, selectedReportID.current || reportID, iouConfirmationPageRoute) - : iouConfirmationPageRoute; + let route: Route = iouConfirmationPageRoute; + + if (isCategorizing) { + route = ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(action, iouType, initialTransactionID, selectedReportID.current || reportID, iouConfirmationPageRoute); + } else if (isMerchantRequired) { + route = ROUTES.MONEY_REQUEST_STEP_MERCHANT.getRoute( + action, + iouType === CONST.IOU.TYPE.CREATE || iouType === CONST.IOU.TYPE.TRACK ? CONST.IOU.TYPE.SUBMIT : iouType, + initialTransactionID, + newReportID, + ); + } Performance.markStart(CONST.TIMING.OPEN_CREATE_EXPENSE_APPROVE); waitForKeyboardDismiss(() => { @@ -355,11 +373,15 @@ function IOURequestStepParticipants({ // We wrap navigation in setNavigationActionToMicrotaskQueue so that data loading in Onyx and navigation do not occur simultaneously, which resets the amount to 0. // More information can be found here: https://github.com/Expensify/App/issues/73728 Navigation.setNavigationActionToMicrotaskQueue(() => { - if (backTo) { + if (backTo && !isMerchantRequired) { // We don't want to compare params because we just changed the participants. Navigation.goBack(route, {compareParams: false}); } else { - Navigation.navigate(route); + // If the merchant step is required and the backTo parameter is set, we need to go back the the confirmation screen first and then navigate to the merchant page with forceReplace to remove this screen from the stack + if (isMerchantRequired && backTo) { + Navigation.goBack(); + } + Navigation.navigate(route, {forceReplace: isMerchantRequired && !!backTo}); } }); }); @@ -369,14 +391,15 @@ function IOURequestStepParticipants({ participants, iouType, initialTransaction, + iouRequestType, initialTransactionID, - reportID, waitForKeyboardDismiss, transactions, isMovingTransactionFromTrackExpense, allPolicies, policyForMovingExpenses, introSelected, + reportID, backTo, selfDMReportID, ],