diff --git a/src/pages/iou/request/IOURequestStartPage.tsx b/src/pages/iou/request/IOURequestStartPage.tsx index e756308f32da5..97b87d2885506 100644 --- a/src/pages/iou/request/IOURequestStartPage.tsx +++ b/src/pages/iou/request/IOURequestStartPage.tsx @@ -97,7 +97,6 @@ function IOURequestStartPage({ }); const [lastSelectedDistanceRates] = useOnyx(ONYXKEYS.NVP_LAST_SELECTED_DISTANCE_RATES); - const [isMultiScanEnabled, setIsMultiScanEnabled] = useState(false); const [currentDate] = useOnyx(ONYXKEYS.CURRENT_DATE); const {isOffline} = useNetwork(); const [hasUserSubmittedExpenseOrScannedReceipt] = useOnyx(ONYXKEYS.NVP_DISMISSED_PRODUCT_TRAINING, {selector: isTestReceiptTooltipDismissedSelector}); @@ -186,7 +185,6 @@ function IOURequestStartPage({ if (transaction?.iouRequestType === newIOUType) { return; } - setIsMultiScanEnabled(false); initMoneyRequest({ reportID, policy, @@ -340,14 +338,12 @@ function IOURequestStartPage({ {() => ( { setTestReceiptAndNavigateRef.current = setTestReceiptAndNavigate; }} - isMultiScanEnabled={isMultiScanEnabled} - setIsMultiScanEnabled={setIsMultiScanEnabled} - isStartingScan /> )} diff --git a/src/pages/iou/request/step/IOURequestStepScan/components/MobileWebCameraView.tsx b/src/pages/iou/request/step/IOURequestStepScan/components/MobileWebCameraView.tsx index 84b8d6b351a14..da3ff64a13c58 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/components/MobileWebCameraView.tsx +++ b/src/pages/iou/request/step/IOURequestStepScan/components/MobileWebCameraView.tsx @@ -43,10 +43,10 @@ type MobileWebCameraViewProps = { iouType: IOUType; currentUserPersonalDetails: CurrentUserPersonalDetails; reportID: string; - isMultiScanEnabled?: boolean; - isStartingScan?: boolean; + isMultiScanEnabled: boolean; + isStartingScan: boolean; updateScanAndNavigate: (file: FileObject, source: string) => void; - setIsMultiScanEnabled?: (value: boolean) => void; + setIsMultiScanEnabled: (value: boolean) => void; PDFValidationComponent: React.ReactNode; shouldAcceptMultipleFiles: boolean; receiptFiles: ReceiptFile[]; @@ -56,9 +56,9 @@ type MobileWebCameraViewProps = { navigateToConfirmationStep: (files: ReceiptFile[], locationPermissionGranted?: boolean, isTestTransaction?: boolean) => void; shouldSkipConfirmation: boolean; setStartLocationPermissionFlow: (value: boolean) => void; - onLayout?: () => void; onBackButtonPress: () => void; shouldShowWrapper: boolean; + onLayout?: () => void; }; /** @@ -103,7 +103,7 @@ function MobileWebCameraView({ iouType, currentUserPersonalDetails, reportID, - isMultiScanEnabled = false, + isMultiScanEnabled, isStartingScan, updateScanAndNavigate, setIsMultiScanEnabled, @@ -116,9 +116,9 @@ function MobileWebCameraView({ navigateToConfirmationStep, shouldSkipConfirmation, setStartLocationPermissionFlow, - onLayout, onBackButtonPress, shouldShowWrapper, + onLayout, }: MobileWebCameraViewProps) { const {blinkStyle, canUseMultiScan, shouldShowMultiScanEducationalPopup, showBlink, toggleMultiScan, dismissMultiScanEducationalPopup, submitReceipts, submitMultiScanReceipts} = useMobileReceiptScan({ @@ -131,6 +131,7 @@ function MobileWebCameraView({ shouldSkipConfirmation, setStartLocationPermissionFlow, setIsMultiScanEnabled, + setReceiptFiles, }); const theme = useTheme(); const styles = useThemeStyles(); diff --git a/src/pages/iou/request/step/IOURequestStepScan/hooks/useMobileReceiptScan.ts b/src/pages/iou/request/step/IOURequestStepScan/hooks/useMobileReceiptScan.ts index 0a7240f3bb3be..aa89b94faa713 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/hooks/useMobileReceiptScan.ts +++ b/src/pages/iou/request/step/IOURequestStepScan/hooks/useMobileReceiptScan.ts @@ -19,12 +19,13 @@ function useMobileReceiptScan({ initialTransaction, iouType, isMultiScanEnabled = false, - isStartingScan = false, + isStartingScan, receiptFiles, navigateToConfirmationStep, shouldSkipConfirmation, setStartLocationPermissionFlow, setIsMultiScanEnabled, + setReceiptFiles, }: UseMobileReceiptScanParams) { const [shouldStartLocationPermissionFlow] = useOnyx(ONYXKEYS.NVP_LAST_LOCATION_PERMISSION_PROMPT, { selector: shouldStartLocationPermissionFlowSelector, @@ -78,7 +79,10 @@ function useMobileReceiptScan({ } removeTransactionReceipt(CONST.IOU.OPTIMISTIC_TRANSACTION_ID); removeDraftTransactionsByIDs(draftTransactionIDs, true); - setIsMultiScanEnabled?.(!isMultiScanEnabled); + if (isMultiScanEnabled) { + setReceiptFiles([]); + } + setIsMultiScanEnabled(!isMultiScanEnabled); } function dismissMultiScanEducationalPopup() { diff --git a/src/pages/iou/request/step/IOURequestStepScan/hooks/useReceiptScan.ts b/src/pages/iou/request/step/IOURequestStepScan/hooks/useReceiptScan.ts index 6b9f64605d8ce..d8ea3695b1b18 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/hooks/useReceiptScan.ts +++ b/src/pages/iou/request/step/IOURequestStepScan/hooks/useReceiptScan.ts @@ -1,6 +1,6 @@ import shouldStartLocationPermissionFlowSelector from '@selectors/LocationPermission'; import {hasSeenTourSelector} from '@selectors/Onboarding'; -import {useEffect, useMemo, useState} from 'react'; +import {useMemo, useState} from 'react'; import TestReceipt from '@assets/images/fake-receipt.png'; import useDefaultExpensePolicy from '@hooks/useDefaultExpensePolicy'; import useFilesValidation from '@hooks/useFilesValidation'; @@ -38,8 +38,6 @@ function useReceiptScan({ currentUserPersonalDetails, backTo, backToReport, - isMultiScanEnabled = false, - isStartingScan = false, updateScanAndNavigate, getSource, }: UseReceiptScanParams) { @@ -70,9 +68,11 @@ function useReceiptScan({ const [userBillingGracePeriodEnds] = useOnyx(ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END); const [ownerBillingGracePeriodEnd] = useOnyx(ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END); const draftTransactionIDs = Object.keys(allTransactionDrafts ?? {}); + const [isMultiScanEnabled, setIsMultiScanEnabled] = useState(false); const isEditing = action === CONST.IOU.ACTION.EDIT; const isReplacingReceipt = (isEditing && hasReceipt(initialTransaction)) || (!!initialTransaction?.receipt && !!backTo); + const isStartingScan = action === CONST.IOU.ACTION.CREATE && !isReplacingReceipt; const shouldAcceptMultipleFiles = !isEditing && !backTo; const shouldGenerateTransactionThreadReport = !isBetaEnabled(CONST.BETAS.NO_OPTIMISTIC_TRANSACTION_THREADS); const isASAPSubmitBetaEnabled = isBetaEnabled(CONST.BETAS.ASAP_SUBMIT); @@ -90,15 +90,6 @@ function useReceiptScan({ const [startLocationPermissionFlow, setStartLocationPermissionFlow] = useState(false); const [receiptFiles, setReceiptFiles] = useState([]); - // Clear receipt files when multi-scan is disabled - useEffect(() => { - if (isMultiScanEnabled) { - return; - } - // eslint-disable-next-line react-hooks/set-state-in-effect - setReceiptFiles([]); - }, [isMultiScanEnabled]); - const [recentWaypoints] = useOnyx(ONYXKEYS.NVP_RECENT_WAYPOINTS); const participants = useMemo( @@ -232,6 +223,9 @@ function useReceiptScan({ }); return { transactions, + isMultiScanEnabled, + setIsMultiScanEnabled, + isStartingScan, isEditing, isReplacingReceipt, shouldAcceptMultipleFiles, diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx index 2c70bc91da24a..66b5b5b3c62b0 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx +++ b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx @@ -64,9 +64,6 @@ function IOURequestStepScan({ transaction: initialTransaction, currentUserPersonalDetails, onLayout, - isMultiScanEnabled = false, - isStartingScan = false, - setIsMultiScanEnabled, }: IOURequestStepScanProps) { const theme = useTheme(); const styles = useThemeStyles(); @@ -288,6 +285,9 @@ function IOURequestStepScan({ const getSource = useCallback((file: FileObject) => file.uri ?? '', []); const { + isMultiScanEnabled, + setIsMultiScanEnabled, + isStartingScan, isEditing, shouldAcceptMultipleFiles, shouldSkipConfirmation, @@ -310,8 +310,6 @@ function IOURequestStepScan({ currentUserPersonalDetails, backTo, backToReport, - isMultiScanEnabled, - isStartingScan, updateScanAndNavigate, getSource, }); @@ -327,6 +325,7 @@ function IOURequestStepScan({ shouldSkipConfirmation, setStartLocationPermissionFlow, setIsMultiScanEnabled, + setReceiptFiles, }); const maybeCancelShutterSpan = useCallback(() => { diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.tsx b/src/pages/iou/request/step/IOURequestStepScan/index.tsx index a3d87108e32c9..ed3175bb9feec 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.tsx +++ b/src/pages/iou/request/step/IOURequestStepScan/index.tsx @@ -32,9 +32,6 @@ function IOURequestStepScan({ transaction: initialTransaction, currentUserPersonalDetails, onLayout, - isMultiScanEnabled = false, - isStartingScan = false, - setIsMultiScanEnabled, }: Omit) { const isMobileWeb = isMobile(); const policy = usePolicy(report?.policyID); @@ -62,6 +59,9 @@ function IOURequestStepScan({ const { transactions, + isMultiScanEnabled, + setIsMultiScanEnabled, + isStartingScan, isEditing, isReplacingReceipt, shouldAcceptMultipleFiles, @@ -85,8 +85,6 @@ function IOURequestStepScan({ currentUserPersonalDetails, backTo, backToReport, - isMultiScanEnabled, - isStartingScan, updateScanAndNavigate, getSource, }); @@ -127,11 +125,10 @@ function IOURequestStepScan({ if (isAllScanFilesCanBeRead) { return; } - setIsMultiScanEnabled?.(false); removeTransactionReceipt(CONST.IOU.OPTIMISTIC_TRANSACTION_ID); removeDraftTransactionsByIDs(draftTransactionIDs, true); }); - }, [setIsMultiScanEnabled, transactions, draftTransactionIDs]); + }, [transactions, draftTransactionIDs]); // this effect will pre-fetch location in web if the location permission is already granted to optimize the flow useEffect(() => { diff --git a/src/pages/iou/request/step/IOURequestStepScan/types.ts b/src/pages/iou/request/step/IOURequestStepScan/types.ts index 280107572e1ca..6772c8792729e 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/types.ts +++ b/src/pages/iou/request/step/IOURequestStepScan/types.ts @@ -38,12 +38,6 @@ type UseReceiptScanParams = { /** Report ID to navigate back to */ backToReport: string | undefined; - /** Whether multi-scan is enabled */ - isMultiScanEnabled: boolean | undefined; - - /** Whether the user is starting a scan request */ - isStartingScan: boolean | undefined; - /** Callback to replace receipt and navigate back when editing */ updateScanAndNavigate: (file: FileObject, source: string) => void; @@ -62,7 +56,7 @@ type UseMobileReceiptScanParams = { isMultiScanEnabled?: boolean; /** Whether the user is starting a scan request */ - isStartingScan?: boolean; + isStartingScan: boolean; /** The current receipt files being scanned */ receiptFiles: ReceiptFile[]; @@ -76,8 +70,11 @@ type UseMobileReceiptScanParams = { /** Callback to start the location permission flow */ setStartLocationPermissionFlow: (value: boolean) => void; - /** Callback to update multi-scan enabled state in parent */ - setIsMultiScanEnabled: ((value: boolean) => void) | undefined; + /** Callback to update multi-scan enabled state */ + setIsMultiScanEnabled: (value: boolean) => void; + + /** Callback to update scanned receipt files */ + setReceiptFiles: (value: ReceiptFile[]) => void; }; type IOURequestStepScanProps = WithCurrentUserPersonalDetailsProps & @@ -90,15 +87,6 @@ type IOURequestStepScanProps = WithCurrentUserPersonalDetailsProps & * Receives a function (`setTestReceiptAndNavigate`) as an argument, */ onLayout?: (setTestReceiptAndNavigate: () => void) => void; - - /** If the receipts preview should be shown */ - isMultiScanEnabled?: boolean; - - /** Updates isMultiScanEnabled flag */ - setIsMultiScanEnabled?: (value: boolean) => void; - - /** Indicates whether users start to create scan request */ - isStartingScan?: boolean; }; type ReceiptFile = { diff --git a/tests/ui/IOURequestStepScanTest.tsx b/tests/ui/IOURequestStepScanTest.tsx index e531a8029e64a..82ee8173bf5da 100644 --- a/tests/ui/IOURequestStepScanTest.tsx +++ b/tests/ui/IOURequestStepScanTest.tsx @@ -1,5 +1,5 @@ import {NavigationContainer} from '@react-navigation/native'; -import {act, render} from '@testing-library/react-native'; +import {act, fireEvent, render, screen} from '@testing-library/react-native'; import React from 'react'; import Onyx from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; @@ -172,17 +172,6 @@ describe('IOURequestStepScan', () => { const POLICY_ID = 'policy-1'; const TRANSACTION_ID_1 = '101'; - const transaction1 = createRandomTransaction(1); - transaction1.reportID = REPORT_ID; - transaction1.transactionID = TRANSACTION_ID_1; - transaction1.receipt = {source: 'file://first-receipt.png', state: CONST.IOU.RECEIPT_STATE.OPEN}; - - await act(async () => { - await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, createMinimalReport(REPORT_ID, POLICY_ID)); - await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${TRANSACTION_ID_1}`, transaction1); - }); - await waitForBatchedUpdates(); - render( @@ -202,9 +191,6 @@ describe('IOURequestStepScan', () => { } as unknown as PlatformStackScreenProps['route'] } navigation={{} as never} - isMultiScanEnabled - isStartingScan - setIsMultiScanEnabled={jest.fn()} /> @@ -213,6 +199,19 @@ describe('IOURequestStepScan', () => { await waitForBatchedUpdatesWithAct(); + fireEvent.press(screen.getByLabelText('multi-scan')); + await waitForBatchedUpdates(); + + const transaction1 = createRandomTransaction(1); + transaction1.reportID = REPORT_ID; + transaction1.transactionID = TRANSACTION_ID_1; + transaction1.receipt = {source: 'file://first-receipt.png', state: CONST.IOU.RECEIPT_STATE.OPEN}; + await act(async () => { + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, createMinimalReport(REPORT_ID, POLICY_ID)); + await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${TRANSACTION_ID_1}`, transaction1); + }); + await waitForBatchedUpdates(); + expect(triggerFileSelection).not.toBeNull(); const secondFile = {name: 'second-receipt.png', type: 'image/png', size: 200, uri: 'file://second-receipt.png'} as FileObject; diff --git a/tests/unit/hooks/useMobileReceiptScan.test.ts b/tests/unit/hooks/useMobileReceiptScan.test.ts index 12b46d41b4a0e..a278b1e1d20ea 100644 --- a/tests/unit/hooks/useMobileReceiptScan.test.ts +++ b/tests/unit/hooks/useMobileReceiptScan.test.ts @@ -37,6 +37,7 @@ function createDefaultParams(): UseMobileReceiptScanParams { shouldSkipConfirmation: false, setStartLocationPermissionFlow: jest.fn(), setIsMultiScanEnabled: jest.fn(), + setReceiptFiles: jest.fn(), }; } @@ -129,6 +130,36 @@ describe('useMobileReceiptScan', () => { expect(mockRemoveTransactionReceipt).toHaveBeenCalledWith(CONST.IOU.OPTIMISTIC_TRANSACTION_ID); expect(mockRemoveDraftTransactions).toHaveBeenCalledWith(expect.anything(), true); }); + + it('should clear receiptFiles when disabling multi-scan', async () => { + const setIsMultiScanEnabled = jest.fn(); + const setReceiptFiles = jest.fn(); + const toggleParams = {...params, setIsMultiScanEnabled, setReceiptFiles, isMultiScanEnabled: true}; + const {result} = renderHook(() => useMobileReceiptScan(toggleParams)); + await waitForBatchedUpdatesWithAct(); + + await act(async () => { + result.current.toggleMultiScan(); + }); + + expect(setReceiptFiles).toHaveBeenCalledWith([]); + expect(setIsMultiScanEnabled).toHaveBeenCalledWith(false); + }); + + it('should not clear receiptFiles when enabling multi-scan', async () => { + const setIsMultiScanEnabled = jest.fn(); + const setReceiptFiles = jest.fn(); + const toggleParams = {...params, setIsMultiScanEnabled, setReceiptFiles, isMultiScanEnabled: false}; + const {result} = renderHook(() => useMobileReceiptScan(toggleParams)); + await waitForBatchedUpdatesWithAct(); + + await act(async () => { + result.current.toggleMultiScan(); + }); + + expect(setReceiptFiles).not.toHaveBeenCalled(); + expect(setIsMultiScanEnabled).toHaveBeenCalledWith(true); + }); }); describe('dismissMultiScanEducationalPopup', () => { diff --git a/tests/unit/hooks/useReceiptScan.test.ts b/tests/unit/hooks/useReceiptScan.test.ts index 6acc3be489b9f..b6d82af878954 100644 --- a/tests/unit/hooks/useReceiptScan.test.ts +++ b/tests/unit/hooks/useReceiptScan.test.ts @@ -62,8 +62,6 @@ function createDefaultParams(): Parameters[0] { getSource: (file: {uri?: string}) => file?.uri ?? 'file://image.png', backTo: undefined, backToReport: undefined, - isMultiScanEnabled: false, - isStartingScan: true, }; } @@ -296,25 +294,6 @@ describe('useReceiptScan', () => { expect(result.current.receiptFiles).toHaveLength(1); expect(result.current.receiptFiles.at(0)).toEqual(receiptFile); }); - - it('should clear receiptFiles when isMultiScanEnabled changes from true to false', async () => { - const multiScanParams = {...params, isMultiScanEnabled: true}; - const {result, rerender} = renderHook((p: Parameters[0]) => useReceiptScan(p), { - initialProps: multiScanParams, - }); - await waitForBatchedUpdatesWithAct(); - - const receiptFile = {file: {uri: 'picture.jpg'}, source: 'file://picture.jpg', transactionID: INITIAL_TRANSACTION_ID}; - await act(async () => { - result.current.setReceiptFiles([receiptFile]); - }); - await waitForBatchedUpdatesWithAct(); - expect(result.current.receiptFiles).toHaveLength(1); - - rerender({...multiScanParams, isMultiScanEnabled: false}); - await waitForBatchedUpdatesWithAct(); - expect(result.current.receiptFiles).toEqual([]); - }); }); describe('processReceipts', () => { @@ -382,6 +361,11 @@ describe('useReceiptScan', () => { const {result} = renderHook(() => useReceiptScan(params)); await waitForBatchedUpdatesWithAct(); + await act(async () => { + result.current.setIsMultiScanEnabled(false); + }); + await waitForBatchedUpdatesWithAct(); + const files = [{uri: 'file://receipt.jpg', name: 'receipt.jpg', type: 'image/jpeg'}]; await act(async () => { result.current.validateFiles(files); @@ -407,8 +391,12 @@ describe('useReceiptScan', () => { }); it('should not call removeDraftTransactionsByIDs when multi-scan is enabled', async () => { - const multiScanParams = {...params, isMultiScanEnabled: true, setIsMultiScanEnabled: jest.fn()}; - const {result} = renderHook(() => useReceiptScan(multiScanParams)); + const {result} = renderHook(() => useReceiptScan(params)); + await waitForBatchedUpdatesWithAct(); + + await act(async () => { + result.current.setIsMultiScanEnabled(true); + }); await waitForBatchedUpdatesWithAct(); const files = [{uri: 'file://receipt.jpg', name: 'receipt.jpg', type: 'image/jpeg'}]; @@ -420,7 +408,7 @@ describe('useReceiptScan', () => { }); it('should not call removeDraftTransactionsByIDs when isStartingScan is false', async () => { - const nonStartingParams = {...params, isStartingScan: false}; + const nonStartingParams = {...params, action: CONST.IOU.ACTION.SUBMIT}; const {result} = renderHook(() => useReceiptScan(nonStartingParams)); await waitForBatchedUpdatesWithAct(); @@ -443,6 +431,11 @@ describe('useReceiptScan', () => { const {result} = renderHook(() => useReceiptScan(params)); await waitForBatchedUpdatesWithAct(); + await act(async () => { + result.current.setIsMultiScanEnabled(false); + }); + await waitForBatchedUpdatesWithAct(); + const files = [{uri: 'file://receipt.jpg', name: 'receipt.jpg', type: 'image/jpeg'}]; await act(async () => { result.current.validateFiles(files);