From 596f0c0cc92bd2ebb1f6fac863ce76a01ef721f9 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Thu, 12 Mar 2026 11:30:59 +0700 Subject: [PATCH 1/9] clean up allTransactionDrafts --- src/hooks/useRestartOnReceiptFailure.ts | 9 ++- src/libs/ReportUtils.ts | 11 ++- src/libs/actions/IOU/index.ts | 12 ++- src/libs/actions/Transaction.ts | 14 ---- src/libs/actions/TransactionEdit.ts | 25 ++---- .../step/IOURequestStepConfirmation.tsx | 4 +- .../request/step/IOURequestStepScan/index.tsx | 6 +- .../step/IOURequestStepScan/useReceiptScan.ts | 9 ++- tests/unit/hooks/useReceiptScan.test.ts | 80 +++++++++++++++++-- 9 files changed, 118 insertions(+), 52 deletions(-) diff --git a/src/hooks/useRestartOnReceiptFailure.ts b/src/hooks/useRestartOnReceiptFailure.ts index 6d76695b8d7ee..4dda7ac06c59d 100644 --- a/src/hooks/useRestartOnReceiptFailure.ts +++ b/src/hooks/useRestartOnReceiptFailure.ts @@ -1,15 +1,20 @@ import {useEffect} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import {checkIfScanFileCanBeRead, setMoneyRequestReceipt} from '@libs/actions/IOU'; -import {removeDraftTransactions} from '@libs/actions/TransactionEdit'; +import {removeDraftTransactionsByIDs} from '@libs/actions/TransactionEdit'; import {isLocalFile as isLocalFileUtil} from '@libs/fileDownload/FileUtils'; import {navigateToStartMoneyRequestStep} from '@libs/IOUUtils'; import {getRequestType} from '@libs/TransactionUtils'; import type {IOUAction, IOUType} from '@src/CONST'; import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import {validTransactionDraftIDsSelector} from '@src/selectors/TransactionDraft'; import type {Transaction} from '@src/types/onyx'; +import useOnyx from './useOnyx'; const useRestartOnReceiptFailure = (transaction: OnyxEntry, reportID: string, iouType: IOUType, action: IOUAction) => { + const [draftTransactionIDs] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {selector: validTransactionDraftIDsSelector}); + // When the component mounts, if there is a receipt, see if the image can be read from the disk. If not, redirect the user to the starting step of the flow. // This is because until the request is saved, the receipt file is only stored in the browsers memory as a blob:// and if the browser is refreshed, then // the image ceases to exist. The best way for the user to recover from this is to start over from the start of the request process. @@ -40,7 +45,7 @@ const useRestartOnReceiptFailure = (transaction: OnyxEntry, reportI return; } - removeDraftTransactions(true); + removeDraftTransactionsByIDs(draftTransactionIDs, true); navigateToStartMoneyRequestStep(requestType, iouType, transaction.transactionID, reportID); }); diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index f8d544f17bef7..17ee8842ee3bd 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -106,7 +106,7 @@ import {hasCreditBankAccount} from './actions/ReimbursementAccount/store'; import {openUnreportedExpense} from './actions/Report'; import type {GuidedSetupData, TaskForParameters} from './actions/Report'; import {isAnonymousUser as isAnonymousUserSession} from './actions/Session'; -import {removeDraftTransactions} from './actions/TransactionEdit'; +import {removeDraftTransactionsByIDs} from './actions/TransactionEdit'; import {getOnboardingMessages} from './actions/Welcome/OnboardingFlow'; import type {OnboardingCompanySize, OnboardingMessage, OnboardingPurpose, OnboardingTaskLinks} from './actions/Welcome/OnboardingFlow'; import type {AddCommentOrAttachmentParams} from './API/parameters'; @@ -11255,7 +11255,14 @@ function createDraftTransactionAndNavigateToParticipantSelector({ attendees: transaction?.modifiedAttendees ?? baseComment.attendees, }; - removeDraftTransactions(false, allTransactionDrafts); + removeDraftTransactionsByIDs( + Object.values(allTransactionDrafts ?? {}).reduce((acc, t) => { + if (t) { + acc.push(t.transactionID); + } + return acc; + }, []), + ); createDraftTransaction({ ...transaction, diff --git a/src/libs/actions/IOU/index.ts b/src/libs/actions/IOU/index.ts index 9052a29ad5433..242bb9d822325 100644 --- a/src/libs/actions/IOU/index.ts +++ b/src/libs/actions/IOU/index.ts @@ -237,7 +237,7 @@ import {buildOptimisticPolicyRecentlyUsedTags} from '@userActions/Policy/Tag'; import type {GuidedSetupData} from '@userActions/Report'; import {buildInviteToRoomOnyxData, completeOnboarding, notifyNewAction, optimisticReportLastData} from '@userActions/Report'; import {mergeTransactionIdsHighlightOnSearchRoute, sanitizeRecentWaypoints} from '@userActions/Transaction'; -import {removeDraftTransaction, removeDraftTransactions, removeDraftTransactionsByIDs} from '@userActions/TransactionEdit'; +import {removeDraftTransaction, removeDraftTransactionsByIDs} from '@userActions/TransactionEdit'; import {getOnboardingMessages} from '@userActions/Welcome/OnboardingFlow'; import type {OnboardingCompanySize} from '@userActions/Welcome/OnboardingFlow'; import type {IOUAction, IOUActionParams, IOUType, OdometerImageType} from '@src/CONST'; @@ -1245,7 +1245,15 @@ 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, draftTransactions); + removeDraftTransactionsByIDs( + Object.values(draftTransactions ?? {}).reduce((acc, t) => { + if (t) { + acc.push(t.transactionID); + } + return acc; + }, []), + true, + ); // 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/libs/actions/Transaction.ts b/src/libs/actions/Transaction.ts index fb1ac93a07a78..61266ab5d2fe6 100644 --- a/src/libs/actions/Transaction.ts +++ b/src/libs/actions/Transaction.ts @@ -58,15 +58,6 @@ import type {Waypoint, WaypointCollection} from '@src/types/onyx/Transaction'; import type TransactionState from '@src/types/utils/TransactionStateType'; import {getPolicyTags} from './IOU/index'; -let allTransactionDrafts: OnyxCollection = {}; -Onyx.connect({ - key: ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, - waitForCollectionCallback: true, - callback: (value) => { - allTransactionDrafts = value ?? {}; - }, -}); - let allReports: OnyxCollection = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT, @@ -1598,10 +1589,6 @@ function changeTransactionsReport({ }); } -function getDraftTransactions(draftTransactions?: OnyxCollection): Transaction[] { - return Object.values(draftTransactions ?? allTransactionDrafts ?? {}).filter((transaction): transaction is Transaction => !!transaction); -} - function mergeTransactionIdsHighlightOnSearchRoute(type: SearchDataTypes, data: Record | null) { return Onyx.merge(ONYXKEYS.TRANSACTION_IDS_HIGHLIGHT_ON_SEARCH_ROUTE, {[type]: data}); } @@ -1626,7 +1613,6 @@ export { clearError, markAsCash, dismissDuplicateTransactionViolation, - getDraftTransactions, generateTransactionID, setReviewDuplicatesKey, abandonReviewDuplicateTransactions, diff --git a/src/libs/actions/TransactionEdit.ts b/src/libs/actions/TransactionEdit.ts index 3450b33c9ead3..4ad7d1248f726 100644 --- a/src/libs/actions/TransactionEdit.ts +++ b/src/libs/actions/TransactionEdit.ts @@ -1,11 +1,11 @@ import {format} from 'date-fns'; import Onyx from 'react-native-onyx'; -import type {Connection, OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import type {Connection, OnyxEntry} from 'react-native-onyx'; import {formatCurrentUserToAttendee} from '@libs/IOUUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {PersonalDetails, Transaction} from '@src/types/onyx'; -import {generateTransactionID, getDraftTransactions} from './Transaction'; +import {generateTransactionID} from './Transaction'; let connection: Connection; @@ -99,28 +99,16 @@ function removeDraftSplitTransaction(transactionID: string | undefined) { Onyx.set(`${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${transactionID}`, null); } -function removeDraftTransactions(shouldExcludeInitialTransaction = false, allTransactionDrafts?: OnyxCollection) { - const draftTransactions = getDraftTransactions(allTransactionDrafts); - const draftTransactionsSet = draftTransactions.reduce( - (acc, item) => { - if (shouldExcludeInitialTransaction && item.transactionID === CONST.IOU.OPTIMISTIC_TRANSACTION_ID) { - return acc; - } - acc[`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${item.transactionID}`] = null; - return acc; - }, - {} as Record, - ); - Onyx.multiSet(draftTransactionsSet); -} - -function removeDraftTransactionsByIDs(transactionIDs: string[] | undefined) { +function removeDraftTransactionsByIDs(transactionIDs: string[] | undefined, shouldExcludeInitialTransaction = false) { if (!transactionIDs?.length) { return; } const draftTransactionsSet = transactionIDs.reduce( (acc, transactionID) => { + if (shouldExcludeInitialTransaction && transactionID === CONST.IOU.OPTIMISTIC_TRANSACTION_ID) { + return acc; + } acc[`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`] = null; return acc; }, @@ -190,7 +178,6 @@ export { createDraftTransaction, removeDraftTransaction, removeTransactionReceipt, - removeDraftTransactions, removeDraftTransactionsByIDs, removeDraftSplitTransaction, replaceDefaultDraftTransaction, diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index f02a48fff78b5..bea81deb56412 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -100,7 +100,7 @@ import {getReceiverType, sendInvoice} from '@userActions/IOU/SendInvoice'; import {sendMoneyElsewhere, sendMoneyWithWallet} from '@userActions/IOU/SendMoney'; import {splitBill, splitBillAndOpenReport, startSplitBill} from '@userActions/IOU/Split'; import {openDraftWorkspaceRequest} from '@userActions/Policy/Policy'; -import {removeDraftTransaction, removeDraftTransactions, replaceDefaultDraftTransaction} from '@userActions/TransactionEdit'; +import {removeDraftTransaction, removeDraftTransactionsByIDs, replaceDefaultDraftTransaction} from '@userActions/TransactionEdit'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -569,7 +569,7 @@ function IOURequestStepConfirmation({ Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_SCAN.getRoute(CONST.IOU.ACTION.CREATE, iouType, initialTransactionID, reportID, Navigation.getActiveRouteWithoutParams())); return; } - removeDraftTransactions(true); + removeDraftTransactionsByIDs(draftTransactionIDs, true); navigateToStartMoneyRequestStep(requestType, iouType, initialTransactionID, reportID); }); }, [requestType, iouType, initialTransactionID, reportID, action, report, transactions, participants]); diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.tsx b/src/pages/iou/request/step/IOURequestStepScan/index.tsx index d790b5f30a1d7..0fb7528c7e133 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.tsx +++ b/src/pages/iou/request/step/IOURequestStepScan/index.tsx @@ -38,9 +38,10 @@ import withFullTransactionOrNotFound from '@pages/iou/request/step/withFullTrans import withWritableReportOrNotFound from '@pages/iou/request/step/withWritableReportOrNotFound'; import variables from '@styles/variables'; import {checkIfScanFileCanBeRead, replaceReceipt, setMoneyRequestReceipt, updateLastLocationPermissionPrompt} from '@userActions/IOU'; -import {buildOptimisticTransactionAndCreateDraft, removeDraftTransactions, removeTransactionReceipt} from '@userActions/TransactionEdit'; +import {buildOptimisticTransactionAndCreateDraft, removeDraftTransactionsByIDs, removeTransactionReceipt} from '@userActions/TransactionEdit'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import {validTransactionDraftIDsSelector} from '@src/selectors/TransactionDraft'; import type {FileObject} from '@src/types/utils/Attachment'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import {cropImageToAspectRatio} from './cropImageToAspectRatio'; @@ -82,6 +83,7 @@ function IOURequestStepScan({ const lazyIllustrations = useMemoizedLazyIllustrations(['MultiScan', 'Hand', 'ReceiptStack', 'Shutter']); const lazyIcons = useMemoizedLazyExpensifyIcons(['Bolt', 'Gallery', 'ReceiptMultiple', 'boltSlash', 'ReplaceReceipt', 'SmartScan']); const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${report?.policyID}`); + const [draftTransactionIDs] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {selector: validTransactionDraftIDsSelector}); // End the create expense span on mount for web (no camera init tracking needed) useEffect(() => { @@ -232,7 +234,7 @@ function IOURequestStepScan({ } setIsMultiScanEnabled?.(false); removeTransactionReceipt(CONST.IOU.OPTIMISTIC_TRANSACTION_ID); - removeDraftTransactions(true); + removeDraftTransactionsByIDs(draftTransactionIDs, true); }); // We want this hook to run on mounting only // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/src/pages/iou/request/step/IOURequestStepScan/useReceiptScan.ts b/src/pages/iou/request/step/IOURequestStepScan/useReceiptScan.ts index 1f43af716736f..8a1a6c8cb9263 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/useReceiptScan.ts +++ b/src/pages/iou/request/step/IOURequestStepScan/useReceiptScan.ts @@ -23,7 +23,7 @@ import {isArchivedReport, isPolicyExpenseChat} from '@libs/ReportUtils'; import {getSpan, startSpan} from '@libs/telemetry/activeSpans'; import {getDefaultTaxCode, hasReceipt, shouldReuseInitialTransaction} from '@libs/TransactionUtils'; import {setMoneyRequestReceipt} from '@userActions/IOU'; -import {buildOptimisticTransactionAndCreateDraft, removeDraftTransactions, removeTransactionReceipt} from '@userActions/TransactionEdit'; +import {buildOptimisticTransactionAndCreateDraft, removeDraftTransactionsByIDs, removeTransactionReceipt} from '@userActions/TransactionEdit'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import {validTransactionDraftsSelector} from '@src/selectors/TransactionDraft'; @@ -84,6 +84,7 @@ function useReceiptScan({ const selfDMReport = useSelfDMReport(); const [allTransactionDrafts] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {selector: validTransactionDraftsSelector}); const [amountOwed] = useOnyx(ONYXKEYS.NVP_PRIVATE_AMOUNT_OWED); + const draftTransactionIDs = Object.keys(allTransactionDrafts ?? {}); const isEditing = action === CONST.IOU.ACTION.EDIT; const canUseMultiScan = isStartingScan && iouType !== CONST.IOU.TYPE.SPLIT; @@ -187,7 +188,7 @@ function useReceiptScan({ function setTestReceiptAndNavigate() { setTestReceipt(TestReceipt, 'png', (source, file, filename) => { setMoneyRequestReceipt(initialTransactionID, source, filename, !isEditing, CONST.TEST_RECEIPT.FILE_TYPE, true); - removeDraftTransactions(true); + removeDraftTransactionsByIDs(draftTransactionIDs, true); navigateToConfirmationStep([{file, source, transactionID: initialTransactionID}], false, true); }); } @@ -214,7 +215,7 @@ function useReceiptScan({ } if (!isMultiScanEnabled && isStartingScan) { - removeDraftTransactions(true); + removeDraftTransactionsByIDs(draftTransactionIDs, true); } for (const [index, file] of files.entries()) { @@ -273,7 +274,7 @@ function useReceiptScan({ setShouldShowMultiScanEducationalPopup(true); } removeTransactionReceipt(CONST.IOU.OPTIMISTIC_TRANSACTION_ID); - removeDraftTransactions(true); + removeDraftTransactionsByIDs(draftTransactionIDs, true); setIsMultiScanEnabled?.(!isMultiScanEnabled); } diff --git a/tests/unit/hooks/useReceiptScan.test.ts b/tests/unit/hooks/useReceiptScan.test.ts index f44a7c28ee6b8..198a280016fbc 100644 --- a/tests/unit/hooks/useReceiptScan.test.ts +++ b/tests/unit/hooks/useReceiptScan.test.ts @@ -10,7 +10,7 @@ import waitForBatchedUpdatesWithAct from '../../utils/waitForBatchedUpdatesWithA const mockHandleMoneyRequestStepScanParticipants = jest.fn(); const mockDismissProductTraining = jest.fn(); -const mockRemoveDraftTransactions = jest.fn(); +const mockRemoveDraftTransactionsByIDs = jest.fn(); const mockRemoveTransactionReceipt = jest.fn(); const mockSetMoneyRequestReceipt = jest.fn(); const mockBuildOptimisticTransactionAndCreateDraft = jest.fn(); @@ -46,7 +46,7 @@ jest.mock('@libs/actions/Welcome', () => ({ })); jest.mock('@userActions/TransactionEdit', () => ({ - removeDraftTransactions: (...args: unknown[]) => mockRemoveDraftTransactions(...args), + removeDraftTransactionsByIDs: (...args: unknown[]) => mockRemoveDraftTransactionsByIDs(...args), removeTransactionReceipt: (...args: unknown[]) => mockRemoveTransactionReceipt(...args), buildOptimisticTransactionAndCreateDraft: (...args: unknown[]) => mockBuildOptimisticTransactionAndCreateDraft(...args), })); @@ -396,7 +396,29 @@ describe('useReceiptScan', () => { expect(setIsMultiScanEnabled).toHaveBeenCalledWith(true); expect(mockRemoveTransactionReceipt).toHaveBeenCalledWith(CONST.IOU.OPTIMISTIC_TRANSACTION_ID); - expect(mockRemoveDraftTransactions).toHaveBeenCalledWith(true); + expect(mockRemoveDraftTransactionsByIDs).toHaveBeenCalledWith([], true); + }); + + it('should pass draft transaction IDs to removeDraftTransactionsByIDs when toggling multi-scan with existing drafts', async () => { + Onyx.set(ONYXKEYS.NVP_DISMISSED_PRODUCT_TRAINING, { + [CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.MULTI_SCAN_EDUCATIONAL_MODAL]: {timestamp: '2024-01-01', dismissedMethod: 'click'}, + }); + await Onyx.mergeCollection(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, { + [`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}draftA`]: {transactionID: 'draftA', reportID: REPORT_ID, amount: 50} as Transaction, + }); + await waitForBatchedUpdatesWithAct(); + + const setIsMultiScanEnabled = jest.fn(); + const toggleParams = {...params, setIsMultiScanEnabled, isMultiScanEnabled: false}; + const {result} = renderHook(() => useReceiptScan(toggleParams)); + await waitForBatchedUpdatesWithAct(); + + await act(async () => { + result.current.toggleMultiScan(); + }); + await waitForBatchedUpdatesWithAct(); + + expect(mockRemoveDraftTransactionsByIDs).toHaveBeenCalledWith(expect.arrayContaining(['draftA']), true); }); it('should set shouldShowMultiScanEducationalPopup false when dismissMultiScanEducationalPopup is called', async () => { @@ -450,7 +472,39 @@ describe('useReceiptScan', () => { expect(mockHandleMoneyRequestStepScanParticipants).not.toHaveBeenCalled(); }); - it('should call removeDraftTransactions when creating and not multi-scan', async () => { + it('should call removeDraftTransactionsByIDs when creating and not multi-scan', async () => { + const {result} = renderHook(() => useReceiptScan(params)); + await waitForBatchedUpdatesWithAct(); + + const files = [{uri: 'file://receipt.jpg', name: 'receipt.jpg', type: 'image/jpeg'}]; + await act(async () => { + result.current.validateFiles(files); + }); + + expect(mockRemoveDraftTransactionsByIDs).toHaveBeenCalledWith([], true); + }); + + it('should not call removeDraftTransactionsByIDs when in editing mode', async () => { + const updateScanAndNavigate = jest.fn(); + const editParams = {...params, action: CONST.IOU.ACTION.EDIT, updateScanAndNavigate}; + const {result} = renderHook(() => useReceiptScan(editParams)); + await waitForBatchedUpdatesWithAct(); + + const files = [{uri: 'file://receipt.jpg', name: 'receipt.jpg', type: 'image/jpeg'}]; + await act(async () => { + result.current.validateFiles(files); + }); + + expect(mockRemoveDraftTransactionsByIDs).not.toHaveBeenCalled(); + }); + + it('should pass draft transaction IDs to removeDraftTransactionsByIDs when drafts exist', async () => { + await Onyx.mergeCollection(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, { + [`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}draft1`]: {transactionID: 'draft1', reportID: REPORT_ID, amount: 100} as Transaction, + [`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}draft2`]: {transactionID: 'draft2', reportID: REPORT_ID, amount: 200} as Transaction, + }); + await waitForBatchedUpdatesWithAct(); + const {result} = renderHook(() => useReceiptScan(params)); await waitForBatchedUpdatesWithAct(); @@ -459,7 +513,23 @@ describe('useReceiptScan', () => { result.current.validateFiles(files); }); - expect(mockRemoveDraftTransactions).toHaveBeenCalledWith(true); + expect(mockRemoveDraftTransactionsByIDs).toHaveBeenCalledWith(expect.arrayContaining(['draft1', 'draft2']), true); + }); + + it('should always pass shouldExcludeInitialTransaction as true to removeDraftTransactionsByIDs', async () => { + const {result} = renderHook(() => useReceiptScan(params)); + await waitForBatchedUpdatesWithAct(); + + const files = [{uri: 'file://receipt.jpg', name: 'receipt.jpg', type: 'image/jpeg'}]; + await act(async () => { + result.current.validateFiles(files); + }); + + const calls = mockRemoveDraftTransactionsByIDs.mock.calls as Array<[string[], boolean]>; + expect(calls.length).toBeGreaterThan(0); + calls.forEach((call) => { + expect(call[1]).toBe(true); + }); }); it('should navigate to confirmation step after processing files', async () => { From 12cf324efabbdb3921ffb83da0e9e658d6b16190 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Thu, 12 Mar 2026 11:31:23 +0700 Subject: [PATCH 2/9] decrease max warnings --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 38a10796e7c3f..7a7d6ad4b60c9 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "perf-test": "NODE_OPTIONS=--experimental-vm-modules npx reassure", "typecheck": "NODE_OPTIONS=--max_old_space_size=8192 tsc", "typecheck-tsgo": "tsgo --project tsconfig.tsgo.json", - "lint": "NODE_OPTIONS=--max_old_space_size=8192 eslint . --max-warnings=334 --cache --cache-location=node_modules/.cache/eslint --cache-strategy content --concurrency=auto", + "lint": "NODE_OPTIONS=--max_old_space_size=8192 eslint . --max-warnings=333 --cache --cache-location=node_modules/.cache/eslint --cache-strategy content --concurrency=auto", "lint-changed": "NODE_OPTIONS=--max_old_space_size=8192 ./scripts/lintChanged.sh", "check-lazy-loading": "ts-node scripts/checkLazyLoading.ts", "lint-watch": "npx eslint-watch --watch --changed", From fca5befbf7758dab0d47718620367c47b1f0e466 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Thu, 12 Mar 2026 23:18:04 +0700 Subject: [PATCH 3/9] update tet --- src/hooks/useRestartOnReceiptFailure.ts | 7 +- tests/unit/hooks/useReceiptScan.test.ts | 96 ++++++++++++++++++++++++- 2 files changed, 99 insertions(+), 4 deletions(-) diff --git a/src/hooks/useRestartOnReceiptFailure.ts b/src/hooks/useRestartOnReceiptFailure.ts index 4dda7ac06c59d..73f36e0e334df 100644 --- a/src/hooks/useRestartOnReceiptFailure.ts +++ b/src/hooks/useRestartOnReceiptFailure.ts @@ -10,10 +10,11 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import {validTransactionDraftIDsSelector} from '@src/selectors/TransactionDraft'; import type {Transaction} from '@src/types/onyx'; +import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; import useOnyx from './useOnyx'; const useRestartOnReceiptFailure = (transaction: OnyxEntry, reportID: string, iouType: IOUType, action: IOUAction) => { - const [draftTransactionIDs] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {selector: validTransactionDraftIDsSelector}); + const [draftTransactionIDs, draftTransactionsMetadata] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {selector: validTransactionDraftIDsSelector}); // When the component mounts, if there is a receipt, see if the image can be read from the disk. If not, redirect the user to the starting step of the flow. // This is because until the request is saved, the receipt file is only stored in the browsers memory as a blob:// and if the browser is refreshed, then @@ -22,7 +23,7 @@ const useRestartOnReceiptFailure = (transaction: OnyxEntry, reportI useEffect(() => { let isScanFilesCanBeRead = true; - if (!transaction || action !== CONST.IOU.ACTION.CREATE) { + if (!transaction || action !== CONST.IOU.ACTION.CREATE || isLoadingOnyxValue(draftTransactionsMetadata)) { return; } const itemReceiptFilename = transaction.receipt?.filename; @@ -51,7 +52,7 @@ const useRestartOnReceiptFailure = (transaction: OnyxEntry, reportI // We want this hook to run on mounting only // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [draftTransactionsMetadata]); }; export default useRestartOnReceiptFailure; diff --git a/tests/unit/hooks/useReceiptScan.test.ts b/tests/unit/hooks/useReceiptScan.test.ts index 198a280016fbc..a39a66c6f0b44 100644 --- a/tests/unit/hooks/useReceiptScan.test.ts +++ b/tests/unit/hooks/useReceiptScan.test.ts @@ -421,6 +421,55 @@ describe('useReceiptScan', () => { expect(mockRemoveDraftTransactionsByIDs).toHaveBeenCalledWith(expect.arrayContaining(['draftA']), true); }); + it('should always pass shouldExcludeInitialTransaction as true when toggling multi-scan', async () => { + Onyx.set(ONYXKEYS.NVP_DISMISSED_PRODUCT_TRAINING, { + [CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.MULTI_SCAN_EDUCATIONAL_MODAL]: {timestamp: '2024-01-01', dismissedMethod: 'click'}, + }); + await Onyx.mergeCollection(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, { + [`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}draftM`]: {transactionID: 'draftM', reportID: REPORT_ID, amount: 100} as Transaction, + [`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}draftN`]: {transactionID: 'draftN', reportID: REPORT_ID, amount: 200} as Transaction, + }); + await waitForBatchedUpdatesWithAct(); + + const setIsMultiScanEnabled = jest.fn(); + const toggleParams = {...params, setIsMultiScanEnabled, isMultiScanEnabled: false}; + const {result} = renderHook(() => useReceiptScan(toggleParams)); + await waitForBatchedUpdatesWithAct(); + + await act(async () => { + result.current.toggleMultiScan(); + }); + await waitForBatchedUpdatesWithAct(); + + const calls = mockRemoveDraftTransactionsByIDs.mock.calls as Array<[string[], boolean]>; + expect(calls.length).toBeGreaterThan(0); + for (const call of calls) { + expect(call[1]).toBe(true); + } + expect(calls.at(0)?.at(0)).toEqual(expect.arrayContaining(['draftM', 'draftN'])); + }); + + it('should call removeDraftTransactionsByIDs and removeTransactionReceipt when toggling multi-scan off', async () => { + Onyx.set(ONYXKEYS.NVP_DISMISSED_PRODUCT_TRAINING, { + [CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.MULTI_SCAN_EDUCATIONAL_MODAL]: {timestamp: '2024-01-01', dismissedMethod: 'click'}, + }); + await waitForBatchedUpdatesWithAct(); + + const setIsMultiScanEnabled = jest.fn(); + const toggleParams = {...params, setIsMultiScanEnabled, isMultiScanEnabled: true}; + const {result} = renderHook(() => useReceiptScan(toggleParams)); + await waitForBatchedUpdatesWithAct(); + + await act(async () => { + result.current.toggleMultiScan(); + }); + await waitForBatchedUpdatesWithAct(); + + expect(mockRemoveTransactionReceipt).toHaveBeenCalledWith(CONST.IOU.OPTIMISTIC_TRANSACTION_ID); + expect(mockRemoveDraftTransactionsByIDs).toHaveBeenCalledWith([], true); + expect(setIsMultiScanEnabled).toHaveBeenCalledWith(false); + }); + it('should set shouldShowMultiScanEducationalPopup false when dismissMultiScanEducationalPopup is called', async () => { const setIsMultiScanEnabled = jest.fn(); const toggleParams = {...params, setIsMultiScanEnabled, isMultiScanEnabled: false}; @@ -527,9 +576,54 @@ describe('useReceiptScan', () => { const calls = mockRemoveDraftTransactionsByIDs.mock.calls as Array<[string[], boolean]>; expect(calls.length).toBeGreaterThan(0); - calls.forEach((call) => { + for (const call of calls) { expect(call[1]).toBe(true); + } + }); + + it('should not call removeDraftTransactionsByIDs when multi-scan is enabled', async () => { + const multiScanParams = {...params, isMultiScanEnabled: true, setIsMultiScanEnabled: jest.fn()}; + const {result} = renderHook(() => useReceiptScan(multiScanParams)); + await waitForBatchedUpdatesWithAct(); + + const files = [{uri: 'file://receipt.jpg', name: 'receipt.jpg', type: 'image/jpeg'}]; + await act(async () => { + result.current.validateFiles(files); }); + + expect(mockRemoveDraftTransactionsByIDs).not.toHaveBeenCalled(); + }); + + it('should not call removeDraftTransactionsByIDs when isStartingScan is false', async () => { + const nonStartingParams = {...params, isStartingScan: false}; + const {result} = renderHook(() => useReceiptScan(nonStartingParams)); + await waitForBatchedUpdatesWithAct(); + + const files = [{uri: 'file://receipt.jpg', name: 'receipt.jpg', type: 'image/jpeg'}]; + await act(async () => { + result.current.validateFiles(files); + }); + + expect(mockRemoveDraftTransactionsByIDs).not.toHaveBeenCalled(); + }); + + it('should call removeDraftTransactionsByIDs with multiple draft IDs and shouldExcludeInitialTransaction true', async () => { + await Onyx.mergeCollection(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, { + [`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}draftX`]: {transactionID: 'draftX', reportID: REPORT_ID, amount: 10} as Transaction, + [`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}draftY`]: {transactionID: 'draftY', reportID: REPORT_ID, amount: 20} as Transaction, + [`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}draftZ`]: {transactionID: 'draftZ', reportID: REPORT_ID, amount: 30} as Transaction, + }); + await waitForBatchedUpdatesWithAct(); + + const {result} = renderHook(() => useReceiptScan(params)); + await waitForBatchedUpdatesWithAct(); + + const files = [{uri: 'file://receipt.jpg', name: 'receipt.jpg', type: 'image/jpeg'}]; + await act(async () => { + result.current.validateFiles(files); + }); + + expect(mockRemoveDraftTransactionsByIDs).toHaveBeenCalledWith(expect.arrayContaining(['draftX', 'draftY', 'draftZ']), true); }); it('should navigate to confirmation step after processing files', async () => { From 1b388a7d1216d406feb0bb14853aaa2527c2b6bc Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Thu, 12 Mar 2026 23:45:10 +0700 Subject: [PATCH 4/9] add tests --- tests/actions/TransactionEditTest.ts | 56 ++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/tests/actions/TransactionEditTest.ts b/tests/actions/TransactionEditTest.ts index 6bc6b69b1aa9b..b54cd376f636b 100644 --- a/tests/actions/TransactionEditTest.ts +++ b/tests/actions/TransactionEditTest.ts @@ -2,6 +2,7 @@ import Onyx from 'react-native-onyx'; import OnyxUpdateManager from '@libs/actions/OnyxUpdateManager'; import {createBackupTransaction, removeDraftTransactionsByIDs, restoreOriginalTransactionFromBackup} from '@libs/actions/TransactionEdit'; import initOnyxDerivedValues from '@userActions/OnyxDerived'; +import CONST from '@src/CONST'; import * as SequentialQueue from '@src/libs/Network/SequentialQueue'; import ONYXKEYS from '@src/ONYXKEYS'; import createRandomTransaction from '../utils/collections/transaction'; @@ -180,5 +181,60 @@ describe('actions/TransactionEdit', () => { const draft1 = await getOnyxValue(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transaction1.transactionID}`); expect(draft1).toBeDefined(); }); + + it('should do nothing when given undefined', async () => { + const transaction1 = createRandomTransaction(1); + + await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transaction1.transactionID}`, transaction1); + await waitForBatchedUpdates(); + + removeDraftTransactionsByIDs(undefined); + await waitForBatchedUpdates(); + + const draft1 = await getOnyxValue(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transaction1.transactionID}`); + expect(draft1).toBeDefined(); + }); + + it('should exclude the initial optimistic transaction when shouldExcludeInitialTransaction is true', async () => { + const transaction1 = createRandomTransaction(3); + const optimisticTransaction = { + ...createRandomTransaction(4), + transactionID: CONST.IOU.OPTIMISTIC_TRANSACTION_ID, + }; + + await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transaction1.transactionID}`, transaction1); + await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_TRANSACTION_ID}`, optimisticTransaction); + await waitForBatchedUpdates(); + + removeDraftTransactionsByIDs([transaction1.transactionID, CONST.IOU.OPTIMISTIC_TRANSACTION_ID], true); + await waitForBatchedUpdates(); + + const draft1 = await getOnyxValue(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transaction1.transactionID}`); + const optimisticDraft = await getOnyxValue(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_TRANSACTION_ID}`); + + expect(draft1).toBeUndefined(); + expect(optimisticDraft).toBeDefined(); + }); + + it('should remove all transactions including optimistic when shouldExcludeInitialTransaction is false', async () => { + const transaction1 = createRandomTransaction(3); + const optimisticTransaction = { + ...createRandomTransaction(4), + transactionID: CONST.IOU.OPTIMISTIC_TRANSACTION_ID, + }; + + await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transaction1.transactionID}`, transaction1); + await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_TRANSACTION_ID}`, optimisticTransaction); + await waitForBatchedUpdates(); + + removeDraftTransactionsByIDs([transaction1.transactionID, CONST.IOU.OPTIMISTIC_TRANSACTION_ID], false); + await waitForBatchedUpdates(); + + const draft1 = await getOnyxValue(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transaction1.transactionID}`); + const optimisticDraft = await getOnyxValue(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_TRANSACTION_ID}`); + + expect(draft1).toBeUndefined(); + expect(optimisticDraft).toBeUndefined(); + }); }); }); From 7b9e389bece21149de3d38a8dfdaa6d85b9205ea Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Mon, 16 Mar 2026 11:55:58 +0700 Subject: [PATCH 5/9] remove unncessary changes --- tests/unit/hooks/useReceiptScan.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/unit/hooks/useReceiptScan.test.ts b/tests/unit/hooks/useReceiptScan.test.ts index 6bd8087ebe4ab..6e444b4bc9777 100644 --- a/tests/unit/hooks/useReceiptScan.test.ts +++ b/tests/unit/hooks/useReceiptScan.test.ts @@ -9,7 +9,6 @@ import type {Report, Transaction} from '@src/types/onyx'; import waitForBatchedUpdatesWithAct from '../../utils/waitForBatchedUpdatesWithAct'; const mockHandleMoneyRequestStepScanParticipants = jest.fn(); -const mockRemoveDraftTransactions = jest.fn(); const mockRemoveDraftTransactionsByIDs = jest.fn(); const mockRemoveTransactionReceipt = jest.fn(); const mockSetMoneyRequestReceipt = jest.fn(); From cbe50eb1df57d64a6e57367b78918a9d697eb0df Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Mon, 16 Mar 2026 12:04:09 +0700 Subject: [PATCH 6/9] lint fix --- .../step/IOURequestStepScan/hooks/useMobileReceiptScan.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepScan/hooks/useMobileReceiptScan.ts b/src/pages/iou/request/step/IOURequestStepScan/hooks/useMobileReceiptScan.ts index 22bf4240b67b6..db0047d3e0878 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/hooks/useMobileReceiptScan.ts +++ b/src/pages/iou/request/step/IOURequestStepScan/hooks/useMobileReceiptScan.ts @@ -7,9 +7,10 @@ import useTransactionDraftValues from '@hooks/useTransactionDraftValues'; import {dismissProductTraining} from '@libs/actions/Welcome'; import HapticFeedback from '@libs/HapticFeedback'; import type {ReceiptFile, UseMobileReceiptScanParams} from '@pages/iou/request/step/IOURequestStepScan/types'; -import {removeDraftTransactions, removeTransactionReceipt} from '@userActions/TransactionEdit'; +import {removeDraftTransactionsByIDs, removeTransactionReceipt} from '@userActions/TransactionEdit'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import {validTransactionDraftIDsSelector} from '@src/selectors/TransactionDraft'; /** * Extends useReceiptScan with mobile-only logic: multi-scan, haptic feedback, and blink animation. @@ -31,6 +32,8 @@ function useMobileReceiptScan({ const optimisticTransactions = useTransactionDraftValues(); const [dismissedProductTraining] = useOnyx(ONYXKEYS.NVP_DISMISSED_PRODUCT_TRAINING); + const [draftTransactionIDs] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {selector: validTransactionDraftIDsSelector}); + const [shouldShowMultiScanEducationalPopup, setShouldShowMultiScanEducationalPopup] = useState(false); const canUseMultiScan = isStartingScan && iouType !== CONST.IOU.TYPE.SPLIT; @@ -75,7 +78,7 @@ function useMobileReceiptScan({ setShouldShowMultiScanEducationalPopup(true); } removeTransactionReceipt(CONST.IOU.OPTIMISTIC_TRANSACTION_ID); - removeDraftTransactions(true); + removeDraftTransactionsByIDs(draftTransactionIDs, true); setIsMultiScanEnabled?.(!isMultiScanEnabled); } From d430c6c9d8b5a34d6a5f6d035f616f89c530303a Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Thu, 19 Mar 2026 10:48:09 +0700 Subject: [PATCH 7/9] update comments --- .../Modal/EmployeeTestDriveModal.tsx | 5 +- src/hooks/useReceiptScanDrop.tsx | 5 +- src/libs/ReportUtils.ts | 13 +--- src/libs/actions/IOU/index.ts | 14 +---- src/pages/ReportDetailsPage.tsx | 11 ++-- src/pages/Share/SubmitDetailsPage.tsx | 5 +- .../inbox/report/PureReportActionItem.tsx | 14 ++--- .../useAttachmentUploadValidation.ts | 5 +- src/pages/inbox/report/ReportActionItem.tsx | 7 ++- .../iou/request/DistanceRequestStartPage.tsx | 7 ++- src/pages/iou/request/IOURequestStartPage.tsx | 7 ++- tests/actions/IOUTest.ts | 62 +++++-------------- tests/ui/ClearReportActionErrorsUITest.tsx | 2 +- tests/ui/PureReportActionItemTest.tsx | 12 ++-- tests/unit/ReportUtilsTest.ts | 18 +++--- 15 files changed, 75 insertions(+), 112 deletions(-) diff --git a/src/components/TestDrive/Modal/EmployeeTestDriveModal.tsx b/src/components/TestDrive/Modal/EmployeeTestDriveModal.tsx index d12f46d459d0b..ed26174c7c460 100644 --- a/src/components/TestDrive/Modal/EmployeeTestDriveModal.tsx +++ b/src/components/TestDrive/Modal/EmployeeTestDriveModal.tsx @@ -1,4 +1,5 @@ import {useRoute} from '@react-navigation/native'; +import {validTransactionDraftIDsSelector} from '@selectors/TransactionDraft'; import {format} from 'date-fns'; import {Str} from 'expensify-common'; import React, {useCallback, useMemo, useState} from 'react'; @@ -49,7 +50,7 @@ function EmployeeTestDriveModal() { const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const personalPolicy = usePersonalPolicy(); const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY); - const [draftTransactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT); + const [draftTransactionIDs] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {selector: validTransactionDraftIDsSelector}); const hasOnlyPersonalPolicies = useMemo(() => hasOnlyPersonalPoliciesUtil(allPolicies), [allPolicies]); const onBossEmailChange = useCallback((value: string) => { @@ -86,7 +87,7 @@ function EmployeeTestDriveModal() { currentDate, currentUserPersonalDetails, hasOnlyPersonalPolicies, - draftTransactions, + draftTransactionIDs, }); setMoneyRequestReceipt(transactionID, source, filename, true, CONST.TEST_RECEIPT.FILE_TYPE, false, true); diff --git a/src/hooks/useReceiptScanDrop.tsx b/src/hooks/useReceiptScanDrop.tsx index 71c9f09a7de6e..5746bea6407be 100644 --- a/src/hooks/useReceiptScanDrop.tsx +++ b/src/hooks/useReceiptScanDrop.tsx @@ -1,3 +1,4 @@ +import {validTransactionDraftIDsSelector} from '@selectors/TransactionDraft'; import React, {useMemo} from 'react'; import {setTransactionReport} from '@libs/actions/Transaction'; import {navigateToParticipantPage} from '@libs/IOUUtils'; @@ -35,7 +36,7 @@ function useReceiptScanDrop() { const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`); const [personalPolicyID] = useOnyx(ONYXKEYS.PERSONAL_POLICY_ID); const [personalPolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${personalPolicyID}`); - const [draftTransactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT); + const [draftTransactionIDs] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {selector: validTransactionDraftIDsSelector}); // Memoize the new report ID to avoid re-generating it on every render and cause the hook to change, which leads to performance issues. const newReportID = useMemo(() => generateReportID(), []); @@ -52,7 +53,7 @@ function useReceiptScanDrop() { currentDate, currentUserPersonalDetails, hasOnlyPersonalPolicies, - draftTransactions, + draftTransactionIDs, }); const newReceiptFiles: ReceiptFile[] = []; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 058b64d2d813f..169e0b15ed089 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -11253,7 +11253,7 @@ type CreateDraftTransactionParams = { actionName: IOUAction; reportActionID: string | undefined; introSelected: OnyxEntry; - allTransactionDrafts: OnyxCollection; + draftTransactionIDs: string[] | undefined; activePolicy: OnyxEntry; userBillingGraceEndPeriods: OnyxCollection; amountOwed: OnyxEntry; @@ -11267,7 +11267,7 @@ function createDraftTransactionAndNavigateToParticipantSelector({ actionName, reportActionID, introSelected, - allTransactionDrafts, + draftTransactionIDs, activePolicy, userBillingGraceEndPeriods, amountOwed, @@ -11297,14 +11297,7 @@ function createDraftTransactionAndNavigateToParticipantSelector({ attendees: transaction?.modifiedAttendees ?? baseComment.attendees, }; - removeDraftTransactionsByIDs( - Object.values(allTransactionDrafts ?? {}).reduce((acc, t) => { - if (t) { - acc.push(t.transactionID); - } - return acc; - }, []), - ); + removeDraftTransactionsByIDs(draftTransactionIDs); createDraftTransaction({ ...transaction, diff --git a/src/libs/actions/IOU/index.ts b/src/libs/actions/IOU/index.ts index a160dc1955a8d..4551044f44bcb 100644 --- a/src/libs/actions/IOU/index.ts +++ b/src/libs/actions/IOU/index.ts @@ -297,7 +297,7 @@ type InitMoneyRequestParams = { lastSelectedDistanceRates?: OnyxEntry; currentUserPersonalDetails: CurrentUserPersonalDetails; hasOnlyPersonalPolicies: boolean; - draftTransactions: OnyxCollection; + draftTransactionIDs: string[] | undefined; }; type MoneyRequestInformation = { @@ -1233,7 +1233,7 @@ function initMoneyRequest({ lastSelectedDistanceRates, currentUserPersonalDetails, hasOnlyPersonalPolicies, - draftTransactions, + draftTransactionIDs, }: InitMoneyRequestParams) { // Generate a brand new transactionID const newTransactionID = CONST.IOU.OPTIMISTIC_TRANSACTION_ID; @@ -1242,15 +1242,7 @@ function initMoneyRequest({ const created = currentDate ?? format(new Date(), 'yyyy-MM-dd'); // We remove draft transactions created during multi scanning if there are some - removeDraftTransactionsByIDs( - Object.values(draftTransactions ?? {}).reduce((acc, t) => { - if (t) { - acc.push(t.transactionID); - } - return acc; - }, []), - true, - ); + removeDraftTransactionsByIDs(draftTransactionIDs, true); // 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/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index 34c67390d8299..0c59e3c94c7f1 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -1,4 +1,5 @@ import {StackActions} from '@react-navigation/native'; +import {validTransactionDraftIDsSelector} from '@selectors/TransactionDraft'; import React, {useCallback, useEffect, useMemo} from 'react'; import {InteractionManager, View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; @@ -191,7 +192,7 @@ function ReportDetailsPage({policy, report, route, reportMetadata}: ReportDetail const [isDebugModeEnabled = false] = useOnyx(ONYXKEYS.IS_DEBUG_MODE_ENABLED); const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST); const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED); - const [allTransactionDrafts] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT); + const [draftTransactionIDs] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {selector: validTransactionDraftIDsSelector}); const [allTransactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const {showConfirmModal} = useConfirmModal(); @@ -458,7 +459,7 @@ function ReportDetailsPage({policy, report, route, reportMetadata}: ReportDetail actionName: CONST.IOU.ACTION.SUBMIT, reportActionID: actionableWhisperReportActionID, introSelected, - allTransactionDrafts, + draftTransactionIDs, activePolicy, userBillingGraceEndPeriods, amountOwed, @@ -481,7 +482,7 @@ function ReportDetailsPage({policy, report, route, reportMetadata}: ReportDetail actionName: CONST.IOU.ACTION.CATEGORIZE, reportActionID: actionableWhisperReportActionID, introSelected, - allTransactionDrafts, + draftTransactionIDs, activePolicy, userBillingGraceEndPeriods, amountOwed, @@ -501,7 +502,7 @@ function ReportDetailsPage({policy, report, route, reportMetadata}: ReportDetail actionName: CONST.IOU.ACTION.SHARE, reportActionID: actionableWhisperReportActionID, introSelected, - allTransactionDrafts, + draftTransactionIDs, activePolicy, userBillingGraceEndPeriods, amountOwed, @@ -627,7 +628,7 @@ function ReportDetailsPage({policy, report, route, reportMetadata}: ReportDetail isRestrictedToPreferredPolicy, preferredPolicyID, introSelected, - allTransactionDrafts, + draftTransactionIDs, activePolicy, parentReport, reportActionsForOriginalReportID, diff --git a/src/pages/Share/SubmitDetailsPage.tsx b/src/pages/Share/SubmitDetailsPage.tsx index 02a178f3e86ef..c9fc70e98b9fb 100644 --- a/src/pages/Share/SubmitDetailsPage.tsx +++ b/src/pages/Share/SubmitDetailsPage.tsx @@ -91,7 +91,6 @@ 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, {selector: hasOnlyPersonalPoliciesUtil}); - const [draftTransactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT); useEffect(() => { if (!errorTitle || !errorMessage) { @@ -113,9 +112,9 @@ function SubmitDetailsPage({ currentDate, currentUserPersonalDetails, hasOnlyPersonalPolicies, - draftTransactions, + draftTransactionIDs, }); - // The draftTransactions can be changed if users update the expense, so we don't want to re-init the money request + // The draftTransactionIDs 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]); diff --git a/src/pages/inbox/report/PureReportActionItem.tsx b/src/pages/inbox/report/PureReportActionItem.tsx index 1c18f15eff511..0f3d04ea2a1d2 100644 --- a/src/pages/inbox/report/PureReportActionItem.tsx +++ b/src/pages/inbox/report/PureReportActionItem.tsx @@ -269,8 +269,8 @@ type PureReportActionItemProps = { /** Model of onboarding selected */ introSelected?: OnyxEntry; - /** All transaction drafts */ - allTransactionDrafts: OnyxCollection; + /** All transaction draft IDs */ + draftTransactionIDs: string[] | undefined; /** Report for this action */ report: OnyxEntry; @@ -480,7 +480,7 @@ const isEmptyHTML = ({props: {html}}: T): boolean = function PureReportActionItem({ personalPolicyID, introSelected, - allTransactionDrafts, + draftTransactionIDs, action, report, policy, @@ -945,7 +945,7 @@ function PureReportActionItem({ actionName: CONST.IOU.ACTION.SUBMIT, reportActionID: action.reportActionID, introSelected, - allTransactionDrafts, + draftTransactionIDs, activePolicy, userBillingGraceEndPeriods, amountOwed, @@ -968,7 +968,7 @@ function PureReportActionItem({ actionName: CONST.IOU.ACTION.CATEGORIZE, reportActionID: action.reportActionID, introSelected, - allTransactionDrafts, + draftTransactionIDs, activePolicy, userBillingGraceEndPeriods, amountOwed, @@ -985,7 +985,7 @@ function PureReportActionItem({ actionName: CONST.IOU.ACTION.SHARE, reportActionID: action.reportActionID, introSelected, - allTransactionDrafts, + draftTransactionIDs, activePolicy, userBillingGraceEndPeriods, amountOwed, @@ -1128,7 +1128,7 @@ function PureReportActionItem({ isOriginalReportArchived, resolveActionableMentionWhisper, introSelected, - allTransactionDrafts, + draftTransactionIDs, activePolicy, report, originalReport, diff --git a/src/pages/inbox/report/ReportActionCompose/useAttachmentUploadValidation.ts b/src/pages/inbox/report/ReportActionCompose/useAttachmentUploadValidation.ts index fc8fe3ad4a652..29de7e92d8f20 100644 --- a/src/pages/inbox/report/ReportActionCompose/useAttachmentUploadValidation.ts +++ b/src/pages/inbox/report/ReportActionCompose/useAttachmentUploadValidation.ts @@ -1,3 +1,4 @@ +import {validTransactionDraftIDsSelector} from '@selectors/TransactionDraft'; import {useCallback, useContext, useMemo, useRef} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import useFilesValidation from '@hooks/useFilesValidation'; @@ -56,7 +57,7 @@ function useAttachmentUploadValidation({ const [ownerBillingGraceEndPeriod] = useOnyx(ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END); const personalPolicy = usePersonalPolicy(); const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY); - const [draftTransactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT); + const [draftTransactionIDs] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {selector: validTransactionDraftIDsSelector}); const hasOnlyPersonalPolicies = useMemo(() => hasOnlyPersonalPoliciesUtil(allPolicies), [allPolicies]); const reportAttachmentsContext = useContext(AttachmentModalContext); @@ -103,7 +104,7 @@ function useAttachmentUploadValidation({ currentDate, currentUserPersonalDetails, hasOnlyPersonalPolicies, - draftTransactions, + draftTransactionIDs, }); for (const [index, file] of files.entries()) { diff --git a/src/pages/inbox/report/ReportActionItem.tsx b/src/pages/inbox/report/ReportActionItem.tsx index 53fe3d6335935..59755df373402 100644 --- a/src/pages/inbox/report/ReportActionItem.tsx +++ b/src/pages/inbox/report/ReportActionItem.tsx @@ -1,3 +1,4 @@ +import {validTransactionDraftIDsSelector} from '@selectors/TransactionDraft'; import React, {useCallback} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import {useBlockedFromConcierge} from '@components/OnyxListItemProvider'; @@ -39,7 +40,7 @@ import PureReportActionItem from './PureReportActionItem'; type ReportActionItemProps = Omit< PureReportActionItemProps, - 'taskReport' | 'linkedReport' | 'iouReportOfLinkedReport' | 'currentUserAccountID' | 'personalPolicyID' | 'allTransactionDrafts' | 'userBillingGraceEndPeriods' + 'taskReport' | 'linkedReport' | 'iouReportOfLinkedReport' | 'currentUserAccountID' | 'personalPolicyID' | 'draftTransactionIDs' | 'userBillingGraceEndPeriods' > & { /** Whether to show the draft message or not */ shouldShowDraftMessage?: boolean; @@ -93,7 +94,7 @@ function ReportActionItem({ const policyIDForTags = report?.policyID === CONST.POLICY.OWNER_EMAIL_FAKE && policyForMovingExpensesID ? policyForMovingExpensesID : report?.policyID; const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyIDForTags}`); const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED); - const [allTransactionDrafts] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT); + const [draftTransactionIDs] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {selector: validTransactionDraftIDsSelector}); const [reportMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`); const [originalReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${originalReportID}`); const [iouReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getIOUReportIDFromReportActionPreview(action)}`); @@ -134,7 +135,7 @@ function ReportActionItem({ // eslint-disable-next-line react/jsx-props-no-spreading {...props} introSelected={introSelected} - allTransactionDrafts={allTransactionDrafts} + draftTransactionIDs={draftTransactionIDs} personalPolicyID={personalPolicyID} action={action} report={report} diff --git a/src/pages/iou/request/DistanceRequestStartPage.tsx b/src/pages/iou/request/DistanceRequestStartPage.tsx index 2ff4962c1786c..14fca2cd9ca4d 100644 --- a/src/pages/iou/request/DistanceRequestStartPage.tsx +++ b/src/pages/iou/request/DistanceRequestStartPage.tsx @@ -1,4 +1,5 @@ import {useFocusEffect} from '@react-navigation/native'; +import {validTransactionDraftIDsSelector} from '@selectors/TransactionDraft'; import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {Keyboard, View} from 'react-native'; import FocusTrapContainerElement from '@components/FocusTrap/FocusTrapContainerElement'; @@ -57,7 +58,7 @@ function DistanceRequestStartPage({ const [lastDistanceExpenseType] = useOnyx(ONYXKEYS.NVP_LAST_DISTANCE_EXPENSE_TYPE); const isLoadingSelectedTab = isLoadingOnyxValue(selectedTabResult); const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${getNonEmptyStringOnyxID(route?.params.transactionID)}`); - const [draftTransactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT); + const [draftTransactionIDs] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {selector: validTransactionDraftIDsSelector}); const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY); const [lastSelectedDistanceRates] = useOnyx(ONYXKEYS.NVP_LAST_SELECTED_DISTANCE_RATES); const [currentDate] = useOnyx(ONYXKEYS.CURRENT_DATE); @@ -120,7 +121,7 @@ function DistanceRequestStartPage({ lastSelectedDistanceRates, currentUserPersonalDetails, hasOnlyPersonalPolicies, - draftTransactions, + draftTransactionIDs, }); }, [ @@ -137,7 +138,7 @@ function DistanceRequestStartPage({ lastSelectedDistanceRates, currentUserPersonalDetails, hasOnlyPersonalPolicies, - draftTransactions, + draftTransactionIDs, ], ); diff --git a/src/pages/iou/request/IOURequestStartPage.tsx b/src/pages/iou/request/IOURequestStartPage.tsx index 21b5278b84d83..1e74a7de0aa54 100644 --- a/src/pages/iou/request/IOURequestStartPage.tsx +++ b/src/pages/iou/request/IOURequestStartPage.tsx @@ -1,5 +1,6 @@ import {useFocusEffect} from '@react-navigation/native'; import {iouRequestPolicyCollectionSelector} from '@selectors/Policy'; +import {validTransactionDraftIDsSelector} from '@selectors/TransactionDraft'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {Keyboard, View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; @@ -95,7 +96,7 @@ function IOURequestStartPage({ }); const [lastSelectedDistanceRates] = useOnyx(ONYXKEYS.NVP_LAST_SELECTED_DISTANCE_RATES); - const [draftTransactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT); + const [draftTransactionIDs] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {selector: validTransactionDraftIDsSelector}); const [isMultiScanEnabled, setIsMultiScanEnabled] = useState(false); const [currentDate] = useOnyx(ONYXKEYS.CURRENT_DATE); const {isOffline} = useNetwork(); @@ -199,7 +200,7 @@ function IOURequestStartPage({ lastSelectedDistanceRates, currentUserPersonalDetails, hasOnlyPersonalPolicies, - draftTransactions, + draftTransactionIDs, }); }, [ @@ -216,7 +217,7 @@ function IOURequestStartPage({ lastSelectedDistanceRates, currentUserPersonalDetails, hasOnlyPersonalPolicies, - draftTransactions, + draftTransactionIDs, ], ); diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index bf582e8124049..78866b21dd418 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -633,7 +633,7 @@ describe('actions/IOU', () => { actionName: CONST.IOU.ACTION.CATEGORIZE, reportActionID: reportActionableTrackExpense?.reportActionID, introSelected: {choice: CONST.ONBOARDING_CHOICES.MANAGE_TEAM}, - allTransactionDrafts: {}, + draftTransactionIDs: [], activePolicy: undefined, userBillingGraceEndPeriods: undefined, amountOwed: 0, @@ -1206,7 +1206,7 @@ describe('actions/IOU', () => { actionName: CONST.IOU.ACTION.CATEGORIZE, reportActionID: actionableWhisper?.reportActionID, introSelected: {choice: CONST.ONBOARDING_CHOICES.MANAGE_TEAM}, - allTransactionDrafts: {}, + draftTransactionIDs: [], activePolicy: undefined, userBillingGraceEndPeriods: undefined, amountOwed: 0, @@ -1823,7 +1823,7 @@ describe('actions/IOU', () => { }); describe('createDraftTransactionAndNavigateToParticipantSelector', () => { - it('should clear existing draft transactions when allTransactionDrafts is provided', async () => { + it('should clear existing draft transactions when draftTransactionIDs is provided', async () => { // Given existing draft transactions const existingDraftTransaction1: Transaction = {...createRandomTransaction(1), transactionID: 'existing-draft-1'}; const existingDraftTransaction2: Transaction = {...createRandomTransaction(2), transactionID: 'existing-draft-2'}; @@ -1841,28 +1841,14 @@ describe('actions/IOU', () => { await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionToCategorize.transactionID}`, transactionToCategorize); await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${selfDMReport.reportID}`, selfDMReport); - // Get the existing drafts to pass to the function - let allTransactionDrafts: OnyxCollection; - await getOnyxData({ - key: ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, - waitForCollectionCallback: true, - callback: (val) => { - allTransactionDrafts = val; - }, - }); - - // Verify existing drafts exist before calling the function - expect(allTransactionDrafts?.[`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${existingDraftTransaction1.transactionID}`]).toBeTruthy(); - expect(allTransactionDrafts?.[`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${existingDraftTransaction2.transactionID}`]).toBeTruthy(); - - // When createDraftTransactionAndNavigateToParticipantSelector is called with allTransactionDrafts + // When createDraftTransactionAndNavigateToParticipantSelector is called with draftTransactionIDs createDraftTransactionAndNavigateToParticipantSelector({ transactionID: transactionToCategorize.transactionID, reportID: selfDMReport.reportID, actionName: CONST.IOU.ACTION.CATEGORIZE, reportActionID, introSelected: {choice: CONST.ONBOARDING_CHOICES.MANAGE_TEAM}, - allTransactionDrafts, + draftTransactionIDs: [existingDraftTransaction1.transactionID, existingDraftTransaction2.transactionID], activePolicy: undefined, userBillingGraceEndPeriods: undefined, amountOwed: 0, @@ -1910,7 +1896,7 @@ describe('actions/IOU', () => { actionName: CONST.IOU.ACTION.CATEGORIZE, reportActionID, introSelected: {choice: CONST.ONBOARDING_CHOICES.MANAGE_TEAM}, - allTransactionDrafts: {}, + draftTransactionIDs: [], activePolicy: undefined, userBillingGraceEndPeriods: undefined, amountOwed: 0, @@ -1947,7 +1933,7 @@ describe('actions/IOU', () => { actionName: CONST.IOU.ACTION.CATEGORIZE, reportActionID: 'some-report-action-id', introSelected: {choice: CONST.ONBOARDING_CHOICES.MANAGE_TEAM}, - allTransactionDrafts: {}, + draftTransactionIDs: [], activePolicy: undefined, userBillingGraceEndPeriods: undefined, amountOwed: 0, @@ -1979,7 +1965,7 @@ describe('actions/IOU', () => { actionName: CONST.IOU.ACTION.CATEGORIZE, reportActionID: 'some-report-action-id', introSelected: {choice: CONST.ONBOARDING_CHOICES.MANAGE_TEAM}, - allTransactionDrafts: {}, + draftTransactionIDs: [], activePolicy: undefined, userBillingGraceEndPeriods: undefined, amountOwed: 0, @@ -11556,7 +11542,7 @@ describe('actions/IOU', () => { currentDate, currentUserPersonalDetails, hasOnlyPersonalPolicies: false, - draftTransactions: undefined, + draftTransactionIDs: [], }); }) .then(async () => { @@ -11579,7 +11565,7 @@ describe('actions/IOU', () => { currentDate, currentUserPersonalDetails, hasOnlyPersonalPolicies: false, - draftTransactions: undefined, + draftTransactionIDs: [], }); }) .then(async () => { @@ -11603,7 +11589,7 @@ describe('actions/IOU', () => { currentDate, currentUserPersonalDetails, hasOnlyPersonalPolicies: false, - draftTransactions: undefined, + draftTransactionIDs: [], }); }) .then(async () => { @@ -11614,7 +11600,7 @@ describe('actions/IOU', () => { }); }); - it('should remove non-optimistic draft transactions when draftTransactions is provided', async () => { + it('should remove non-optimistic draft transactions when draftTransactionIDs is provided', async () => { const otherDraftTransactionID = '123456'; const otherDraftTransaction: Transaction = { ...createRandomTransaction(1), @@ -11624,10 +11610,6 @@ describe('actions/IOU', () => { // 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({ @@ -11641,7 +11623,7 @@ describe('actions/IOU', () => { currentDate, currentUserPersonalDetails, hasOnlyPersonalPolicies: false, - draftTransactions, + draftTransactionIDs: [otherDraftTransactionID], }); }) .then(async () => { @@ -11652,7 +11634,7 @@ describe('actions/IOU', () => { }); }); - it('should preserve optimistic transaction in draftTransactions while removing others', async () => { + it('should preserve optimistic transaction in draftTransactionIDs while removing others', async () => { const otherDraftTransactionID = '789012'; const otherDraftTransaction: Transaction = { ...createRandomTransaction(2), @@ -11667,11 +11649,6 @@ describe('actions/IOU', () => { 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({ @@ -11685,7 +11662,7 @@ describe('actions/IOU', () => { currentDate, currentUserPersonalDetails, hasOnlyPersonalPolicies: false, - draftTransactions, + draftTransactionIDs: [otherDraftTransactionID, CONST.IOU.OPTIMISTIC_TRANSACTION_ID], }); }) .then(async () => { @@ -11696,7 +11673,7 @@ describe('actions/IOU', () => { }); }); - it('should remove multiple draft transactions when draftTransactions contains several entries', async () => { + it('should remove multiple draft transactions when draftTransactionIDs contains several entries', async () => { const draftTransactionID1 = '111111'; const draftTransactionID2 = '222222'; const draftTransaction1: Transaction = { @@ -11712,11 +11689,6 @@ describe('actions/IOU', () => { 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({ @@ -11730,7 +11702,7 @@ describe('actions/IOU', () => { currentDate, currentUserPersonalDetails, hasOnlyPersonalPolicies: false, - draftTransactions, + draftTransactionIDs: [draftTransactionID1, draftTransactionID2], }); }) .then(async () => { diff --git a/tests/ui/ClearReportActionErrorsUITest.tsx b/tests/ui/ClearReportActionErrorsUITest.tsx index cdd04dc103813..a4bef0a1b139e 100644 --- a/tests/ui/ClearReportActionErrorsUITest.tsx +++ b/tests/ui/ClearReportActionErrorsUITest.tsx @@ -105,7 +105,7 @@ describe('ClearReportActionErrors UI', () => { linkedReport={undefined} iouReportOfLinkedReport={undefined} currentUserAccountID={ACTOR_ACCOUNT_ID} - allTransactionDrafts={undefined} + draftTransactionIDs={[]} userBillingGraceEndPeriods={undefined} clearAllRelatedReportActionErrors={clearErrorFn} originalReportID={originalReportID} diff --git a/tests/ui/PureReportActionItemTest.tsx b/tests/ui/PureReportActionItemTest.tsx index e1fd526a5c896..a5144a7e083c8 100644 --- a/tests/ui/PureReportActionItemTest.tsx +++ b/tests/ui/PureReportActionItemTest.tsx @@ -111,7 +111,7 @@ describe('PureReportActionItem', () => { linkedReport={undefined} iouReportOfLinkedReport={undefined} currentUserAccountID={ACTOR_ACCOUNT_ID} - allTransactionDrafts={undefined} + draftTransactionIDs={[]} userBillingGraceEndPeriods={undefined} /> @@ -336,7 +336,7 @@ describe('PureReportActionItem', () => { iouReportOfLinkedReport={undefined} reportMetadata={reportMetadata} currentUserAccountID={ACTOR_ACCOUNT_ID} - allTransactionDrafts={undefined} + draftTransactionIDs={[]} userBillingGraceEndPeriods={undefined} /> @@ -393,7 +393,7 @@ describe('PureReportActionItem', () => { linkedReport={undefined} iouReportOfLinkedReport={undefined} currentUserAccountID={ACTOR_ACCOUNT_ID} - allTransactionDrafts={undefined} + draftTransactionIDs={[]} userBillingGraceEndPeriods={undefined} /> @@ -462,7 +462,7 @@ describe('PureReportActionItem', () => { linkedReport={undefined} iouReportOfLinkedReport={undefined} currentUserAccountID={ACTOR_ACCOUNT_ID} - allTransactionDrafts={undefined} + draftTransactionIDs={[]} userBillingGraceEndPeriods={undefined} /> @@ -526,7 +526,7 @@ describe('PureReportActionItem', () => { linkedReport={undefined} iouReportOfLinkedReport={undefined} currentUserAccountID={ACTOR_ACCOUNT_ID} - allTransactionDrafts={undefined} + draftTransactionIDs={[]} userBillingGraceEndPeriods={undefined} /> @@ -576,7 +576,7 @@ describe('PureReportActionItem', () => { linkedReport={undefined} iouReportOfLinkedReport={undefined} currentUserAccountID={ACTOR_ACCOUNT_ID} - allTransactionDrafts={undefined} + draftTransactionIDs={[]} modifiedExpenseMessage={modifiedExpenseMessage} userBillingGraceEndPeriods={undefined} /> diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index 5227afd2a31b0..2690ce890ba2c 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -12907,7 +12907,7 @@ describe('ReportUtils', () => { actionName: CONST.IOU.ACTION.CATEGORIZE, reportActionID: '1', introSelected: undefined, - allTransactionDrafts: undefined, + draftTransactionIDs: [], activePolicy, userBillingGraceEndPeriods: undefined, amountOwed: 1, @@ -12942,7 +12942,7 @@ describe('ReportUtils', () => { actionName: CONST.IOU.ACTION.CATEGORIZE, reportActionID: '1', introSelected: undefined, - allTransactionDrafts: undefined, + draftTransactionIDs: [], activePolicy, userBillingGraceEndPeriods, amountOwed: 0, @@ -12980,7 +12980,7 @@ describe('ReportUtils', () => { actionName: CONST.IOU.ACTION.CATEGORIZE, reportActionID: '1', introSelected: undefined, - allTransactionDrafts: undefined, + draftTransactionIDs: [], activePolicy, userBillingGraceEndPeriods: undefined, amountOwed: 0, @@ -13020,7 +13020,7 @@ describe('ReportUtils', () => { actionName: CONST.IOU.ACTION.CATEGORIZE, reportActionID: '2', introSelected: undefined, - allTransactionDrafts: undefined, + draftTransactionIDs: [], activePolicy: undefined, userBillingGraceEndPeriods: undefined, amountOwed: 0, @@ -13047,7 +13047,7 @@ describe('ReportUtils', () => { actionName: CONST.IOU.ACTION.CATEGORIZE, reportActionID: '1', introSelected: undefined, - allTransactionDrafts: undefined, + draftTransactionIDs: [], activePolicy: undefined, userBillingGraceEndPeriods: undefined, amountOwed: 0, @@ -13095,7 +13095,7 @@ describe('ReportUtils', () => { actionName: CONST.IOU.ACTION.CATEGORIZE, reportActionID: '1', introSelected: undefined, - allTransactionDrafts: undefined, + draftTransactionIDs: [], activePolicy: undefined, userBillingGraceEndPeriods: undefined, amountOwed: 0, @@ -13139,7 +13139,7 @@ describe('ReportUtils', () => { actionName: CONST.IOU.ACTION.CATEGORIZE, reportActionID: '1', introSelected: undefined, - allTransactionDrafts: undefined, + draftTransactionIDs: [], activePolicy, userBillingGraceEndPeriods: undefined, amountOwed: 0, @@ -13181,7 +13181,7 @@ describe('ReportUtils', () => { actionName: CONST.IOU.ACTION.CATEGORIZE, reportActionID: '1', introSelected: undefined, - allTransactionDrafts: undefined, + draftTransactionIDs: [], activePolicy, userBillingGraceEndPeriods: undefined, amountOwed: 0, @@ -13215,7 +13215,7 @@ describe('ReportUtils', () => { actionName: CONST.IOU.ACTION.CATEGORIZE, reportActionID: '1', introSelected: undefined, - allTransactionDrafts: undefined, + draftTransactionIDs: [], activePolicy, userBillingGraceEndPeriods: undefined, amountOwed: 50, From 1015e7e88b3d99eaa821884b76a5766bfb1f2a16 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Thu, 19 Mar 2026 10:53:13 +0700 Subject: [PATCH 8/9] update test --- tests/unit/hooks/useMobileReceiptScan.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/unit/hooks/useMobileReceiptScan.test.ts b/tests/unit/hooks/useMobileReceiptScan.test.ts index 32b230129c0da..12b46d41b4a0e 100644 --- a/tests/unit/hooks/useMobileReceiptScan.test.ts +++ b/tests/unit/hooks/useMobileReceiptScan.test.ts @@ -18,7 +18,7 @@ jest.mock('@libs/actions/Welcome', () => ({ jest.mock('@userActions/TransactionEdit', () => ({ // eslint-disable-next-line @typescript-eslint/no-unsafe-return - removeDraftTransactions: (...args: unknown[]) => mockRemoveDraftTransactions(...args), + removeDraftTransactionsByIDs: (...args: unknown[]) => mockRemoveDraftTransactions(...args), // eslint-disable-next-line @typescript-eslint/no-unsafe-return removeTransactionReceipt: (...args: unknown[]) => mockRemoveTransactionReceipt(...args), })); @@ -103,7 +103,7 @@ describe('useMobileReceiptScan', () => { expect(result.current.shouldShowMultiScanEducationalPopup).toBe(true); expect(setIsMultiScanEnabled).toHaveBeenCalledWith(true); expect(mockRemoveTransactionReceipt).toHaveBeenCalledWith(CONST.IOU.OPTIMISTIC_TRANSACTION_ID); - expect(mockRemoveDraftTransactions).toHaveBeenCalledWith(true); + expect(mockRemoveDraftTransactions).toHaveBeenCalledWith(expect.anything(), true); }); it('should not set shouldShowMultiScanEducationalPopup to true after the modal is dismissed', async () => { @@ -127,7 +127,7 @@ describe('useMobileReceiptScan', () => { expect(result.current.shouldShowMultiScanEducationalPopup).toBe(false); expect(setIsMultiScanEnabled).toHaveBeenCalledWith(true); expect(mockRemoveTransactionReceipt).toHaveBeenCalledWith(CONST.IOU.OPTIMISTIC_TRANSACTION_ID); - expect(mockRemoveDraftTransactions).toHaveBeenCalledWith(true); + expect(mockRemoveDraftTransactions).toHaveBeenCalledWith(expect.anything(), true); }); }); From f0b23508f41d35c5f8a8acf2b9481fcfdfd72fed Mon Sep 17 00:00:00 2001 From: dukenv0307 <129500732+dukenv0307@users.noreply.github.com> Date: Thu, 19 Mar 2026 11:33:49 +0700 Subject: [PATCH 9/9] Update src/pages/iou/request/step/IOURequestStepScan/hooks/useMobileReceiptScan.ts Co-authored-by: DylanDylann <141406735+DylanDylann@users.noreply.github.com> --- .../step/IOURequestStepScan/hooks/useMobileReceiptScan.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/iou/request/step/IOURequestStepScan/hooks/useMobileReceiptScan.ts b/src/pages/iou/request/step/IOURequestStepScan/hooks/useMobileReceiptScan.ts index db0047d3e0878..0a7240f3bb3be 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/hooks/useMobileReceiptScan.ts +++ b/src/pages/iou/request/step/IOURequestStepScan/hooks/useMobileReceiptScan.ts @@ -33,7 +33,6 @@ function useMobileReceiptScan({ const [dismissedProductTraining] = useOnyx(ONYXKEYS.NVP_DISMISSED_PRODUCT_TRAINING); const [draftTransactionIDs] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {selector: validTransactionDraftIDsSelector}); - const [shouldShowMultiScanEducationalPopup, setShouldShowMultiScanEducationalPopup] = useState(false); const canUseMultiScan = isStartingScan && iouType !== CONST.IOU.TYPE.SPLIT;