diff --git a/src/components/ExportDownloadStatusManager.tsx b/src/components/ExportDownloadStatusManager.tsx
new file mode 100644
index 000000000000..6ec31075c4f2
--- /dev/null
+++ b/src/components/ExportDownloadStatusManager.tsx
@@ -0,0 +1,59 @@
+import useOnyx from '@hooks/useOnyx';
+
+import {clearExportDownload} from '@libs/actions/Export';
+
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+
+import React from 'react';
+
+import ExportDownloadStatusModal from './ExportDownloadStatusModal';
+
+/**
+ * Renders the queued export status modal for the whole app. It watches the export collection and shows the modal
+ * for the active export (preparing, ready, or failed). Because it is the single owner of the modal and reads
+ * straight from Onyx, a screen only has to start an export (which writes its record); this shows the progress,
+ * delivers the file when it is ready, and still surfaces it after a reload or once the screen that started it is
+ * gone. There is no per-screen modal to coordinate with.
+ */
+function ExportDownloadStatusManager() {
+ const [exportDownloads] = useOnyx(ONYXKEYS.COLLECTION.EXPORT_DOWNLOAD);
+
+ // Skip Concierge hand-offs: once the user opts into Concierge delivery, the file (or the error) is delivered
+ // through the Concierge chat, so the modal has nothing left to show. Setting shouldSendFromConcierge is what
+ // closes the modal, and the worker deletes the record once it is done.
+ const activeEntry = Object.entries(exportDownloads ?? {}).find(
+ ([, exportDownload]) =>
+ !exportDownload?.shouldSendFromConcierge &&
+ (exportDownload?.state === CONST.EXPORT_DOWNLOAD.STATE.PREPARING ||
+ exportDownload?.state === CONST.EXPORT_DOWNLOAD.STATE.READY ||
+ exportDownload?.state === CONST.EXPORT_DOWNLOAD.STATE.FAILED),
+ );
+
+ if (!activeEntry) {
+ return null;
+ }
+
+ const exportID = activeEntry[0].replace(ONYXKEYS.COLLECTION.EXPORT_DOWNLOAD, '');
+ const exportDownload = activeEntry[1];
+
+ const handleClose = () => {
+ // The modal blocks dismissal while still preparing, so this is belt-and-suspenders.
+ if (exportDownload?.state === CONST.EXPORT_DOWNLOAD.STATE.PREPARING) {
+ return;
+ }
+ clearExportDownload(exportID, exportDownload);
+ };
+
+ return (
+
+ );
+}
+
+ExportDownloadStatusManager.displayName = 'ExportDownloadStatusManager';
+
+export default ExportDownloadStatusManager;
diff --git a/src/components/ExportDownloadStatusModal.tsx b/src/components/ExportDownloadStatusModal.tsx
index 086291969a4a..1021e25a7069 100644
--- a/src/components/ExportDownloadStatusModal.tsx
+++ b/src/components/ExportDownloadStatusModal.tsx
@@ -2,7 +2,6 @@ import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'
import useEnvironment from '@hooks/useEnvironment';
import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
-import useOpenConciergeAnywhere from '@hooks/useOpenConciergeAnywhere';
import usePreviousDefined from '@hooks/usePreviousDefined';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useThemeStyles from '@hooks/useThemeStyles';
@@ -64,7 +63,6 @@ function ExportDownloadStatusModal({exportID, isVisible, onClose, failedBody}: E
const receiptCount = displayedExport?.receiptCount;
const failedReceiptCount = displayedExport?.failedReceiptCount ?? 0;
const isPreparing = state === CONST.EXPORT_DOWNLOAD.STATE.PREPARING && !shouldSendFromConcierge;
- const isConcierge = !!shouldSendFromConcierge;
const isReady = state === CONST.EXPORT_DOWNLOAD.STATE.READY;
const isFailed = state === CONST.EXPORT_DOWNLOAD.STATE.FAILED;
const isEmptyReceipts = isReady && exportType === CONST.EXPORT_DOWNLOAD.TYPE.RECEIPTS && receiptCount === 0;
@@ -94,17 +92,11 @@ function ExportDownloadStatusModal({exportID, isVisible, onClose, failedBody}: E
const handleSendFromConcierge = () => {
sendExportFileFromConcierge(exportID, displayedExport ?? undefined);
};
- const {openConciergeAnywhere} = useOpenConciergeAnywhere();
-
- const handleGoToConcierge = () => {
- onClose();
- openConciergeAnywhere({forceConcierge: true});
- };
const handleDownloadFile = () => {
downloadFile();
- // Clearing the export download is owned by the parent's onClose handler (it runs on every dismissal and
- // skips the clear for the Concierge path). Clearing here too would queue a duplicate ClearExportDownload write.
+ // Clearing the export download is owned by the parent's onClose handler (it runs on every dismissal).
+ // Clearing here too would queue a duplicate ClearExportDownload write.
onClose();
};
@@ -128,26 +120,6 @@ function ExportDownloadStatusModal({exportID, isVisible, onClose, failedBody}: E
);
}
- if (isConcierge) {
- return (
- <>
- {translate('exportDownload.conciergeTitle')}
- {translate('exportDownload.conciergeBody')}
-
-
- >
- );
- }
-
if (isEmptyReceipts) {
return (
<>
diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx
index 92f168746b29..1fba8db9c14a 100644
--- a/src/components/MoneyReportHeader.tsx
+++ b/src/components/MoneyReportHeader.tsx
@@ -24,7 +24,6 @@ import {View} from 'react-native';
import HeaderLoadingBar from './HeaderLoadingBar';
import HeaderWithBackButton from './HeaderWithBackButton';
import MoneyReportHeaderActions from './MoneyReportHeaderActions';
-import {ExportDownloadStatusProvider} from './MoneyReportHeaderActions/ExportDownloadStatusProvider';
import MoneyReportHeaderModals from './MoneyReportHeaderModals';
import MoneyReportHeaderMoreContent from './MoneyReportHeaderMoreContent';
import {PaymentAnimationsProvider} from './PaymentAnimationsContext';
@@ -44,15 +43,13 @@ type MoneyReportHeaderProps = {
function MoneyReportHeader({reportID, shouldDisplayBackButton = false, onBackButtonPress}: MoneyReportHeaderProps) {
return (
-
-
-
-
-
+
+
+
);
}
diff --git a/src/components/MoneyReportHeaderActions/ExportDownloadStatusProvider.tsx b/src/components/MoneyReportHeaderActions/ExportDownloadStatusProvider.tsx
deleted file mode 100644
index 4a46ba784640..000000000000
--- a/src/components/MoneyReportHeaderActions/ExportDownloadStatusProvider.tsx
+++ /dev/null
@@ -1,42 +0,0 @@
-import {useSearchSelectionActions} from '@components/Search/SearchContext';
-
-import useExportDownloadStatusModal from '@hooks/useExportDownloadStatusModal';
-
-import React, {createContext, useContext} from 'react';
-
-type ExportDownloadStatusContextValue = {
- /** Start tracking a queued export so the shared status modal renders for it */
- trackExport: (exportID: string) => void;
-};
-
-const ExportDownloadStatusContext = createContext({
- trackExport: () => {},
-});
-
-type ExportDownloadStatusProviderProps = {
- /** The children to render inside the provider */
- children: React.ReactNode;
-};
-
-/**
- * Owns the queued export status modal for the money report header. The state lives here, above the
- * two mutually-exclusive layout branches in MoneyReportHeader, so the modal survives orientation /
- * layout changes that remount the header actions subtree.
- */
-function ExportDownloadStatusProvider({children}: ExportDownloadStatusProviderProps) {
- const {clearSelectedTransactions} = useSearchSelectionActions();
- const {trackExport, exportDownloadStatusModal} = useExportDownloadStatusModal(() => clearSelectedTransactions(true));
-
- return (
-
- {exportDownloadStatusModal}
- {children}
-
- );
-}
-
-function useExportDownloadStatus(): ExportDownloadStatusContextValue {
- return useContext(ExportDownloadStatusContext);
-}
-
-export {ExportDownloadStatusProvider, useExportDownloadStatus};
diff --git a/src/components/MoneyRequestReportView/SelectionToolbar/index.tsx b/src/components/MoneyRequestReportView/SelectionToolbar/index.tsx
index a7abed31dcc6..b84b12f1a46d 100644
--- a/src/components/MoneyRequestReportView/SelectionToolbar/index.tsx
+++ b/src/components/MoneyRequestReportView/SelectionToolbar/index.tsx
@@ -9,7 +9,6 @@ import BulkDuplicateHandler from '@components/Search/BulkDuplicateHandler';
import {useSearchSelectionActions, useSearchSelectionContext} from '@components/Search/SearchContext';
import useConfirmModal from '@hooks/useConfirmModal';
-import useExportDownloadStatusModal from '@hooks/useExportDownloadStatusModal';
import useFilterSelectedTransactions from '@hooks/useFilterSelectedTransactions';
import useLocalize from '@hooks/useLocalize';
import useMobileSelectionMode from '@hooks/useMobileSelectionMode';
@@ -82,7 +81,6 @@ function SelectionToolbar({reportID, transactions, reportActions}: SelectionTool
const isMobileSelectionModeEnabled = useMobileSelectionMode();
const {showConfirmModal} = useConfirmModal();
- const {trackExport, exportDownloadStatusModal} = useExportDownloadStatusModal(() => clearSelectedTransactions(true));
const [offlineModalVisible, setOfflineModalVisible] = useState(false);
const [isDownloadErrorModalVisible, setIsDownloadErrorModalVisible] = useState(false);
@@ -100,7 +98,7 @@ function SelectionToolbar({reportID, transactions, reportActions}: SelectionTool
return;
}
- const exportID = queueExportSearchWithTemplate(
+ queueExportSearchWithTemplate(
{
templateName,
templateType,
@@ -112,7 +110,8 @@ function SelectionToolbar({reportID, transactions, reportActions}: SelectionTool
},
true,
);
- trackExport(exportID);
+ // Clear the selection now that the export has started; the app-level ExportDownloadStatusManager shows the modal.
+ clearSelectedTransactions(true);
};
const onDeleteSelected = (handleDeleteTransactions: () => void, handleDeleteTransactionsWithNavigation: (backToRoute?: Route) => void) => {
@@ -238,7 +237,6 @@ function SelectionToolbar({reportID, transactions, reportActions}: SelectionTool
return (
<>
- {exportDownloadStatusModal}
{isDuplicateOptionVisible && (
)}
- {exportDownloadStatusModal}
>
);
}
diff --git a/src/hooks/useExportActions.ts b/src/hooks/useExportActions.ts
index da4a98859094..4f0f6ded763b 100644
--- a/src/hooks/useExportActions.ts
+++ b/src/hooks/useExportActions.ts
@@ -1,6 +1,6 @@
import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types';
-import {useExportDownloadStatus} from '@components/MoneyReportHeaderActions/ExportDownloadStatusProvider';
import type {PopoverMenuItem} from '@components/PopoverMenu';
+import {useSearchSelectionActions} from '@components/Search/SearchContext';
import {exportReceiptsToZip} from '@libs/actions/Export';
import {openOldDotLink} from '@libs/actions/Link';
@@ -70,7 +70,7 @@ function useExportActions({reportID, policy, onPDFModalOpen}: UseExportActionsPa
const {showDecisionModal} = useDecisionModal();
const {triggerExportOrConfirm} = useExportAgainModal(moneyRequestReport?.reportID, moneyRequestReport?.policyID);
- const {trackExport} = useExportDownloadStatus();
+ const {clearSelectedTransactions} = useSearchSelectionActions();
const expensifyIcons = useMemoizedLazyExpensifyIcons([
'Table',
@@ -116,7 +116,7 @@ function useExportActions({reportID, policy, onPDFModalOpen}: UseExportActionsPa
return;
}
- const exportID = queueExportSearchWithTemplate(
+ queueExportSearchWithTemplate(
{
templateName,
templateType,
@@ -128,7 +128,8 @@ function useExportActions({reportID, policy, onPDFModalOpen}: UseExportActionsPa
},
true,
);
- trackExport(exportID);
+ // Clear the selection now that the export has started; the app-level ExportDownloadStatusManager shows the modal.
+ clearSelectedTransactions(true);
};
const exportSubmenuOptions: Record> = {
@@ -278,8 +279,8 @@ function useExportActions({reportID, policy, onPDFModalOpen}: UseExportActionsPa
if (!moneyRequestReport?.reportID) {
return;
}
- const exportID = exportReceiptsToZip([moneyRequestReport.reportID]);
- trackExport(exportID);
+ exportReceiptsToZip([moneyRequestReport.reportID]);
+ clearSelectedTransactions(true);
},
},
[CONST.REPORT.SECONDARY_ACTIONS.PRINT]: {
diff --git a/src/hooks/useExportDownloadStatusModal.tsx b/src/hooks/useExportDownloadStatusModal.tsx
deleted file mode 100644
index d8d7f13eacfc..000000000000
--- a/src/hooks/useExportDownloadStatusModal.tsx
+++ /dev/null
@@ -1,56 +0,0 @@
-import ExportDownloadStatusModal from '@components/ExportDownloadStatusModal';
-
-import {clearExportDownload} from '@libs/actions/Export';
-
-import CONST from '@src/CONST';
-import ONYXKEYS from '@src/ONYXKEYS';
-
-import React, {useState} from 'react';
-
-import useOnyx from './useOnyx';
-
-type UseExportDownloadStatusModalReturn = {
- /** Start tracking a queued export so the status modal renders for it */
- trackExport: (exportID: string) => void;
-
- /** The realtime export status modal for the in-progress export (or null when none is active). Render it directly in the consumer. */
- exportDownloadStatusModal: React.JSX.Element | null;
-};
-
-/**
- * Encapsulates the shared wiring for the queued export status modal (ExportDownloadStatusModal): it tracks the
- * active export, renders the modal, and handles close/cleanup (no-op while still preparing, unless handed off to
- * Concierge). Used by every surface that triggers a tracked template export so the modal wiring lives in one place.
- *
- * @param onCleanup - Optional extra cleanup to run once the modal is dismissed (e.g. clearing the selection).
- */
-function useExportDownloadStatusModal(onCleanup?: () => void): UseExportDownloadStatusModalReturn {
- const [activeExportID, setActiveExportID] = useState(undefined);
- const [activeExportDownload] = useOnyx(`${ONYXKEYS.COLLECTION.EXPORT_DOWNLOAD}${activeExportID}`);
-
- const handleExportModalClose = () => {
- // Keep the modal open while the export is still preparing (unless it was handed off to Concierge).
- if (activeExportDownload?.state === CONST.EXPORT_DOWNLOAD.STATE.PREPARING && !activeExportDownload?.shouldSendFromConcierge) {
- return;
- }
- // For the Concierge path the worker deletes the NVP after sending, so clearing it here would wipe
- // shouldSendFromConcierge before the worker reads it and the file would never reach Concierge.
- if (activeExportID && !activeExportDownload?.shouldSendFromConcierge) {
- clearExportDownload(activeExportID, activeExportDownload);
- }
- setActiveExportID(undefined);
- onCleanup?.();
- };
-
- const exportDownloadStatusModal = activeExportID ? (
-
- ) : null;
-
- return {trackExport: setActiveExportID, exportDownloadStatusModal};
-}
-
-export default useExportDownloadStatusModal;
diff --git a/src/hooks/useSearchBulkActions.ts b/src/hooks/useSearchBulkActions.ts
index b773dec8561e..973daa9952f1 100644
--- a/src/hooks/useSearchBulkActions.ts
+++ b/src/hooks/useSearchBulkActions.ts
@@ -118,7 +118,6 @@ import useDelegateAccountID from './useDelegateAccountID';
import useDeleteTransactions from './useDeleteTransactions';
import useDuplicateTransactionsAndViolations from './useDuplicateTransactionsAndViolations';
import useEnvironment from './useEnvironment';
-import useExportDownloadStatusModal from './useExportDownloadStatusModal';
import {useMemoizedLazyExpensifyIcons} from './useLazyAsset';
import useLocalize from './useLocalize';
import useNetwork from './useNetwork';
@@ -490,10 +489,6 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) {
> | null>(null);
const [emptyReportsCount, setEmptyReportsCount] = useState(0);
- const {trackExport, exportDownloadStatusModal} = useExportDownloadStatusModal(() => {
- selectAllMatchingItems(false);
- clearSelectedTransactions(undefined, true);
- });
const [dismissedRejectUseExplanation] = useOnyx(ONYXKEYS.NVP_DISMISSED_REJECT_USE_EXPLANATION);
const [dismissedHoldUseExplanation] = useOnyx(ONYXKEYS.NVP_DISMISSED_HOLD_USE_EXPLANATION);
@@ -802,10 +797,9 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) {
return;
}
const serializedQuery = queryJSON ? serializeQueryJSONForBackend(queryJSON) : JSON.stringify(queryJSON);
- let exportID: string;
if (areAllMatchingItemsSelected) {
- exportID = queueExportSearchWithTemplate(
+ queueExportSearchWithTemplate(
{
templateName,
templateType,
@@ -819,7 +813,7 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) {
);
} else {
const isGroupExport = !!queryJSON?.groupBy && selectedTransactionsKeys.some((key) => key.startsWith(CONST.SEARCH.GROUP_PREFIX));
- exportID = queueExportSearchWithTemplate(
+ queueExportSearchWithTemplate(
{
templateName,
templateType,
@@ -837,7 +831,9 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) {
true,
);
}
- trackExport(exportID);
+ // Clear the selection now that the export has started; ExportDownloadStatusManager shows the modal.
+ selectAllMatchingItems(false);
+ clearSelectedTransactions(undefined, true);
},
[
selectedReports,
@@ -848,7 +844,8 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) {
queryJSON,
selectedTransactionReportIDs,
selectedTransactionsKeys,
- trackExport,
+ selectAllMatchingItems,
+ clearSelectedTransactions,
],
);
@@ -918,7 +915,7 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) {
}
const reportIDList = selectedReports?.map((report) => report?.reportID).filter((reportID) => reportID !== undefined) ?? [];
const exportParameters = getCSVExportParameters(isBasicExport, allMatchingExportData?.queryJSON ?? queryJSON);
- const exportID = queueExportSearchItemsToCSV({
+ queueExportSearchItemsToCSV({
jsonQuery: exportParameters.jsonQuery,
reportIDList,
transactionIDList: selectedTransactionsKeys,
@@ -927,7 +924,9 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) {
exportColumnLabels: exportParameters.exportColumnLabels,
exportName,
});
- trackExport(exportID);
+ // Clear the selection now that the export has started; ExportDownloadStatusManager shows the modal.
+ selectAllMatchingItems(false);
+ clearSelectedTransactions(undefined, true);
return;
}
@@ -972,10 +971,10 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) {
excludedTransactions,
translate,
clearSelectedTransactions,
+ selectAllMatchingItems,
hash,
currentSearchResults?.data,
getCSVExportParameters,
- trackExport,
],
);
@@ -2194,8 +2193,10 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) {
setIsPdfModalVisible(true);
return;
}
- const exportID = exportReportsToPDF(selectedReportIDs);
- trackExport(exportID);
+ exportReportsToPDF(selectedReportIDs);
+ // Clear the selection now that the export has started; ExportDownloadStatusManager shows the modal.
+ selectAllMatchingItems(false);
+ clearSelectedTransactions(undefined, true);
},
});
}
@@ -2539,7 +2540,7 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) {
isProduction,
shouldOpenSplitExpenseEditFlowOnDelete,
styles.textWrap,
- trackExport,
+ selectAllMatchingItems,
allReportsShouldMarkAsDone,
noReportsShouldMarkAsDone,
queryJSON?.groupBy,
@@ -2617,7 +2618,6 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) {
handleExpensifyCardStatementPDFModalHide,
isExpensifyCardStatementMultiFeedAlertVisible,
handleExpensifyCardStatementMultiFeedAlertClose,
- exportDownloadStatusModal,
dismissModalAndUpdateUseHold,
dismissRejectModalBasedOnAction,
isDuplicateOptionVisible,
diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx
index d33ff7a10b95..6a8b7f699371 100644
--- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx
+++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx
@@ -1,5 +1,6 @@
import ComposeProviders from '@components/ComposeProviders';
import DelegateNoAccessModalProvider from '@components/DelegateNoAccessModalProvider';
+import ExportDownloadStatusManager from '@components/ExportDownloadStatusManager';
import GPSInProgressModal from '@components/GPSInProgressModal';
import GPSTripStateChecker from '@components/GPSTripStateChecker';
import {KeyboardDismissibleFlatListContextProvider} from '@components/KeyboardDismissibleFlatList/KeyboardDismissibleFlatListContext';
@@ -167,6 +168,7 @@ function AuthScreens() {
+
{
expect(value).toEqual(expect.objectContaining({state: 'failed'}));
});
- test('clearStaleExportDownloads clears ready/failed entries but preserves preparing ones', async () => {
+ test('clearStaleExportDownloads clears failed entries but keeps preparing and ready ones', async () => {
const key1 = `${ONYXKEYS.COLLECTION.EXPORT_DOWNLOAD}stale-1` as const;
const key2 = `${ONYXKEYS.COLLECTION.EXPORT_DOWNLOAD}stale-2` as const;
const key3 = `${ONYXKEYS.COLLECTION.EXPORT_DOWNLOAD}stale-3` as const;
@@ -128,8 +128,22 @@ describe('Export actions', () => {
const value1 = await getOnyxValue(`${ONYXKEYS.COLLECTION.EXPORT_DOWNLOAD}stale-1`);
const value2 = await getOnyxValue(`${ONYXKEYS.COLLECTION.EXPORT_DOWNLOAD}stale-2`);
const value3 = await getOnyxValue(`${ONYXKEYS.COLLECTION.EXPORT_DOWNLOAD}stale-3`);
- expect(value1).toBeUndefined();
+ expect(value1).toEqual(expect.objectContaining({state: 'ready'}));
expect(value2).toBeUndefined();
expect(value3).toEqual(expect.objectContaining({state: 'preparing'}));
});
+
+ test('clearStaleExportDownloads leaves a preparing Concierge hand-off untouched', async () => {
+ const key = `${ONYXKEYS.COLLECTION.EXPORT_DOWNLOAD}stale-concierge` as const;
+ await Onyx.merge(key, {state: 'preparing', shouldSendFromConcierge: true});
+ await waitForBatchedUpdates();
+
+ Export.clearStaleExportDownloads();
+ await waitForBatchedUpdates();
+
+ // Concierge delivery is owned by the worker, so the record is left as-is; the status manager skips it by
+ // checking shouldSendFromConcierge.
+ const value = await getOnyxValue(`${ONYXKEYS.COLLECTION.EXPORT_DOWNLOAD}stale-concierge`);
+ expect(value).toEqual({state: 'preparing', shouldSendFromConcierge: true});
+ });
});
diff --git a/tests/unit/ExportDownloadStatusModalTest.tsx b/tests/unit/ExportDownloadStatusModalTest.tsx
index 520a01d61a28..62f51fc1361d 100644
--- a/tests/unit/ExportDownloadStatusModalTest.tsx
+++ b/tests/unit/ExportDownloadStatusModalTest.tsx
@@ -30,14 +30,6 @@ jest.mock('@libs/Navigation/Navigation', () => ({
isTopmostRouteModalScreen: jest.fn(() => false),
getActiveRouteWithoutParams: jest.fn(() => ''),
}));
-const mockOpenConciergeAnywhere = jest.fn();
-jest.mock('@hooks/useOpenConciergeAnywhere', () => ({
- __esModule: true,
- default: () => ({
- openConciergeAnywhere: mockOpenConciergeAnywhere,
- isInSidePanel: false,
- }),
-}));
jest.mock('@hooks/useLocalize', () => ({
__esModule: true,
default: () => ({
@@ -100,7 +92,7 @@ describe('ExportDownloadStatusModal', () => {
expect(onClose).not.toHaveBeenCalled();
});
- it('transitions to Concierge state on Send button press', async () => {
+ it('calls sendExportFileFromConcierge when the Send button is pressed', async () => {
await Onyx.set(`${ONYXKEYS.COLLECTION.EXPORT_DOWNLOAD}${EXPORT_ID}`, {state: 'preparing'});
renderModal();
@@ -111,18 +103,6 @@ describe('ExportDownloadStatusModal', () => {
expect(mockSendFromConcierge).toHaveBeenCalledWith(EXPORT_ID, expect.objectContaining({state: 'preparing'}));
});
- it('shows Concierge state when shouldSendFromConcierge is true', async () => {
- await Onyx.set(`${ONYXKEYS.COLLECTION.EXPORT_DOWNLOAD}${EXPORT_ID}`, {state: 'preparing', shouldSendFromConcierge: true});
-
- renderModal();
- await waitForBatchedUpdatesWithAct();
-
- expect(screen.getByText('exportDownload.conciergeTitle')).toBeTruthy();
- expect(screen.getByText('exportDownload.conciergeBody')).toBeTruthy();
- expect(screen.getByText('exportDownload.goToConcierge')).toBeTruthy();
- expect(screen.getByText('exportDownload.dismiss')).toBeTruthy();
- });
-
it('auto-downloads CSV on ready state transition with csvexport secureType', async () => {
await Onyx.set(`${ONYXKEYS.COLLECTION.EXPORT_DOWNLOAD}${EXPORT_ID}`, {state: 'ready', fileName: CSV_FILE_NAME});
@@ -206,19 +186,6 @@ describe('ExportDownloadStatusModal', () => {
expect(screen.getByText('exportDownload.readyTitle')).toBeTruthy();
});
- it('"Go to Concierge" navigates and closes', async () => {
- const onClose = jest.fn();
- await Onyx.set(`${ONYXKEYS.COLLECTION.EXPORT_DOWNLOAD}${EXPORT_ID}`, {state: 'preparing', shouldSendFromConcierge: true});
-
- renderModal({onClose});
- await waitForBatchedUpdatesWithAct();
-
- fireEvent.press(screen.getByText('exportDownload.goToConcierge'));
-
- expect(onClose).toHaveBeenCalled();
- expect(mockOpenConciergeAnywhere).toHaveBeenCalled();
- });
-
it('shows partial failure body when failedReportCount > 0 in ready state', async () => {
await Onyx.set(`${ONYXKEYS.COLLECTION.EXPORT_DOWNLOAD}${EXPORT_ID}`, {
state: 'ready',
diff --git a/tests/unit/hooks/useExportActionsTest.ts b/tests/unit/hooks/useExportActionsTest.ts
index 003334f6ac76..124fac2df11c 100644
--- a/tests/unit/hooks/useExportActionsTest.ts
+++ b/tests/unit/hooks/useExportActionsTest.ts
@@ -1,11 +1,13 @@
import {act, renderHook} from '@testing-library/react-native';
+import type * as SearchContextModule from '@components/Search/SearchContext';
+
import useExportActions from '@hooks/useExportActions';
import {queueExportSearchWithTemplate} from '@libs/actions/Search';
const mockQueueExportSearchWithTemplate = jest.mocked(queueExportSearchWithTemplate);
-const mockTrackExport = jest.fn();
+const mockClearSelectedTransactions = jest.fn();
const REPORT_ID = 'report1';
const POLICY_ID = 'policy1';
@@ -27,8 +29,9 @@ jest.mock('@libs/actions/Link', () => ({
openOldDotLink: jest.fn(),
}));
-jest.mock('@components/MoneyReportHeaderActions/ExportDownloadStatusProvider', () => ({
- useExportDownloadStatus: () => ({trackExport: mockTrackExport}),
+jest.mock('@components/Search/SearchContext', () => ({
+ ...jest.requireActual('@components/Search/SearchContext'),
+ useSearchSelectionActions: () => ({clearSelectedTransactions: mockClearSelectedTransactions}),
}));
let mockIsOffline = false;
@@ -113,7 +116,7 @@ describe('useExportActions - template export status modal', () => {
},
true,
);
- expect(mockTrackExport).toHaveBeenCalledWith('mock-export-id');
+ expect(mockClearSelectedTransactions).toHaveBeenCalledWith(true);
});
it('does not queue the export and shows the offline modal when offline', () => {
diff --git a/tests/unit/hooks/useExportDownloadStatusModalTest.ts b/tests/unit/hooks/useExportDownloadStatusModalTest.ts
deleted file mode 100644
index 8ea3d41b6188..000000000000
--- a/tests/unit/hooks/useExportDownloadStatusModalTest.ts
+++ /dev/null
@@ -1,107 +0,0 @@
-import {act, renderHook} from '@testing-library/react-native';
-
-import useExportDownloadStatusModal from '@hooks/useExportDownloadStatusModal';
-
-import {clearExportDownload} from '@libs/actions/Export';
-
-import CONST from '@src/CONST';
-
-import type {ReactElement} from 'react';
-
-const mockClearExportDownload = jest.mocked(clearExportDownload);
-
-jest.mock('@libs/actions/Export', () => ({
- clearExportDownload: jest.fn(),
-}));
-
-jest.mock('@hooks/useLocalize', () => ({
- __esModule: true,
- default: () => ({translate: (key: string) => key}),
-}));
-
-let mockExportDownload: {state?: string; shouldSendFromConcierge?: boolean} | undefined;
-jest.mock('@hooks/useOnyx', () => ({
- __esModule: true,
- default: () => [mockExportDownload],
-}));
-
-type ExportDownloadStatusModalProps = {exportID: string; onClose: () => void};
-
-describe('useExportDownloadStatusModal', () => {
- beforeEach(() => {
- jest.clearAllMocks();
- mockExportDownload = undefined;
- });
-
- it('renders no modal until an export is tracked', () => {
- const {result} = renderHook(() => useExportDownloadStatusModal());
- expect(result.current.exportDownloadStatusModal).toBeNull();
- });
-
- it('renders the status modal for the tracked export', () => {
- const {result} = renderHook(() => useExportDownloadStatusModal());
-
- act(() => {
- result.current.trackExport('export-1');
- });
-
- const modal: ReactElement | null = result.current.exportDownloadStatusModal;
- expect(modal?.props.exportID).toBe('export-1');
- });
-
- it('clears the download, runs cleanup and hides the modal on close', () => {
- const onCleanup = jest.fn();
- const {result} = renderHook(() => useExportDownloadStatusModal(onCleanup));
-
- act(() => {
- result.current.trackExport('export-1');
- });
- const modal: ReactElement | null = result.current.exportDownloadStatusModal;
-
- act(() => {
- modal?.props.onClose();
- });
-
- expect(mockClearExportDownload).toHaveBeenCalledWith('export-1', undefined);
- expect(onCleanup).toHaveBeenCalled();
- expect(result.current.exportDownloadStatusModal).toBeNull();
- });
-
- it('keeps the export NVP intact when sending via Concierge', () => {
- mockExportDownload = {state: CONST.EXPORT_DOWNLOAD.STATE.READY, shouldSendFromConcierge: true};
- const onCleanup = jest.fn();
- const {result} = renderHook(() => useExportDownloadStatusModal(onCleanup));
-
- act(() => {
- result.current.trackExport('export-1');
- });
- const modal: ReactElement | null = result.current.exportDownloadStatusModal;
-
- act(() => {
- modal?.props.onClose();
- });
-
- expect(mockClearExportDownload).not.toHaveBeenCalled();
- expect(onCleanup).toHaveBeenCalled();
- expect(result.current.exportDownloadStatusModal).toBeNull();
- });
-
- it('keeps the modal open and skips cleanup while the export is still preparing', () => {
- mockExportDownload = {state: CONST.EXPORT_DOWNLOAD.STATE.PREPARING};
- const onCleanup = jest.fn();
- const {result} = renderHook(() => useExportDownloadStatusModal(onCleanup));
-
- act(() => {
- result.current.trackExport('export-1');
- });
- const modal: ReactElement | null = result.current.exportDownloadStatusModal;
-
- act(() => {
- modal?.props.onClose();
- });
-
- expect(mockClearExportDownload).not.toHaveBeenCalled();
- expect(onCleanup).not.toHaveBeenCalled();
- expect(result.current.exportDownloadStatusModal).not.toBeNull();
- });
-});
diff --git a/tests/unit/hooks/useSearchBulkActionsDownloadPDFTest.ts b/tests/unit/hooks/useSearchBulkActionsDownloadPDFTest.ts
index 39418c32d0f0..37a87017a5a1 100644
--- a/tests/unit/hooks/useSearchBulkActionsDownloadPDFTest.ts
+++ b/tests/unit/hooks/useSearchBulkActionsDownloadPDFTest.ts
@@ -464,7 +464,6 @@ describe('useSearchBulkActions - Download as PDF', () => {
expect(exportReportsToPDF).toHaveBeenCalledTimes(1);
expect(exportReportsToPDF).toHaveBeenCalledWith(expect.arrayContaining(['1', '2']));
expect(exportReportToPDF).not.toHaveBeenCalled();
- expect(result.current.exportDownloadStatusModal).not.toBeNull();
});
it('should show Export as PDF for selected Expensify Card settlement groups', async () => {
diff --git a/tests/unit/hooks/useSearchBulkActionsTest.ts b/tests/unit/hooks/useSearchBulkActionsTest.ts
index 4ea9bdd5099d..bd645da3d610 100644
--- a/tests/unit/hooks/useSearchBulkActionsTest.ts
+++ b/tests/unit/hooks/useSearchBulkActionsTest.ts
@@ -286,7 +286,6 @@ describe('useSearchBulkActions - CSV export flow', () => {
expect(mockQueueExportSearchItemsToCSV).toHaveBeenCalled();
expect(mockQueueExportSearchItemsToCSV).toHaveBeenCalledWith(expect.objectContaining({excludedTransactionIDList: ['tx2']}));
- expect(result.current.exportDownloadStatusModal).not.toBeNull();
});
it('exports an excluded unloaded group as a query filter instead of a transaction ID', async () => {
@@ -466,7 +465,6 @@ describe('useSearchBulkActions - CSV export flow', () => {
});
expect(mockQueueExportSearchItemsToCSV).not.toHaveBeenCalled();
- expect(result.current.exportDownloadStatusModal).toBeNull();
});
it('beginExportWithTemplate tracks the export', async () => {
@@ -492,7 +490,6 @@ describe('useSearchBulkActions - CSV export flow', () => {
});
expect(mockQueueExportSearchWithTemplate).toHaveBeenCalled();
- expect(result.current.exportDownloadStatusModal).not.toBeNull();
});
it('hides template exports when an all-matching expense selection has exclusions', async () => {