Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
b498090
Update create transaction Manual flow to ask for merchant after askin…
nkdengineer Nov 25, 2025
b45abed
resolve conflict
nkdengineer Dec 3, 2025
5dff273
fix participant step
nkdengineer Dec 3, 2025
56db3b1
correct shouldRequireMerchant
nkdengineer Dec 3, 2025
8a53969
fix lint
nkdengineer Dec 3, 2025
4904aaf
merge main
nkdengineer Dec 5, 2025
78ed936
Merge branch 'main' into fix/76050
nkdengineer Dec 7, 2025
66d0de0
fix navigate issue
nkdengineer Dec 7, 2025
17dbd27
Merge branch 'main' into fix/76050
nkdengineer Dec 8, 2025
316fe35
fix scan request case
nkdengineer Dec 8, 2025
65461b4
fix scan case
nkdengineer Dec 8, 2025
fc64926
resolve conflict
nkdengineer Dec 9, 2025
2b5ad5b
Merge branch 'main' into fix/76050
nkdengineer Dec 11, 2025
858426f
add backTo param
nkdengineer Dec 11, 2025
32077d6
resolve conflict
nkdengineer Dec 15, 2025
fc0971f
run prettier
nkdengineer Dec 15, 2025
b1bff4c
fix submit to someone case
nkdengineer Dec 15, 2025
bf0bc56
fix prettier
nkdengineer Dec 15, 2025
385a5f2
resolve conflict
nkdengineer Dec 17, 2025
9db4198
fix dependency
nkdengineer Dec 17, 2025
ac5d8ab
Merge branch 'main' into fix/76050
nkdengineer Dec 18, 2025
37857d1
merge main
nkdengineer Dec 21, 2025
1d5c45c
fix saving without changing merchant case
nkdengineer Dec 21, 2025
bd04925
Merge branch 'main' into fix/76050
nkdengineer Dec 24, 2025
9270039
resolve conflict
nkdengineer Dec 30, 2025
3a22f99
fix edit merchant issue
nkdengineer Dec 30, 2025
9c943ca
Merge branch 'main' into fix/76050
nkdengineer Jan 5, 2026
fed4a50
fix merchant issue and navigate issue
nkdengineer Jan 5, 2026
11d156e
resolve conflict
nkdengineer Jan 8, 2026
92b0c11
resolve conflict
nkdengineer Jan 10, 2026
1e38ac7
run prettier
nkdengineer Jan 10, 2026
678776f
resolve conflict
nkdengineer Jan 14, 2026
11c605c
resolve conflict
nkdengineer Jan 16, 2026
1944da8
remove disable lint
nkdengineer Jan 16, 2026
ed9831f
Update src/pages/iou/request/step/IOURequestStepParticipants.tsx
nkdengineer Jan 16, 2026
010c06d
Update src/pages/iou/request/step/IOURequestStepParticipants.tsx
nkdengineer Jan 16, 2026
11e6ef3
Update src/pages/iou/request/step/IOURequestStepAmount.tsx
nkdengineer Jan 16, 2026
41af1e9
Merge branch 'main' into fix/76050
nkdengineer Jan 16, 2026
874c1a4
run prettier
nkdengineer Jan 16, 2026
77f3955
resolve conflict
nkdengineer Jan 19, 2026
a7c34cf
resolve conflict
nkdengineer Jan 21, 2026
ae9d0de
fix lint
nkdengineer Jan 21, 2026
96596f6
resolve conflict
nkdengineer Jan 22, 2026
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: 30 additions & 2 deletions src/libs/IOUUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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<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 @@ -371,5 +398,6 @@ export {
formatCurrentUserToAttendee,
navigateToParticipantPage,
shouldShowReceiptEmptyState,
shouldRequireMerchant,
navigateToConfirmationPage,
};
16 changes: 14 additions & 2 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} 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';
Expand Down Expand Up @@ -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;
}

Expand All @@ -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),
);
});
};

Expand All @@ -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 {
Expand Down
16 changes: 15 additions & 1 deletion src/pages/iou/request/step/IOURequestStepMerchant.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -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<typeof ONYXKEYS.FORMS.MONEY_REQUEST_MERCHANT_FORM>) => {
Expand Down Expand Up @@ -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')}
Expand Down
35 changes: 28 additions & 7 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, isPerDiemRequest} from '@libs/TransactionUtils';
import {getRequestType, hasRoute, isCorporateCardTransaction, isDistanceRequest, isMerchantMissing, isPerDiemRequest} from '@libs/TransactionUtils';
import MoneyRequestParticipantsSelector from '@pages/iou/request/MoneyRequestParticipantsSelector';
import {
navigateToStartStepIfScanFileCannotBeRead,
Expand All @@ -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';
Expand Down Expand Up @@ -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<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 @@ -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) {
Expand Down Expand Up @@ -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,
Expand All @@ -377,21 +384,34 @@ 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(() => {
// 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) {
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});
}
});
});
Expand All @@ -401,14 +421,15 @@ function IOURequestStepParticipants({
participants,
iouType,
initialTransaction,
iouRequestType,
initialTransactionID,
reportID,
waitForKeyboardDismiss,
transactions,
isMovingTransactionFromTrackExpense,
allPolicies,
policyForMovingExpenses,
introSelected,
reportID,
backTo,
],
);
Expand Down
Loading