From 66580a9cc5937b261be456a5bd4685fc4c368a54 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Tue, 20 Jan 2026 19:09:38 +0700 Subject: [PATCH 1/2] refactor initMoneyRequest --- .../Modal/EmployeeTestDriveModal.tsx | 2 + src/libs/actions/IOU/index.ts | 4 +- src/pages/Search/SearchPage.tsx | 2 + src/pages/Share/SubmitDetailsPage.tsx | 4 +- .../useAttachmentUploadValidation.ts | 2 + .../iou/request/DistanceRequestStartPage.tsx | 3 + src/pages/iou/request/IOURequestStartPage.tsx | 3 + tests/actions/IOUTest.ts | 131 ++++++++++++++++++ 8 files changed, 149 insertions(+), 2 deletions(-) diff --git a/src/components/TestDrive/Modal/EmployeeTestDriveModal.tsx b/src/components/TestDrive/Modal/EmployeeTestDriveModal.tsx index 2322dd99b9b9..56bcd7f51148 100644 --- a/src/components/TestDrive/Modal/EmployeeTestDriveModal.tsx +++ b/src/components/TestDrive/Modal/EmployeeTestDriveModal.tsx @@ -49,6 +49,7 @@ function EmployeeTestDriveModal() { const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const personalPolicy = usePersonalPolicy(); const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: true}); + const [draftTransactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {canBeMissing: true}); const hasOnlyPersonalPolicies = useMemo(() => hasOnlyPersonalPoliciesUtil(allPolicies), [allPolicies]); const onBossEmailChange = useCallback((value: string) => { @@ -85,6 +86,7 @@ function EmployeeTestDriveModal() { currentDate, currentUserPersonalDetails, hasOnlyPersonalPolicies, + draftTransactions, }); setMoneyRequestReceipt(transactionID, source, filename, true, CONST.TEST_RECEIPT.FILE_TYPE, false, true); diff --git a/src/libs/actions/IOU/index.ts b/src/libs/actions/IOU/index.ts index 76be6b209886..8c10239dcd6e 100644 --- a/src/libs/actions/IOU/index.ts +++ b/src/libs/actions/IOU/index.ts @@ -290,6 +290,7 @@ type InitMoneyRequestParams = { lastSelectedDistanceRates?: OnyxEntry; currentUserPersonalDetails: CurrentUserPersonalDetails; hasOnlyPersonalPolicies: boolean; + draftTransactions: OnyxCollection; }; type MoneyRequestInformation = { @@ -1089,6 +1090,7 @@ function initMoneyRequest({ lastSelectedDistanceRates, currentUserPersonalDetails, hasOnlyPersonalPolicies, + draftTransactions, }: InitMoneyRequestParams) { // Generate a brand new transactionID const newTransactionID = CONST.IOU.OPTIMISTIC_TRANSACTION_ID; @@ -1099,7 +1101,7 @@ function initMoneyRequest({ const created = currentDate || format(new Date(), 'yyyy-MM-dd'); // We remove draft transactions created during multi scanning if there are some - removeDraftTransactions(true); + removeDraftTransactions(true, draftTransactions); // in case we have to re-init money request, but the IOU request type is the same with the old draft transaction, // we should keep most of the existing data by using the ONYX MERGE operation diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index 5be142631cb7..05bc1d678f62 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -123,6 +123,7 @@ function SearchPage({route}: SearchPageProps) { const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`, {canBeMissing: true}); const [personalPolicyID] = useOnyx(ONYXKEYS.PERSONAL_POLICY_ID, {canBeMissing: true}); const [personalPolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${personalPolicyID}`, {canBeMissing: true}); + const [draftTransactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {canBeMissing: true}); const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST, {canBeMissing: true}); const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: true}); const [integrationsExportTemplates] = useOnyx(ONYXKEYS.NVP_INTEGRATION_SERVER_EXPORT_TEMPLATES, {canBeMissing: true}); @@ -1035,6 +1036,7 @@ function SearchPage({route}: SearchPageProps) { currentDate, currentUserPersonalDetails, hasOnlyPersonalPolicies, + draftTransactions, }); const newReceiptFiles: ReceiptFile[] = []; diff --git a/src/pages/Share/SubmitDetailsPage.tsx b/src/pages/Share/SubmitDetailsPage.tsx index 3705b202264d..6dc4a9018e99 100644 --- a/src/pages/Share/SubmitDetailsPage.tsx +++ b/src/pages/Share/SubmitDetailsPage.tsx @@ -84,6 +84,7 @@ function SubmitDetailsPage({ const fileName = shouldUsePreValidatedFile ? getFileName(validFilesToUpload?.uri ?? CONST.ATTACHMENT_IMAGE_DEFAULT_NAME) : getFileName(currentAttachment?.content ?? ''); const fileType = shouldUsePreValidatedFile ? (validFilesToUpload?.type ?? CONST.RECEIPT_ALLOWED_FILE_TYPES.JPEG) : (currentAttachment?.mimeType ?? ''); const [hasOnlyPersonalPolicies = false] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: true, selector: hasOnlyPersonalPoliciesUtil}); + const [draftTransactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {canBeMissing: true}); useEffect(() => { if (!errorTitle || !errorMessage) { @@ -105,8 +106,9 @@ function SubmitDetailsPage({ currentDate, currentUserPersonalDetails, hasOnlyPersonalPolicies, + draftTransactions, }); - }, [reportOrAccountID, policy, personalPolicy, report, parentReport, currentDate, currentUserPersonalDetails, hasOnlyPersonalPolicies]); + }, [reportOrAccountID, policy, personalPolicy, report, parentReport, currentDate, currentUserPersonalDetails, hasOnlyPersonalPolicies, draftTransactions]); const selectedParticipants = unknownUserDetails ? [unknownUserDetails] : getMoneyRequestParticipantsFromReport(report, currentUserPersonalDetails.accountID); const participants = selectedParticipants.map((participant) => { diff --git a/src/pages/home/report/ReportActionCompose/useAttachmentUploadValidation.ts b/src/pages/home/report/ReportActionCompose/useAttachmentUploadValidation.ts index 8004a4674ec6..d55863fda5d0 100644 --- a/src/pages/home/report/ReportActionCompose/useAttachmentUploadValidation.ts +++ b/src/pages/home/report/ReportActionCompose/useAttachmentUploadValidation.ts @@ -55,6 +55,7 @@ function useAttachmentUploadValidation({ const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policy?.id}`, {canBeMissing: true}); const personalPolicy = usePersonalPolicy(); const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: true}); + const [draftTransactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {canBeMissing: true}); const hasOnlyPersonalPolicies = useMemo(() => hasOnlyPersonalPoliciesUtil(allPolicies), [allPolicies]); const reportAttachmentsContext = useContext(AttachmentModalContext); @@ -101,6 +102,7 @@ function useAttachmentUploadValidation({ currentDate, currentUserPersonalDetails, hasOnlyPersonalPolicies, + draftTransactions, }); for (const [index, file] of files.entries()) { diff --git a/src/pages/iou/request/DistanceRequestStartPage.tsx b/src/pages/iou/request/DistanceRequestStartPage.tsx index e2bd8a4a32ea..6aaf719ada45 100644 --- a/src/pages/iou/request/DistanceRequestStartPage.tsx +++ b/src/pages/iou/request/DistanceRequestStartPage.tsx @@ -58,6 +58,7 @@ function DistanceRequestStartPage({ const [lastDistanceExpenseType] = useOnyx(ONYXKEYS.NVP_LAST_DISTANCE_EXPENSE_TYPE, {canBeMissing: true}); const isLoadingSelectedTab = isLoadingOnyxValue(selectedTabResult); const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${getNonEmptyStringOnyxID(route?.params.transactionID)}`, {canBeMissing: true}); + const [draftTransactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {canBeMissing: true}); const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: false}); const [lastSelectedDistanceRates] = useOnyx(ONYXKEYS.NVP_LAST_SELECTED_DISTANCE_RATES, {canBeMissing: true}); const [currentDate] = useOnyx(ONYXKEYS.CURRENT_DATE, {canBeMissing: true}); @@ -120,6 +121,7 @@ function DistanceRequestStartPage({ lastSelectedDistanceRates, currentUserPersonalDetails, hasOnlyPersonalPolicies, + draftTransactions, }); }, [ @@ -134,6 +136,7 @@ function DistanceRequestStartPage({ lastSelectedDistanceRates, currentUserPersonalDetails, hasOnlyPersonalPolicies, + draftTransactions, ], ); diff --git a/src/pages/iou/request/IOURequestStartPage.tsx b/src/pages/iou/request/IOURequestStartPage.tsx index fb6356974cd4..8d4a41eef8ce 100644 --- a/src/pages/iou/request/IOURequestStartPage.tsx +++ b/src/pages/iou/request/IOURequestStartPage.tsx @@ -88,6 +88,7 @@ function IOURequestStartPage({ selector: transactionDraftValuesSelector, canBeMissing: true, }); + const [draftTransactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {canBeMissing: true}); const [isMultiScanEnabled, setIsMultiScanEnabled] = useState((optimisticTransactions ?? []).length > 1); const [currentDate] = useOnyx(ONYXKEYS.CURRENT_DATE, {canBeMissing: true}); const {isOffline} = useNetwork(); @@ -192,6 +193,7 @@ function IOURequestStartPage({ lastSelectedDistanceRates, currentUserPersonalDetails, hasOnlyPersonalPolicies, + draftTransactions, }); }, [ @@ -207,6 +209,7 @@ function IOURequestStartPage({ lastSelectedDistanceRates, currentUserPersonalDetails, hasOnlyPersonalPolicies, + draftTransactions, ], ); diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index 01b1bbd6ba1f..e580379ee9be 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -7880,6 +7880,7 @@ describe('actions/IOU', () => { currentDate, currentUserPersonalDetails, hasOnlyPersonalPolicies: false, + draftTransactions: undefined, }); }) .then(async () => { @@ -7902,6 +7903,7 @@ describe('actions/IOU', () => { currentDate, currentUserPersonalDetails, hasOnlyPersonalPolicies: false, + draftTransactions: undefined, }); }) .then(async () => { @@ -7925,6 +7927,7 @@ describe('actions/IOU', () => { currentDate, currentUserPersonalDetails, hasOnlyPersonalPolicies: false, + draftTransactions: undefined, }); }) .then(async () => { @@ -7934,6 +7937,134 @@ describe('actions/IOU', () => { }); }); }); + + it('should remove non-optimistic draft transactions when draftTransactions is provided', async () => { + const otherDraftTransactionID = '123456'; + const otherDraftTransaction: Transaction = { + ...createRandomTransaction(1), + transactionID: otherDraftTransactionID, + }; + + // Set up an additional draft transaction + await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${otherDraftTransactionID}`, otherDraftTransaction); + + const draftTransactions = { + [`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${otherDraftTransactionID}`]: otherDraftTransaction, + }; + + await waitForBatchedUpdates() + .then(() => { + initMoneyRequest({ + reportID: fakeReport.reportID, + policy: fakePolicy, + personalPolicy: fakePersonalPolicy, + isFromGlobalCreate: true, + newIouRequestType: CONST.IOU.REQUEST_TYPE.MANUAL, + report: fakeReport, + parentReport: fakeParentReport, + currentDate, + currentUserPersonalDetails, + hasOnlyPersonalPolicies: false, + draftTransactions, + }); + }) + .then(async () => { + // The other draft transaction should be removed (Onyx returns undefined for removed keys) + expect(await getOnyxValue(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${otherDraftTransactionID}`)).toBeUndefined(); + // The optimistic transaction should be created + expect(await getOnyxValue(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_TRANSACTION_ID}`)).toStrictEqual(transactionResult); + }); + }); + + it('should preserve optimistic transaction in draftTransactions while removing others', async () => { + const otherDraftTransactionID = '789012'; + const otherDraftTransaction: Transaction = { + ...createRandomTransaction(2), + transactionID: otherDraftTransactionID, + }; + const existingOptimisticTransaction: Transaction = { + ...createRandomTransaction(3), + transactionID: CONST.IOU.OPTIMISTIC_TRANSACTION_ID, + }; + + // Set up both draft transactions + await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${otherDraftTransactionID}`, otherDraftTransaction); + await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_TRANSACTION_ID}`, existingOptimisticTransaction); + + const draftTransactions = { + [`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${otherDraftTransactionID}`]: otherDraftTransaction, + [`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_TRANSACTION_ID}`]: existingOptimisticTransaction, + }; + + await waitForBatchedUpdates() + .then(() => { + initMoneyRequest({ + reportID: fakeReport.reportID, + policy: fakePolicy, + personalPolicy: fakePersonalPolicy, + isFromGlobalCreate: true, + newIouRequestType: CONST.IOU.REQUEST_TYPE.MANUAL, + report: fakeReport, + parentReport: fakeParentReport, + currentDate, + currentUserPersonalDetails, + hasOnlyPersonalPolicies: false, + draftTransactions, + }); + }) + .then(async () => { + // The other draft transaction should be removed (Onyx returns undefined for removed keys) + expect(await getOnyxValue(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${otherDraftTransactionID}`)).toBeUndefined(); + // The optimistic transaction should be updated with the new transaction result (not removed) + expect(await getOnyxValue(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_TRANSACTION_ID}`)).toStrictEqual(transactionResult); + }); + }); + + it('should remove multiple draft transactions when draftTransactions contains several entries', async () => { + const draftTransactionID1 = '111111'; + const draftTransactionID2 = '222222'; + const draftTransaction1: Transaction = { + ...createRandomTransaction(4), + transactionID: draftTransactionID1, + }; + const draftTransaction2: Transaction = { + ...createRandomTransaction(5), + transactionID: draftTransactionID2, + }; + + // Set up multiple draft transactions + await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${draftTransactionID1}`, draftTransaction1); + await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${draftTransactionID2}`, draftTransaction2); + + const draftTransactions = { + [`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${draftTransactionID1}`]: draftTransaction1, + [`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${draftTransactionID2}`]: draftTransaction2, + }; + + await waitForBatchedUpdates() + .then(() => { + initMoneyRequest({ + reportID: fakeReport.reportID, + policy: fakePolicy, + personalPolicy: fakePersonalPolicy, + isFromGlobalCreate: true, + newIouRequestType: CONST.IOU.REQUEST_TYPE.MANUAL, + report: fakeReport, + parentReport: fakeParentReport, + currentDate, + currentUserPersonalDetails, + hasOnlyPersonalPolicies: false, + draftTransactions, + }); + }) + .then(async () => { + // Both draft transactions should be removed (Onyx returns undefined for removed keys) + expect(await getOnyxValue(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${draftTransactionID1}`)).toBeUndefined(); + expect(await getOnyxValue(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${draftTransactionID2}`)).toBeUndefined(); + // The optimistic transaction should be created + expect(await getOnyxValue(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_TRANSACTION_ID}`)).toStrictEqual(transactionResult); + }); + }); }); describe('updateMoneyRequestAmountAndCurrency', () => { From 8a79f9e457d6b3729c2a87a54c5ae97a800da970 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Fri, 23 Jan 2026 18:23:41 +0700 Subject: [PATCH 2/2] remove draftTransactions from deps --- src/pages/Share/SubmitDetailsPage.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pages/Share/SubmitDetailsPage.tsx b/src/pages/Share/SubmitDetailsPage.tsx index 45a19b246fd0..ff597a11db64 100644 --- a/src/pages/Share/SubmitDetailsPage.tsx +++ b/src/pages/Share/SubmitDetailsPage.tsx @@ -108,7 +108,9 @@ function SubmitDetailsPage({ hasOnlyPersonalPolicies, draftTransactions, }); - }, [reportOrAccountID, policy, personalPolicy, report, parentReport, currentDate, currentUserPersonalDetails, hasOnlyPersonalPolicies, draftTransactions]); + // The draftTransactions can be changed if users update the expense, so we don't want to re-init the money request + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [reportOrAccountID, policy, personalPolicy, report, parentReport, currentDate, currentUserPersonalDetails, hasOnlyPersonalPolicies]); const selectedParticipants = unknownUserDetails ? [unknownUserDetails] : getMoneyRequestParticipantsFromReport(report, currentUserPersonalDetails.accountID); const participants = selectedParticipants.map((participant) => {