diff --git a/src/libs/IOUUtils.ts b/src/libs/IOUUtils.ts index c40a3045397fd..7d373e0b33338 100644 --- a/src/libs/IOUUtils.ts +++ b/src/libs/IOUUtils.ts @@ -4,14 +4,15 @@ import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import type {OnyxInputOrEntry, PersonalDetails, Policy, Report} from '@src/types/onyx'; import type {Attendee} 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, isExpenseRequest, isPolicyExpenseChat} 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) { @@ -323,6 +324,32 @@ 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; + } + + // Check if merchant is required based on report type and participants + const isMerchantRequired = !!(isPolicyExpenseChat(report) || isExpenseRequest(report) || transaction?.participants?.some((participant) => !!participant.isPolicyExpenseChat)); + + // For scan requests, merchant is not required unless it's a split bill being edited + if (isScanRequest(transaction) && !isEditingSplitBill) { + return false; + } + + return isMerchantRequired && isMerchantMissing(transaction); +} + function navigateToConfirmationPage( iouType: IOUType, transactionID: string, @@ -371,5 +398,6 @@ export { formatCurrentUserToAttendee, navigateToParticipantPage, shouldShowReceiptEmptyState, + shouldRequireMerchant, navigateToConfirmationPage, }; diff --git a/src/pages/iou/request/step/IOURequestStepAmount.tsx b/src/pages/iou/request/step/IOURequestStepAmount.tsx index f05f4155fac40..20d8160e2a208 100644 --- a/src/pages/iou/request/step/IOURequestStepAmount.tsx +++ b/src/pages/iou/request/step/IOURequestStepAmount.tsx @@ -18,7 +18,7 @@ import useShowNotFoundPageInIOUStep from '@hooks/useShowNotFoundPageInIOUStep'; import {setTransactionReport} from '@libs/actions/Transaction'; import {convertToBackendAmount} from '@libs/CurrencyUtils'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; -import {isMovingTransactionFromTrackExpense, navigateToConfirmationPage, navigateToParticipantPage} from '@libs/IOUUtils'; +import {isMovingTransactionFromTrackExpense, navigateToConfirmationPage, navigateToParticipantPage, shouldRequireMerchant} from '@libs/IOUUtils'; import Navigation from '@libs/Navigation/Navigation'; import {getParticipantsOption, getReportOption} from '@libs/OptionsListUtils'; import {isPaidGroupPolicy} from '@libs/PolicyUtils'; @@ -286,8 +286,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)); + return; + } navigateToConfirmationPage(iouType, transactionID, reportID, backToReport); }); + return; } @@ -307,7 +313,9 @@ function IOURequestStepAmount({ const resetToDefaultWorkspace = () => { setTransactionReport(transactionID, {reportID: transactionReportID}, true); setMoneyRequestParticipantsFromReport(transactionID, activePolicyExpenseChat, currentUserPersonalDetails.accountID).then(() => { - Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(CONST.IOU.ACTION.CREATE, CONST.IOU.TYPE.SUBMIT, transactionID, activePolicyExpenseChat?.reportID)); + Navigation.navigate( + ROUTES.MONEY_REQUEST_STEP_MERCHANT.getRoute(action, CONST.IOU.TYPE.SUBMIT, transactionID, activePolicyExpenseChat?.reportID, undefined, reportActionID), + ); }); }; @@ -329,6 +337,10 @@ function IOURequestStepAmount({ const chatReportID = selectedReport?.chatReportID ?? iouReportID; 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 949fa2633c258..0caf69269ceb6 100644 --- a/src/pages/iou/request/step/IOURequestStepMerchant.tsx +++ b/src/pages/iou/request/step/IOURequestStepMerchant.tsx @@ -1,3 +1,4 @@ +import {useFocusEffect} from '@react-navigation/native'; import React, {useCallback, useEffect, useRef, useState} from 'react'; import {InteractionManager, View} from 'react-native'; import FormProvider from '@components/Form/FormProvider'; @@ -20,6 +21,7 @@ import {isValidInputLength} from '@libs/ValidationUtils'; import {setDraftSplitTransaction, setMoneyRequestMerchant, updateMoneyRequestMerchant} from '@userActions/IOU'; 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'; @@ -74,13 +76,24 @@ function IOURequestStepMerchant({ 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, undefined, undefined, Navigation.getActiveRoute())); + return; + } navigateBack(); - }, [isSaved, navigateBack]); + }, [isSaved, navigateBack, action, iouType, transactionID, reportID, backTo, isEditing]); const validate = useCallback( (value: FormOnyxValues) => { @@ -167,6 +180,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 a83880c8a8767..0523fd0df7138 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'; @@ -97,6 +98,8 @@ 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); + // We can assume that shouldAutoReport is true as the initial value is not used. shouldAutoReport is only used after the selectedReportID changes in addParticipant where we'd update shouldAutoReport too const shouldAutoReport = useRef(true); const numberOfParticipants = useRef(participants?.length ?? 0); @@ -233,6 +236,7 @@ function IOURequestStepParticipants({ (val: Participant[]) => { HttpUtils.cancelPendingRequests(READ_COMMANDS.SEARCH_FOR_REPORTS); + selectedParticipants.current = val; const firstParticipant = val.at(0); if (firstParticipant?.isSelfDM && !isSplitRequest) { @@ -367,6 +371,9 @@ function IOURequestStepParticipants({ return; } + const firstParticipant = selectedParticipants.current?.at(0); + const isMerchantRequired = !!firstParticipant?.isPolicyExpenseChat && isMerchantMissing(initialTransaction) && iouRequestType === CONST.IOU.REQUEST_TYPE.MANUAL; + const iouConfirmationPageRoute = ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute( action, iouType === CONST.IOU.TYPE.CREATE || iouType === CONST.IOU.TYPE.TRACK ? CONST.IOU.TYPE.SUBMIT : iouType, @@ -377,9 +384,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) { + ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(action, iouType, initialTransactionID, selectedReportID.current || reportID, iouConfirmationPageRoute); + } else { + 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(() => { @@ -387,11 +403,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}); } }); }); @@ -401,14 +421,15 @@ function IOURequestStepParticipants({ participants, iouType, initialTransaction, + iouRequestType, initialTransactionID, - reportID, waitForKeyboardDismiss, transactions, isMovingTransactionFromTrackExpense, allPolicies, policyForMovingExpenses, introSelected, + reportID, backTo, ], );