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
5 changes: 5 additions & 0 deletions src/components/ParticipantPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ type ParticipantPickerProps = {
/** Whether the IOU is workspaces only */
isWorkspacesOnly?: boolean;

/** Whether to exclude P2P recipients (and the invite-by-email option) from the list. Used for negative amounts, which P2P chats don't support. */
shouldExcludeP2P?: boolean;

/** Callback fired when participants are updated */
onParticipantsAdded: (value: Participant[]) => void;

Expand All @@ -47,6 +50,7 @@ function ParticipantPicker({
isPerDiemRequest = false,
isTimeRequest = false,
isWorkspacesOnly = false,
shouldExcludeP2P = false,
onParticipantsAdded,
onFinish,
isVisible = true,
Expand All @@ -67,6 +71,7 @@ function ParticipantPicker({
isPerDiemRequest={isPerDiemRequest}
isTimeRequest={isTimeRequest}
isWorkspacesOnly={isWorkspacesOnly}
shouldExcludeP2P={shouldExcludeP2P}
onRestrictedParticipantSelected={onClose}
initiallySelectedReportID={selectedParticipant?.reportID}
shouldMoveSelectedToTop
Expand Down
23 changes: 22 additions & 1 deletion src/hooks/useParticipantSubmission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {setTransactionReport} from '@libs/actions/Transaction';
import {READ_COMMANDS} from '@libs/API/types';
import DistanceRequestUtils from '@libs/DistanceRequestUtils';
import HttpUtils from '@libs/HttpUtils';
import {isParticipantP2P} from '@libs/IOUUtils';
import Navigation from '@libs/Navigation/Navigation';
import {isGroupPolicy} from '@libs/PolicyUtils';
import {findSelfDMReportID, generateReportID, isInvoiceRoomWithID} from '@libs/ReportUtils';
Expand Down Expand Up @@ -208,12 +209,23 @@ function useParticipantSubmission({
});
};

// P2P chats don't support negative amounts. When a negative amount was entered before a participant was
// selected (e.g. "Submit it to someone" from a self DM track expense), assigning it to a P2P participant
// would fail at submit, so keep the expense on the self DM as a track expense — mirroring the guard in the
// confirmation step's handleParticipantsAdded. This doesn't apply to an expense already bound to a policy
// expense chat, which must stay on that workspace.
const shouldKeepNegativeExpenseOnSelfDM = (firstParticipant: Participant | undefined) => {
const currentTransaction = dataRef.current.initialTransaction;
const isTransactionOnPolicyExpenseChat = currentTransaction?.participants?.some((participant) => participant?.isPolicyExpenseChat);
return (currentTransaction?.amount ?? 0) < 0 && !isTransactionOnPolicyExpenseChat && isParticipantP2P(firstParticipant);
};

const addParticipant = (val: Participant[]) => {
HttpUtils.cancelPendingRequests(READ_COMMANDS.SEARCH_FOR_REPORTS);

const firstParticipant = val.at(0);

if (firstParticipant?.isSelfDM && !isSplitRequest) {
if ((firstParticipant?.isSelfDM || shouldKeepNegativeExpenseOnSelfDM(firstParticipant)) && !isSplitRequest) {
trackExpense();
return;
}
Expand Down Expand Up @@ -301,6 +313,15 @@ function useParticipantSubmission({
// (last-rendered value from dataRef) because the Onyx write from addParticipant may not have
// caused a re-render yet by the time goToNextStep is called.
const effectiveParticipants = nextParticipants ?? currentParticipants;

// ParticipantSearchResults.addSingleParticipant fires onFinish (this callback) right after onParticipantsAdded
// for any non-self row. When the negative-amount P2P fallback in addParticipant already kept the expense on the
// self DM as a track expense (and queued that navigation), skip the submit navigation here so it doesn't
// override the track flow and land the user on a submit confirmation for the self-DM draft.
if (!isSplitRequest && shouldKeepNegativeExpenseOnSelfDM(effectiveParticipants?.at(0))) {
return;
}

const isPolicyExpenseChat = effectiveParticipants?.some((participant) => participant.isPolicyExpenseChat);
if (iouType === CONST.IOU.TYPE.SPLIT && !isPolicyExpenseChat && splitTransaction?.amount && splitTransaction?.currency) {
const participantAccountIDs = effectiveParticipants?.map((participant) => participant.accountID) as number[];
Expand Down
5 changes: 5 additions & 0 deletions src/pages/iou/request/MoneyRequestParticipantsSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ type MoneyRequestParticipantsSelectorProps = {
/** Whether this is a transaction from a credit card import */
isTransactionFromCreditCardImport?: boolean;

/** Whether to exclude P2P recipients (and the invite-by-email option) from the list. Used for negative amounts, which P2P chats don't support. */
shouldExcludeP2P?: boolean;

/** Report ID of a pre-selected participant whose selection state can't be derived from the participants array (e.g. self DM with accountID 0) */
initiallySelectedReportID?: string;

Expand Down Expand Up @@ -63,6 +66,7 @@ function MoneyRequestParticipantsSelector({
isTimeRequest = false,
isWorkspacesOnly = false,
isTransactionFromCreditCardImport = false,
shouldExcludeP2P = false,
initiallySelectedReportID,
shouldMoveSelectedToTop = false,
onRestrictedParticipantSelected,
Expand Down Expand Up @@ -92,6 +96,7 @@ function MoneyRequestParticipantsSelector({
isTimeRequest={isTimeRequest}
isNative={isNative}
isTransactionFromCreditCardImport={isTransactionFromCreditCardImport}
shouldExcludeP2P={shouldExcludeP2P}
selectionListRef={selectionListRef}
textInputAutoFocus={textInputAutoFocus}
setTextInputAutoFocus={setTextInputAutoFocus}
Expand Down
8 changes: 6 additions & 2 deletions src/pages/iou/request/ParticipantSearchResults.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ type ParticipantSearchResultsProps = {
/** Whether this is a transaction from a credit card import */
isTransactionFromCreditCardImport: boolean;

/** Whether to exclude P2P recipients (and the invite-by-email option) from the list. Used for negative amounts, which P2P chats don't support. */
shouldExcludeP2P?: boolean;

/** Forwarded ref for the SelectionList — used by the parent's useImperativeHandle */
selectionListRef: Ref<SelectionListWithSectionsHandle | null>;

Expand Down Expand Up @@ -112,6 +115,7 @@ function ParticipantSearchResults({
isTimeRequest,
isNative,
isTransactionFromCreditCardImport,
shouldExcludeP2P = false,
selectionListRef,
textInputAutoFocus,
setTextInputAutoFocus,
Expand Down Expand Up @@ -166,7 +170,7 @@ function ParticipantSearchResults({
excludeLogins: CONST.EXPENSIFY_EMAILS_OBJECT,
includeOwnedWorkspaceChats: iouType === CONST.IOU.TYPE.SUBMIT || iouType === CONST.IOU.TYPE.CREATE || iouType === CONST.IOU.TYPE.SPLIT || iouType === CONST.IOU.TYPE.TRACK,
excludeNonAdminWorkspaces: action === CONST.IOU.ACTION.SHARE,
includeP2P: !isCategorizeOrShareAction && !isPerDiemRequest && !isTimeRequest && !isTransactionFromCreditCardImport,
includeP2P: !isCategorizeOrShareAction && !isPerDiemRequest && !isTimeRequest && !isTransactionFromCreditCardImport && !shouldExcludeP2P,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep self-DM visible when hiding P2P recipients

When shouldExcludeP2P is true for a negative amount, this passes includeP2P: false into the generic report filter. That filter treats includeP2P=false as “policy expense chats only” (!includeP2P && !isPolicyExpenseChat), so it also removes the self-DM even though self-DM is the valid Track destination and includeSelfDM is still requested. In the negative self-DM flow, users without a workspace chat (or users who want to keep the expense personal) can end up with no selectable Personal/self-DM option and cannot proceed from the participant step.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We also excluded self DM in original flow

includeInvoiceRooms: iouType === CONST.IOU.TYPE.INVOICE,
action,
shouldSeparateSelfDMChat: iouType !== CONST.IOU.TYPE.INVOICE,
Expand Down Expand Up @@ -217,7 +221,7 @@ function ParticipantSearchResults({
const {searchTerm, debouncedSearchTerm, setSearchTerm, availableOptions, selectedOptions, toggleSelection, areOptionsInitialized, onListEndReached, contactState} = useSearchSelector({
selectionMode: isIOUSplit ? CONST.SEARCH_SELECTOR.SELECTION_MODE_MULTI : CONST.SEARCH_SELECTOR.SELECTION_MODE_SINGLE,
searchContext: CONST.SEARCH_SELECTOR.SEARCH_CONTEXT_GENERAL,
includeUserToInvite: !isCategorizeOrShareAction && !isPerDiemRequest && !isTimeRequest && !isTransactionFromCreditCardImport,
includeUserToInvite: !isCategorizeOrShareAction && !isPerDiemRequest && !isTimeRequest && !isTransactionFromCreditCardImport && !shouldExcludeP2P,
excludeLogins: CONST.EXPENSIFY_EMAILS_OBJECT,
includeRecentReports: true,
maxRecentReportsToShow: CONST.IOU.MAX_RECENT_REPORTS_TO_SHOW,
Expand Down
16 changes: 14 additions & 2 deletions src/pages/iou/request/step/IOURequestStepConfirmation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
import {
getIsWorkspacesOnlyForTransaction,
isMovingTransactionFromTrackExpense as isMovingTransactionFromTrackExpenseIOUUtils,
isParticipantP2P,
navigateToStartMoneyRequestStep,
resolveOptimisticChatReportID,
resolveReportForMoneyRequest,
Expand Down Expand Up @@ -312,7 +313,17 @@ function IOURequestStepConfirmation({
if (!activeTransactionID) {
return;
}
if (participantsList.at(0)?.isSelfDM) {
const selectedParticipant = participantsList.at(0);
// P2P chats don't support negative amounts. When a negative amount was entered before a participant
// was selected (e.g. "Submit it to someone" from a self DM), assigning it to a P2P participant would
// fail at submit, so keep the expense on the self DM (its default) instead of assigning the P2P
// participant, stopping the user at selection rather than at submit. This only applies while the
// expense is still on the self DM — a negative expense already bound to a policy expense chat (e.g.
// global create auto-assigned the default workspace) must stay on that workspace rather than being
// silently converted into a personal track expense.
const isTransactionOnPolicyExpenseChat = transaction?.participants?.some((participant) => participant?.isPolicyExpenseChat);
const shouldKeepOnSelfDM = !!selectedParticipant?.isSelfDM || ((transaction?.amount ?? 0) < 0 && !isTransactionOnPolicyExpenseChat && isParticipantP2P(selectedParticipant));
if (shouldKeepOnSelfDM) {
setMoneyRequestParticipantsFromReport(activeTransactionID, selfDMReport, currentUserPersonalDetails.accountID);
setTransactionReport(activeTransactionID, {reportID: CONST.REPORT.UNREPORTED_REPORT_ID}, true);
navigation.setParams({iouType: CONST.IOU.TYPE.TRACK});
Expand All @@ -330,7 +341,7 @@ function IOURequestStepConfirmation({
closeParticipantPicker();
}
},
[activeTransactionID, closeParticipantPicker, currentUserPersonalDetails.accountID, navigation, selfDMReport, iouType, reportID],
[activeTransactionID, closeParticipantPicker, currentUserPersonalDetails.accountID, navigation, selfDMReport, iouType, transaction?.amount, transaction?.participants, reportID],
);

useEffect(() => {
Expand Down Expand Up @@ -902,6 +913,7 @@ function IOURequestStepConfirmation({
isPerDiemRequest={isPerDiemRequest}
isTimeRequest={isTimeRequest}
isWorkspacesOnly={getIsWorkspacesOnlyForTransaction(transaction, requestType)}
shouldExcludeP2P={(transaction?.amount ?? 0) < 0}
onParticipantsAdded={handleParticipantsAdded}
onFinish={closeParticipantPicker}
isVisible={isParticipantPickerVisible}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ function IOURequestStepParticipants({
isTimeRequest={isTime}
isWorkspacesOnly={isWorkspacesOnly}
isTransactionFromCreditCardImport={isTransactionFromCreditCardImport}
shouldExcludeP2P={(initialTransaction?.amount ?? 0) < 0}
initiallySelectedReportID={selectedParticipant?.reportID}
shouldMoveSelectedToTop
/>
Expand Down
Loading