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
14 changes: 13 additions & 1 deletion src/hooks/useParticipantSubmission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ function useParticipantSubmission({
const [lastSelectedDistanceRates] = useOnyx(ONYXKEYS.NVP_LAST_SELECTED_DISTANCE_RATES);
const selfDMReportID = findSelfDMReportID();
const [selfDMReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${selfDMReportID}`);
const [reportDrafts] = useOnyx(ONYXKEYS.COLLECTION.REPORT_DRAFT);
const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID);
const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`);
const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED);
Expand Down Expand Up @@ -120,6 +121,7 @@ function useParticipantSubmission({
lastSelectedDistanceRates,
selfDMReportID,
selfDMReport,
reportDrafts,
introSelected,
currentUserPersonalDetails,
policyForMovingExpenses,
Expand All @@ -136,6 +138,7 @@ function useParticipantSubmission({
lastSelectedDistanceRates,
selfDMReportID,
selfDMReport,
reportDrafts,
introSelected,
currentUserPersonalDetails,
policyForMovingExpenses,
Expand Down Expand Up @@ -237,7 +240,16 @@ function useParticipantSubmission({
}

// Block selecting a workspace with commuter exclusions before participants/workspace are committed.
const selectedPolicyID = firstParticipant?.policyID ?? (firstParticipant?.reportID ? getReportOrDraftReport(firstParticipant.reportID)?.policyID : undefined);
const selectedPolicyID =
firstParticipant?.policyID ??
(firstParticipant?.reportID
? getReportOrDraftReport(
firstParticipant.reportID,
undefined,
undefined,
dataRef.current.reportDrafts?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT}${firstParticipant.reportID}`] ?? {},
)?.policyID
: undefined);
if (blockDistanceRequestIfNeeded(selectedPolicyID)) {
return;
}
Expand Down
23 changes: 19 additions & 4 deletions src/pages/iou/request/step/IOURequestStepConfirmation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,10 @@ function IOURequestStepConfirmationContent({
);

const sourceReportID = transaction?.reportID ?? reportID;
const sourceReport = useMemo(() => (sourceReportID ? getReportOrDraftReport(sourceReportID) : undefined), [sourceReportID]);
const sourceReport = useMemo(
() => (sourceReportID ? getReportOrDraftReport(sourceReportID, undefined, undefined, reportDrafts?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT}${sourceReportID}`] ?? {}) : undefined),
[sourceReportID, reportDrafts],
);
const {participants: resolvedDefaultParticipants, isLoading: isLoadingDefaultParticipants} = useDefaultParticipants({sourceReport, transaction, iouType});
const hasSelectedParticipants = (transaction?.participants ?? []).some((participant) => participant?.selected);
const defaultParticipants = useMemo(() => {
Expand Down Expand Up @@ -424,7 +427,12 @@ function IOURequestStepConfirmationContent({
return;
}
const selectedParticipant = participantsList.at(0);
const selectedPolicyID = selectedParticipant?.policyID ?? (selectedParticipant?.reportID ? getReportOrDraftReport(selectedParticipant.reportID)?.policyID : undefined);
const selectedPolicyID =
selectedParticipant?.policyID ??
(selectedParticipant?.reportID
? getReportOrDraftReport(selectedParticipant.reportID, undefined, undefined, reportDrafts?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT}${selectedParticipant.reportID}`] ?? {})
?.policyID
: undefined);
if (blockDistanceRequestIfNeeded(selectedPolicyID)) {
return;
}
Expand Down Expand Up @@ -515,6 +523,7 @@ function IOURequestStepConfirmationContent({
[
activeTransactionID,
closeParticipantPicker,
reportDrafts,
currentUserPersonalDetails.accountID,
navigation,
selfDMReport,
Expand Down Expand Up @@ -560,7 +569,12 @@ function IOURequestStepConfirmationContent({
return true;
}

return !!participant?.reportID && isPolicyExpenseChatUtils(getReportOrDraftReport(participant.reportID));
return (
!!participant?.reportID &&
isPolicyExpenseChatUtils(
getReportOrDraftReport(participant.reportID, undefined, undefined, reportDrafts?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT}${participant.reportID}`] ?? {}),
)
);
});

if (isPolicyExpenseChatUtils(report)) {
Expand All @@ -573,7 +587,7 @@ function IOURequestStepConfirmationContent({
}

return hasPolicyExpenseChat(defaultParticipants);
}, [report, transaction?.participants, defaultParticipants]);
}, [report, transaction?.participants, defaultParticipants, reportDrafts]);

const isFromGlobalCreate = transaction?.isFromGlobalCreate === true || transaction?.isFromFloatingActionButton === true;

Expand Down Expand Up @@ -609,6 +623,7 @@ function IOURequestStepConfirmationContent({
const preMountedDraftReportIDRef = useRef<string | undefined>(undefined);

const {createTransaction, sendMoney, isConfirmed, setIsConfirmed, formHasBeenSubmitted} = useExpenseSubmission({
reportDrafts,
transaction,
transactions,
receiptFiles,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ function SubmitExpenseOrchestrator({
children,
}: SubmitExpenseOrchestratorProps) {
const [destinationReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${destinationReportID}`);
const [destinationReportDraft] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_DRAFT}${destinationReportID}`);
const [isConfirming, setIsConfirming] = useState(false);
const [startLocationPermissionFlow, setStartLocationPermissionFlow] = useState(false);
const confirmingSafetyTimeout = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
Expand Down Expand Up @@ -404,7 +405,7 @@ function SubmitExpenseOrchestrator({
}

if (destinationReportID) {
dismissRHPToReport(destinationReportID, runAfterDismiss);
dismissRHPToReport(destinationReportID, runAfterDismiss, destinationReportDraft ?? {});
return;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ import {endSubmitFollowUpActionSpan, setPendingSubmitFollowUpAction} from '@libs

import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
import type {Report} from '@src/types/onyx';
import type {SearchDataTypes} from '@src/types/onyx/SearchResults';
import type {EmptyObject} from '@src/types/utils/EmptyObject';

import type {OnyxEntry} from 'react-native-onyx';

function dismissOnly(runAfterDismiss: () => void) {
setPendingSubmitFollowUpAction(CONST.TELEMETRY.SUBMIT_FOLLOW_UP_ACTION.DISMISS_MODAL_ONLY);
Expand Down Expand Up @@ -77,8 +81,8 @@ function dismissSuperWideRHP(destinationReportID: string | undefined, runAfterDi
});
}

function dismissRHPToReport(reportID: string, runAfterDismiss: () => void) {
const report = getReportOrDraftReport(reportID);
function dismissRHPToReport(reportID: string, runAfterDismiss: () => void, reportDraft: OnyxEntry<Report> | EmptyObject) {
const report = getReportOrDraftReport(reportID, undefined, undefined, reportDraft);
const hasExistingTransactions = isMoneyRequestReport(report) && report?.transactionCount !== 0;

if (!hasExistingTransactions) {
Expand Down
15 changes: 11 additions & 4 deletions src/pages/iou/request/step/confirmation/useExpenseSubmission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ import type Transaction from '@src/types/onyx/Transaction';
import type DeepValueOf from '@src/types/utils/DeepValueOf';
import {isEmptyObject} from '@src/types/utils/EmptyObject';

import type {OnyxEntry} from 'react-native-onyx';
import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';

import {delegateEmailSelector} from '@selectors/Account';
Expand Down Expand Up @@ -126,6 +126,9 @@ type UseExpenseSubmissionParams = {
report: OnyxEntry<Report>;
reportID: string;

/** Draft reports, needed to resolve chats that only exist in REPORT_DRAFT (e.g. a not-yet-created workspace chat) */
Comment thread
daledah marked this conversation as resolved.
reportDrafts: OnyxCollection<Report>;

// Policy data
policy: OnyxEntry<Policy>;
policyCategories: OnyxEntry<PolicyCategories>;
Expand Down Expand Up @@ -192,6 +195,7 @@ function useExpenseSubmission(params: UseExpenseSubmissionParams) {
canEnterScanFieldsManually,
report,
reportID,
reportDrafts,
policy,
policyCategories,
isDraftPolicy,
Expand Down Expand Up @@ -257,7 +261,9 @@ function useExpenseSubmission(params: UseExpenseSubmissionParams) {
const [selfDMReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${findSelfDMReportID()}`);
const reportTransactions = useReportTransactions(report?.reportID);
const isMoneyRequestReport = isMoneyRequestReportReportUtils(report);
const currentChatReport = isMoneyRequestReport ? getReportOrDraftReport(report?.chatReportID) : report;
const currentChatReport = isMoneyRequestReport
? getReportOrDraftReport(report?.chatReportID, undefined, undefined, reportDrafts?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT}${report?.chatReportID}`] ?? {})
: report;
const isSelfDMDestination = isSelfDMSoleDestination(participants, iouType, currentUserPersonalDetails.accountID);
// A self-DM destination passes `undefined` as the chat to trackExpense, which then resolves the chat to the self-DM — a real report that is never a draft
const destinationChatReportID = isSelfDMDestination ? undefined : currentChatReport?.reportID;
Expand Down Expand Up @@ -391,6 +397,7 @@ function useExpenseSubmission(params: UseExpenseSubmissionParams) {
report,
fallbackOptimisticChatReportID,
action,
participantReportDraft: reportDrafts?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT}${participant.reportID}`] ?? {},
});
// Move-from-track (SUBMIT/CATEGORIZE/SHARE) reuses the tracked transaction's ID — mirror the builder's `existingTransactionID ?? optimisticTransactionID`.
const lastTransactionID = getExistingTransactionID(lastTransaction?.linkedTrackedExpenseReportAction) ?? lastOptimisticTransactionID;
Expand Down Expand Up @@ -688,9 +695,9 @@ function useExpenseSubmission(params: UseExpenseSubmissionParams) {
const isExpenseReport = isMoneyRequestReportReportUtils(report);
let existingChatReport = report;
if (isExpenseReport) {
existingChatReport = getReportOrDraftReport(report?.chatReportID);
existingChatReport = getReportOrDraftReport(report?.chatReportID, undefined, undefined, reportDrafts?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT}${report?.chatReportID}`] ?? {});
} else if (!report?.reportID && participant.isPolicyExpenseChat && participant.reportID) {
existingChatReport = getReportOrDraftReport(participant.reportID);
existingChatReport = getReportOrDraftReport(participant.reportID, undefined, undefined, reportDrafts?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT}${participant.reportID}`] ?? {});
}
// The recipient can be swapped without this screen remounting, so `existingChatReport` above
// can still be whoever was selected before. Use the ID confirmation committed for the current
Expand Down
15 changes: 13 additions & 2 deletions src/pages/iou/request/step/resolveChatTarget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {IOUAction, IOUType} from '@src/CONST';
import CONST from '@src/CONST';
import type {Report} from '@src/types/onyx';
import type {Participant} from '@src/types/onyx/IOU';
import type {EmptyObject} from '@src/types/utils/EmptyObject';

import type {OnyxEntry} from 'react-native-onyx';

Expand All @@ -30,6 +31,9 @@ type ResolveChatTargetForSubmitCleanupParams = {
report: OnyxEntry<Report>;
fallbackOptimisticChatReportID: string;
action: IOUAction;

/** The participant's draft report, so a not-yet-created workspace chat still resolves. Pass an empty object when there is none. */
Comment thread
daledah marked this conversation as resolved.
participantReportDraft: OnyxEntry<Report> | EmptyObject;
};

/** Pre-action scan-flow resolver: returns both the builder's optimistic ID and the cleanup nav target so they stay in lockstep. */
Expand All @@ -49,7 +53,14 @@ function resolveChatTargetForScan({iouType, participant, report, currentUserAcco
* lands on the same report the action wrote to. Falls back to `fallbackOptimisticChatReportID` (UI's
* pre-generated optimistic ID for this submission) when no existing chat resolves.
*/
function resolveChatTargetForSubmitCleanup({participant, currentUserAccountID, report, fallbackOptimisticChatReportID, action}: ResolveChatTargetForSubmitCleanupParams): ChatTarget {
function resolveChatTargetForSubmitCleanup({
participant,
currentUserAccountID,
report,
fallbackOptimisticChatReportID,
action,
participantReportDraft,
}: ResolveChatTargetForSubmitCleanupParams): ChatTarget {
if (isMoneyRequestReport(report)) {
return {report, chatReportID: fallbackOptimisticChatReportID, optimisticChatReportID: undefined};
}
Expand All @@ -76,7 +87,7 @@ function resolveChatTargetForSubmitCleanup({participant, currentUserAccountID, r
}

let chatReportID: string | undefined;
if (participant.isPolicyExpenseChat && participant.reportID && getReportOrDraftReport(participant.reportID)) {
if (participant.isPolicyExpenseChat && participant.reportID && getReportOrDraftReport(participant.reportID, undefined, undefined, participantReportDraft)) {
chatReportID = participant.reportID;
} else if (participant.accountID) {
chatReportID = getChatByParticipants([participant.accountID, currentUserAccountID])?.reportID;
Expand Down
54 changes: 54 additions & 0 deletions tests/unit/hooks/useExpenseSubmission.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ function buildParams(overrides: Partial<Parameters<typeof useExpenseSubmission>[
canEnterScanFieldsManually: false,
report: {reportID: REPORT_ID, type: CONST.REPORT.TYPE.CHAT} as Report,
reportID: REPORT_ID,
reportDrafts: {},
policy: createMock<Policy>({id: 'policy-1'}),
policyCategories: {} as PolicyCategories,
isDraftPolicy: false,
Expand Down Expand Up @@ -670,6 +671,59 @@ describe('useExpenseSubmission orchestrator-suppressed cleanup', () => {
});

describe('per diem path', () => {
// submitPerDiemExpense resolves the destination chat through getReportOrDraftReport. For an expense report
// whose chat only exists in REPORT_DRAFT, that draft now has to arrive via the reportDrafts param.
function renderPerDiemWithDraftChat(reportDrafts: Parameters<typeof useExpenseSubmission>[0]['reportDrafts']) {
const perDiemTransaction = buildPerDiemTransaction();
return renderHook(() =>
useExpenseSubmission(
buildParams({
iouType: CONST.IOU.TYPE.SUBMIT,
requestType: CONST.IOU.REQUEST_TYPE.PER_DIEM,
isPerDiemRequest: true,
transaction: perDiemTransaction,
transactions: [perDiemTransaction],
report: {reportID: 'expense-1', type: CONST.REPORT.TYPE.EXPENSE, chatReportID: 'draft-chat-1'} as Report,
reportDrafts,
}),
),
);
}

it('resolves the destination chat from the supplied reportDrafts when it has no real report', async () => {
const draftChat = {reportID: 'draft-chat-1', chatType: CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT} as Report;
const {result} = renderPerDiemWithDraftChat({[`${ONYXKEYS.COLLECTION.REPORT_DRAFT}draft-chat-1`]: draftChat});
await waitForBatchedUpdatesWithAct();

await act(async () => {
result.current.createTransaction(false, true);
});
await waitForBatchedUpdatesWithAct();

// The draft chat resolved, so no optimistic chat report has to be generated for it.
expect(mockSubmitPerDiemExpenseAction).toHaveBeenCalledWith(expect.objectContaining({optimisticChatReportID: undefined}));
});

it('ignores a draft that is only in Onyx and not in the supplied reportDrafts', async () => {
// The two sources disagree on purpose: Onyx has the draft, the param does not. The param has to win.
await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_DRAFT}draft-chat-1`, {
reportID: 'draft-chat-1',
chatType: CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT,
} as Report);

const {result} = renderPerDiemWithDraftChat({});
await waitForBatchedUpdatesWithAct();

await act(async () => {
result.current.createTransaction(false, true);
});
await waitForBatchedUpdatesWithAct();

// Nothing resolved the destination chat, so an optimistic chat report had to be generated for it.
expect(mockSubmitPerDiemExpenseAction).toHaveBeenCalled();
expect(mockSubmitPerDiemExpenseAction).not.toHaveBeenCalledWith(expect.objectContaining({optimisticChatReportID: undefined}));
});

it('keeps initial self-DM per diem tracking on submitPerDiemExpenseForSelfDM', async () => {
const perDiemTransaction = buildPerDiemTransaction();

Expand Down
Loading
Loading