Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 2 additions & 30 deletions src/libs/IOUUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,14 @@ 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, isExpenseRequest, isPolicyExpenseChat} from './ReportUtils';
import {getCurrency, getTagArrayFromName, isMerchantMissing, isScanRequest} from './TransactionUtils';
import {getReportTransactions} from './ReportUtils';
import {getCurrency, getTagArrayFromName} 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) {
Expand Down Expand Up @@ -324,32 +323,6 @@ 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<Transaction> | undefined, report: OnyxInputOrEntry<Report> | 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,
Expand Down Expand Up @@ -398,6 +371,5 @@ export {
formatCurrentUserToAttendee,
navigateToParticipantPage,
shouldShowReceiptEmptyState,
shouldRequireMerchant,
navigateToConfirmationPage,
};
16 changes: 2 additions & 14 deletions src/pages/iou/request/step/IOURequestStepAmount.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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, shouldRequireMerchant} from '@libs/IOUUtils';
import {isMovingTransactionFromTrackExpense, navigateToConfirmationPage, navigateToParticipantPage} from '@libs/IOUUtils';
import Navigation from '@libs/Navigation/Navigation';
import {getParticipantsOption, getReportOption} from '@libs/OptionsListUtils';
import {isPaidGroupPolicy} from '@libs/PolicyUtils';
Expand Down Expand Up @@ -283,14 +283,8 @@ 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);
Comment on lines 285 to 286

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Enforce merchant for expense request threads

By always navigating straight to confirmation after setting participants, this flow no longer forces the merchant step even when isExpenseRequest(report) is true. The confirmation validator only treats isPolicyExpenseChat as requiring a merchant, so expense request threads (where isExpenseRequest is true but isPolicyExpenseChat is false) can now be submitted with the merchant still missing, despite IOURequestStepMerchant still validating merchant as required for expense requests. This allows policy expense requests to be created without a merchant in that context.

Useful? React with 👍 / 👎.

});

return;
}

Expand All @@ -310,9 +304,7 @@ function IOURequestStepAmount({
const resetToDefaultWorkspace = () => {
setTransactionReport(transactionID, {reportID: transactionReportID}, true);
setMoneyRequestParticipantsFromReport(transactionID, activePolicyExpenseChat, currentUserPersonalDetails.accountID).then(() => {
Navigation.navigate(
ROUTES.MONEY_REQUEST_STEP_MERCHANT.getRoute(action, CONST.IOU.TYPE.SUBMIT, transactionID, activePolicyExpenseChat?.reportID, undefined, reportActionID),
);
Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(CONST.IOU.ACTION.CREATE, CONST.IOU.TYPE.SUBMIT, transactionID, activePolicyExpenseChat?.reportID));
});
};

Expand All @@ -334,10 +326,6 @@ 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 {
Expand Down
16 changes: 1 addition & 15 deletions src/pages/iou/request/step/IOURequestStepMerchant.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
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';
Expand All @@ -21,7 +20,6 @@ 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';
Expand Down Expand Up @@ -76,24 +74,13 @@ 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, action, iouType, transactionID, reportID, backTo, isEditing]);
}, [isSaved, navigateBack]);

const validate = useCallback(
(value: FormOnyxValues<typeof ONYXKEYS.FORMS.MONEY_REQUEST_MERCHANT_FORM>) => {
Expand Down Expand Up @@ -180,7 +167,6 @@ 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')}
Expand Down
35 changes: 7 additions & 28 deletions src/pages/iou/request/step/IOURequestStepParticipants.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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, isMerchantMissing, isPerDiemRequest} from '@libs/TransactionUtils';
import {getRequestType, hasRoute, isCorporateCardTransaction, isDistanceRequest, isPerDiemRequest} from '@libs/TransactionUtils';
import MoneyRequestParticipantsSelector from '@pages/iou/request/MoneyRequestParticipantsSelector';
import {
navigateToStartStepIfScanFileCannotBeRead,
Expand All @@ -38,7 +38,6 @@ 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';
Expand Down Expand Up @@ -98,8 +97,6 @@ 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<string>(participants?.length === 1 ? (participants.at(0)?.reportID ?? reportID) : reportID);
const selectedParticipants = useRef<Participant[]>(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);
Expand Down Expand Up @@ -236,7 +233,6 @@ function IOURequestStepParticipants({
(val: Participant[]) => {
HttpUtils.cancelPendingRequests(READ_COMMANDS.SEARCH_FOR_REPORTS);

selectedParticipants.current = val;
const firstParticipant = val.at(0);

if (firstParticipant?.isSelfDM && !isSplitRequest) {
Expand Down Expand Up @@ -371,9 +367,6 @@ 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,
Expand All @@ -384,34 +377,21 @@ function IOURequestStepParticipants({
action === CONST.IOU.ACTION.SHARE ? Navigation.getActiveRoute() : undefined,
);

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,
);
}
const route = isCategorizing
? ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(action, iouType, initialTransactionID, selectedReportID.current || reportID, iouConfirmationPageRoute)
: iouConfirmationPageRoute;

Performance.markStart(CONST.TIMING.OPEN_CREATE_EXPENSE_APPROVE);
waitForKeyboardDismiss(() => {
// If the backTo parameter is set, we should navigate back to the confirmation screen that is already on the stack.
// 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 && !isMerchantRequired) {
if (backTo) {
// We don't want to compare params because we just changed the participants.
Navigation.goBack(route, {compareParams: false});
} else {
// 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});
Navigation.navigate(route);
}
});
});
Expand All @@ -421,15 +401,14 @@ function IOURequestStepParticipants({
participants,
iouType,
initialTransaction,
iouRequestType,
initialTransactionID,
reportID,
waitForKeyboardDismiss,
transactions,
isMovingTransactionFromTrackExpense,
allPolicies,
policyForMovingExpenses,
introSelected,
reportID,
backTo,
],
);
Expand Down
Loading