diff --git a/src/hooks/useRestartOnReceiptFailure.ts b/src/hooks/useRestartOnReceiptFailure.ts index 6d76695b8d7ee..73f36e0e334df 100644 --- a/src/hooks/useRestartOnReceiptFailure.ts +++ b/src/hooks/useRestartOnReceiptFailure.ts @@ -1,15 +1,21 @@ 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 isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; +import useOnyx from './useOnyx'; const useRestartOnReceiptFailure = (transaction: OnyxEntry, reportID: string, iouType: IOUType, action: IOUAction) => { + 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 // 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. @@ -17,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; @@ -40,13 +46,13 @@ const useRestartOnReceiptFailure = (transaction: OnyxEntry, reportI return; } - removeDraftTransactions(true); + removeDraftTransactionsByIDs(draftTransactionIDs, true); navigateToStartMoneyRequestStep(requestType, iouType, transaction.transactionID, reportID); }); // We want this hook to run on mounting only // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [draftTransactionsMetadata]); }; export default useRestartOnReceiptFailure; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index bf7da4cfcdd01..592115fcf9be6 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'; @@ -11285,7 +11285,7 @@ type CreateDraftTransactionParams = { actionName: IOUAction; reportActionID: string | undefined; introSelected: OnyxEntry; - allTransactionDrafts: OnyxCollection; + draftTransactionIDs: string[] | undefined; activePolicy: OnyxEntry; userBillingGraceEndPeriods: OnyxCollection; amountOwed: OnyxEntry; @@ -11299,7 +11299,7 @@ function createDraftTransactionAndNavigateToParticipantSelector({ actionName, reportActionID, introSelected, - allTransactionDrafts, + draftTransactionIDs, activePolicy, userBillingGraceEndPeriods, amountOwed, @@ -11329,7 +11329,7 @@ function createDraftTransactionAndNavigateToParticipantSelector({ attendees: transaction?.modifiedAttendees ?? baseComment.attendees, }; - removeDraftTransactions(false, allTransactionDrafts); + removeDraftTransactionsByIDs(draftTransactionIDs); createDraftTransaction({ ...transaction, diff --git a/src/libs/actions/IOU/index.ts b/src/libs/actions/IOU/index.ts index 3841fd6e1bca7..fdc5fe67e3d32 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, sanitizeWaypointsForAPI, stringifyWaypointsForAPI} 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'; @@ -1271,12 +1271,7 @@ function initMoneyRequest({ const created = currentDate ?? format(new Date(), 'yyyy-MM-dd'); // We remove draft transactions created during multi scanning if there are some - if (draftTransactionIDs) { - const idsToRemove = draftTransactionIDs.filter((id) => id !== CONST.IOU.OPTIMISTIC_TRANSACTION_ID); - removeDraftTransactionsByIDs(idsToRemove); - } else { - removeDraftTransactions(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/libs/actions/Transaction.ts b/src/libs/actions/Transaction.ts index 324ba20d618f2..e07fe1727a4b0 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, @@ -1622,10 +1613,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}); } @@ -1650,7 +1637,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/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index 0de596655173c..490c6a1bc312d 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 c19904f6894e9..ba0b0ec56e6d7 100644 --- a/src/pages/Share/SubmitDetailsPage.tsx +++ b/src/pages/Share/SubmitDetailsPage.tsx @@ -91,6 +91,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, {selector: hasOnlyPersonalPoliciesUtil}); + useEffect(() => { if (!errorTitle || !errorMessage) { return; 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/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/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index 47df1a1f22f76..9d09be321b9a0 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -101,7 +101,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'; @@ -596,7 +596,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/hooks/useMobileReceiptScan.ts b/src/pages/iou/request/step/IOURequestStepScan/hooks/useMobileReceiptScan.ts index 22bf4240b67b6..0a7240f3bb3be 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,7 @@ 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 +77,7 @@ function useMobileReceiptScan({ setShouldShowMultiScanEducationalPopup(true); } removeTransactionReceipt(CONST.IOU.OPTIMISTIC_TRANSACTION_ID); - removeDraftTransactions(true); + removeDraftTransactionsByIDs(draftTransactionIDs, true); setIsMultiScanEnabled?.(!isMultiScanEnabled); } diff --git a/src/pages/iou/request/step/IOURequestStepScan/hooks/useReceiptScan.ts b/src/pages/iou/request/step/IOURequestStepScan/hooks/useReceiptScan.ts index 7f374973c9005..65a2247cef2f8 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/hooks/useReceiptScan.ts +++ b/src/pages/iou/request/step/IOURequestStepScan/hooks/useReceiptScan.ts @@ -20,7 +20,7 @@ import {getSpan, startSpan} from '@libs/telemetry/activeSpans'; import {getDefaultTaxCode, hasReceipt, shouldReuseInitialTransaction} from '@libs/TransactionUtils'; import type {ReceiptFile, UseReceiptScanParams} from '@pages/iou/request/step/IOURequestStepScan/types'; import {setMoneyRequestReceipt} from '@userActions/IOU'; -import {buildOptimisticTransactionAndCreateDraft, removeDraftTransactions} from '@userActions/TransactionEdit'; +import {buildOptimisticTransactionAndCreateDraft, removeDraftTransactionsByIDs} from '@userActions/TransactionEdit'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import {validTransactionDraftsSelector} from '@src/selectors/TransactionDraft'; @@ -66,6 +66,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 isArchived = isArchivedReport(reportNameValuePairs); @@ -160,7 +161,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); }); } @@ -187,7 +188,7 @@ function useReceiptScan({ } if (!isMultiScanEnabled && isStartingScan) { - removeDraftTransactions(true); + removeDraftTransactionsByIDs(draftTransactionIDs, true); } for (const [index, file] of files.entries()) { diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.tsx b/src/pages/iou/request/step/IOURequestStepScan/index.tsx index 2824febeae81a..1e291b658d86c 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.tsx +++ b/src/pages/iou/request/step/IOURequestStepScan/index.tsx @@ -13,9 +13,10 @@ import {endSpan} from '@libs/telemetry/activeSpans'; import withFullTransactionOrNotFound from '@pages/iou/request/step/withFullTransactionOrNotFound'; import withWritableReportOrNotFound from '@pages/iou/request/step/withWritableReportOrNotFound'; import {checkIfScanFileCanBeRead, replaceReceipt, updateLastLocationPermissionPrompt} from '@userActions/IOU'; -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'; import type {FileObject} from '@src/types/utils/Attachment'; import DesktopWebUploadView from './components/DesktopWebUploadView'; import MobileWebCameraView from './components/MobileWebCameraView'; @@ -38,6 +39,7 @@ function IOURequestStepScan({ const isMobileWeb = isMobile(); const policy = usePolicy(report?.policyID); 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(() => { @@ -120,7 +122,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/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index 56b4003cb3cf3..9610276d7cdc4 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, @@ -11616,6 +11602,7 @@ describe('actions/IOU', () => { currentDate, currentUserPersonalDetails, hasOnlyPersonalPolicies: false, + draftTransactionIDs: [], }); }) .then(async () => { @@ -11638,6 +11625,7 @@ describe('actions/IOU', () => { currentDate, currentUserPersonalDetails, hasOnlyPersonalPolicies: false, + draftTransactionIDs: [], }); }) .then(async () => { @@ -11661,6 +11649,7 @@ describe('actions/IOU', () => { currentDate, currentUserPersonalDetails, hasOnlyPersonalPolicies: false, + draftTransactionIDs: [], }); }) .then(async () => { @@ -11671,7 +11660,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), @@ -11694,6 +11683,7 @@ describe('actions/IOU', () => { currentDate, currentUserPersonalDetails, hasOnlyPersonalPolicies: false, + draftTransactionIDs: [otherDraftTransactionID], }); }) .then(async () => { @@ -11704,7 +11694,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), @@ -11732,6 +11722,7 @@ describe('actions/IOU', () => { currentDate, currentUserPersonalDetails, hasOnlyPersonalPolicies: false, + draftTransactionIDs: [otherDraftTransactionID, CONST.IOU.OPTIMISTIC_TRANSACTION_ID], }); }) .then(async () => { @@ -11742,7 +11733,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 = { @@ -11771,6 +11762,7 @@ describe('actions/IOU', () => { currentDate, currentUserPersonalDetails, hasOnlyPersonalPolicies: false, + draftTransactionIDs: [draftTransactionID1, draftTransactionID2], }); }) .then(async () => { 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(); + }); }); }); 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 18600814b0da1..ca385b7380a8c 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -13221,7 +13221,7 @@ describe('ReportUtils', () => { actionName: CONST.IOU.ACTION.CATEGORIZE, reportActionID: '1', introSelected: undefined, - allTransactionDrafts: undefined, + draftTransactionIDs: [], activePolicy, userBillingGraceEndPeriods: undefined, amountOwed: 1, @@ -13256,7 +13256,7 @@ describe('ReportUtils', () => { actionName: CONST.IOU.ACTION.CATEGORIZE, reportActionID: '1', introSelected: undefined, - allTransactionDrafts: undefined, + draftTransactionIDs: [], activePolicy, userBillingGraceEndPeriods, amountOwed: 0, @@ -13294,7 +13294,7 @@ describe('ReportUtils', () => { actionName: CONST.IOU.ACTION.CATEGORIZE, reportActionID: '1', introSelected: undefined, - allTransactionDrafts: undefined, + draftTransactionIDs: [], activePolicy, userBillingGraceEndPeriods: undefined, amountOwed: 0, @@ -13334,7 +13334,7 @@ describe('ReportUtils', () => { actionName: CONST.IOU.ACTION.CATEGORIZE, reportActionID: '2', introSelected: undefined, - allTransactionDrafts: undefined, + draftTransactionIDs: [], activePolicy: undefined, userBillingGraceEndPeriods: undefined, amountOwed: 0, @@ -13361,7 +13361,7 @@ describe('ReportUtils', () => { actionName: CONST.IOU.ACTION.CATEGORIZE, reportActionID: '1', introSelected: undefined, - allTransactionDrafts: undefined, + draftTransactionIDs: [], activePolicy: undefined, userBillingGraceEndPeriods: undefined, amountOwed: 0, @@ -13409,7 +13409,7 @@ describe('ReportUtils', () => { actionName: CONST.IOU.ACTION.CATEGORIZE, reportActionID: '1', introSelected: undefined, - allTransactionDrafts: undefined, + draftTransactionIDs: [], activePolicy: undefined, userBillingGraceEndPeriods: undefined, amountOwed: 0, @@ -13453,7 +13453,7 @@ describe('ReportUtils', () => { actionName: CONST.IOU.ACTION.CATEGORIZE, reportActionID: '1', introSelected: undefined, - allTransactionDrafts: undefined, + draftTransactionIDs: [], activePolicy, userBillingGraceEndPeriods: undefined, amountOwed: 0, @@ -13495,7 +13495,7 @@ describe('ReportUtils', () => { actionName: CONST.IOU.ACTION.CATEGORIZE, reportActionID: '1', introSelected: undefined, - allTransactionDrafts: undefined, + draftTransactionIDs: [], activePolicy, userBillingGraceEndPeriods: undefined, amountOwed: 0, @@ -13529,7 +13529,7 @@ describe('ReportUtils', () => { actionName: CONST.IOU.ACTION.CATEGORIZE, reportActionID: '1', introSelected: undefined, - allTransactionDrafts: undefined, + draftTransactionIDs: [], activePolicy, userBillingGraceEndPeriods: undefined, amountOwed: 50, 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); }); }); diff --git a/tests/unit/hooks/useReceiptScan.test.ts b/tests/unit/hooks/useReceiptScan.test.ts index 4f38e4b6dea0d..6acc3be489b9f 100644 --- a/tests/unit/hooks/useReceiptScan.test.ts +++ b/tests/unit/hooks/useReceiptScan.test.ts @@ -9,8 +9,8 @@ import type {Report, Transaction} from '@src/types/onyx'; import waitForBatchedUpdatesWithAct from '../../utils/waitForBatchedUpdatesWithAct'; const mockHandleMoneyRequestStepScanParticipants = jest.fn(); +const mockRemoveDraftTransactionsByIDs = jest.fn(); const mockGetMoneyRequestParticipantOptions = jest.fn().mockReturnValue([]); -const mockRemoveDraftTransactions = jest.fn(); const mockRemoveTransactionReceipt = jest.fn(); const mockSetMoneyRequestReceipt = jest.fn(); const mockBuildOptimisticTransactionAndCreateDraft = jest.fn(); @@ -37,7 +37,7 @@ jest.mock('@libs/actions/IOU/MoneyRequest', () => ({ })); jest.mock('@userActions/TransactionEdit', () => ({ - removeDraftTransactions: (...args: unknown[]) => mockRemoveDraftTransactions(...args), + removeDraftTransactionsByIDs: (...args: unknown[]) => mockRemoveDraftTransactionsByIDs(...args), removeTransactionReceipt: (...args: unknown[]) => mockRemoveTransactionReceipt(...args), buildOptimisticTransactionAndCreateDraft: (...args: unknown[]) => mockBuildOptimisticTransactionAndCreateDraft(...args), })); @@ -346,7 +346,7 @@ 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(); @@ -355,7 +355,100 @@ describe('useReceiptScan', () => { result.current.validateFiles(files); }); - expect(mockRemoveDraftTransactions).toHaveBeenCalledWith(true); + 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(); + + const files = [{uri: 'file://receipt.jpg', name: 'receipt.jpg', type: 'image/jpeg'}]; + await act(async () => { + result.current.validateFiles(files); + }); + + 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); + 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 () => {