Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
f1fd1ab
feat: Wire ExportDownloadStatusModal into CSV export flow
truph01 Jun 1, 2026
1b0763e
fix: add failure data
truph01 Jun 1, 2026
43cf551
fix: lint
truph01 Jun 1, 2026
01b3131
fix: update fileDownload
truph01 Jun 1, 2026
68737e0
fix: conflicts
truph01 Jun 3, 2026
2b13d84
fix: conflicts
truph01 Jun 3, 2026
5b63de7
fix: missing exportID
truph01 Jun 3, 2026
20d4f61
fix: lint
truph01 Jun 3, 2026
1dedaf2
fix: tests
truph01 Jun 3, 2026
28a401d
Merge branch 'Expensify:main' into fix/90298
truph01 Jun 4, 2026
585b3be
fix: lint
truph01 Jun 4, 2026
8b46b5c
Merge branch 'Expensify:main' into fix/90298
truph01 Jun 4, 2026
28b87e9
Merge branch 'Expensify:main' into fix/90298
truph01 Jun 8, 2026
e4f23b2
fix: conflicts
truph01 Jun 9, 2026
7b04b39
fix: conflicts
truph01 Jun 9, 2026
2b2ad75
fix: update download logic
truph01 Jun 9, 2026
b697c3d
fix: test
truph01 Jun 9, 2026
293d556
fix: remove line
truph01 Jun 9, 2026
8f0e62e
fix: spell check
truph01 Jun 9, 2026
b93b44b
Merge branch 'Expensify:main' into fix/90298
truph01 Jun 9, 2026
8bca27f
fix: prettier
truph01 Jun 9, 2026
68506e5
fix: open external link in safari
truph01 Jun 9, 2026
e21f674
fix: test
truph01 Jun 9, 2026
0e9f019
fix: conflicts
truph01 Jun 11, 2026
b597f27
fix: conflicts
truph01 Jun 11, 2026
3143396
fix: lint
truph01 Jun 11, 2026
e80fe22
fix: introduce shouldTrackExportProgress
truph01 Jun 11, 2026
ac91450
fix: don't clear selected transactions in export
truph01 Jun 11, 2026
41975d5
fix: prettier
truph01 Jun 11, 2026
a4e38ba
Merge branch 'Expensify:main' into fix/90298
truph01 Jun 12, 2026
dc924c1
fix: lint
truph01 Jun 12, 2026
ce1883d
fix: remove redundant code
truph01 Jun 12, 2026
14496aa
Merge branch 'Expensify:main' into fix/90298
truph01 Jun 15, 2026
35ca7f0
fix: conflicts
truph01 Jun 16, 2026
9806705
fix: conflicts
truph01 Jun 16, 2026
52c17b0
fix: typecheck
truph01 Jun 16, 2026
5eb8252
fix: resolve comment
truph01 Jun 16, 2026
46aa29a
fix: test
truph01 Jun 16, 2026
8894dff
fix: turn off selection mode after closing modal
truph01 Jun 16, 2026
d06dd6b
fix: resolve comment
truph01 Jun 16, 2026
2fbb6ff
fix: prettier
truph01 Jun 16, 2026
d0da579
Merge branch 'Expensify:main' into fix/90298
truph01 Jun 17, 2026
47608dd
fix: update handleGoToConcierge
truph01 Jun 17, 2026
c02e8bd
fix: test
truph01 Jun 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,7 @@
"creditamount",
"creditcards",
"crios",
"csvexport",
"csvg",
"customairshipextender",
"customfield",
Expand Down
80 changes: 47 additions & 33 deletions src/components/ExportDownloadStatusModal.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
import {hasSeenTourSelector} from '@selectors/Onboarding';
import React, {useEffect} from 'react';
import {View} from 'react-native';
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useEnvironment from '@hooks/useEnvironment';
import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
import usePreviousDefined from '@hooks/usePreviousDefined';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useThemeStyles from '@hooks/useThemeStyles';
import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL';
import {isMobileSafari} from '@libs/Browser';
import {getOldDotURLFromEnvironment} from '@libs/Environment/Environment';
import fileDownload from '@libs/fileDownload';
import Navigation from '@libs/Navigation/Navigation';
import addTrailingForwardSlash from '@libs/UrlUtils';
import {clearExportDownload, sendExportFileFromConcierge} from '@userActions/Export';
import {navigateToConciergeChat} from '@userActions/Report';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import ActivityIndicator from './ActivityIndicator';
import Button from './Button';
import Modal from './Modal';
Expand All @@ -37,45 +42,54 @@ function ExportDownloadStatusModal({exportID, isVisible, onClose, failedBody}: E
// isSmallScreenWidth is needed here because the modal type depends on actual screen width, not layout mode
// eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
const {isSmallScreenWidth} = useResponsiveLayout();
const {accountID: currentUserAccountID} = useCurrentUserPersonalDetails();
const {login: currentUserLogin} = useCurrentUserPersonalDetails();
const {environment} = useEnvironment();

const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID);
const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED);
const [betas] = useOnyx(ONYXKEYS.BETAS);
const [isSelfTourViewed] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: hasSeenTourSelector});
const [encryptedAuthToken] = useOnyx(ONYXKEYS.SESSION, {selector: (session) => session?.encryptedAuthToken});

const [exportDownload] = useOnyx(`${ONYXKEYS.COLLECTION.EXPORT_DOWNLOAD}${exportID}`);
const displayedExport = usePreviousDefined(exportDownload);

const state = displayedExport?.state;
const shouldSendFromConcierge = displayedExport?.shouldSendFromConcierge;
const downloadURL = displayedExport?.downloadURL;
const fileName = displayedExport?.fileName;
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;

// Build the secure download URL the same way downloadReportPDF does, so the host always follows
// the app's current environment (instead of the env baked into a backend-built URL) and authenticates
// via the encryptedAuthToken — no separate OldDot sign-in needed.
const downloadFile = () => {
if (!fileName || !currentUserLogin) {
return;
}
const baseURL = addTrailingForwardSlash(getOldDotURLFromEnvironment(environment));
const isCSV = fileName.endsWith('.csv');
const secureType = isCSV ? 'csvexport' : 'pdfreport';
const url = `${baseURL}secure?secureType=${secureType}&filename=${encodeURIComponent(fileName)}&downloadName=${encodeURIComponent(fileName)}&email=${encodeURIComponent(currentUserLogin)}`;
fileDownload(translate, addEncryptedAuthTokenToURL(url, encryptedAuthToken ?? '', true), fileName, '', isMobileSafari());
};

useEffect(() => {
if (!isReady || !downloadURL || shouldSendFromConcierge) {
if (!isReady || !fileName || shouldSendFromConcierge) {
return;
}
fileDownload(translate, downloadURL);
}, [isReady, downloadURL, translate, shouldSendFromConcierge]);
downloadFile();
// eslint-disable-next-line react-hooks/exhaustive-deps

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@truph01, why do we need this eslint-disable line?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't want to add downloadFile to the dependencies and without the eslint-disabled, it will throws error:

83:8 error React Hook useEffect has a missing dependency: 'downloadFile'. Either include it or remove the dependency array react-hooks/exhaustive-deps

}, [isReady, fileName, shouldSendFromConcierge]);

const handleSendFromConcierge = () => {
sendExportFileFromConcierge(exportID, displayedExport ?? undefined);
};

const handleGoToConcierge = () => {
onClose();
navigateToConciergeChat(conciergeReportID, introSelected, currentUserAccountID, isSelfTourViewed, betas);
};

const handleDownloadFile = () => {
if (!downloadURL) {
return;
if (conciergeReportID) {
Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute({reportID: conciergeReportID}));
}
fileDownload(translate, downloadURL);
};

const handleClose = () => {
Expand All @@ -89,15 +103,15 @@ function ExportDownloadStatusModal({exportID, isVisible, onClose, failedBody}: E
if (isPreparing) {
return (
<>
<ActivityIndicator
size="large"
style={styles.mb4}
reasonAttributes={{context: 'ExportDownloadStatusModal.preparing'}}
/>
<Text style={[styles.textHeadlineH1, styles.textAlignCenter, styles.mb2]}>{translate('exportDownload.preparingTitle')}</Text>
<Text style={[styles.textAlignCenter, styles.mb5]}>{translate('exportDownload.preparingBody')}</Text>
<View style={[styles.flexRow, styles.justifyContentBetween, styles.alignItemsCenter, styles.mb2]}>
<Text style={[styles.textHeadlineH1, styles.flexShrink1]}>{translate('exportDownload.preparingTitle')}</Text>
<ActivityIndicator
size="small"
reasonAttributes={{context: 'ExportDownloadStatusModal.preparing'}}
/>
</View>
<Text style={styles.mb5}>{translate('exportDownload.preparingBody')}</Text>
<Button
success
text={translate('exportDownload.sendFromConcierge')}
onPress={handleSendFromConcierge}
style={styles.w100}
Expand All @@ -109,8 +123,8 @@ function ExportDownloadStatusModal({exportID, isVisible, onClose, failedBody}: E
if (isConcierge) {
return (
<>
<Text style={[styles.textHeadlineH1, styles.textAlignCenter, styles.mb2]}>{translate('exportDownload.conciergeTitle')}</Text>
<Text style={[styles.textAlignCenter, styles.mb5]}>{translate('exportDownload.conciergeBody')}</Text>
<Text style={[styles.textHeadlineH1, styles.mb2]}>{translate('exportDownload.conciergeTitle')}</Text>
<Text style={styles.mb5}>{translate('exportDownload.conciergeBody')}</Text>
<Button
success
text={translate('exportDownload.goToConcierge')}
Expand All @@ -129,12 +143,12 @@ function ExportDownloadStatusModal({exportID, isVisible, onClose, failedBody}: E
if (isReady) {
return (
<>
<Text style={[styles.textHeadlineH1, styles.textAlignCenter, styles.mb2]}>{translate('exportDownload.readyTitle')}</Text>
<Text style={[styles.textAlignCenter, styles.mb5]}>{translate('exportDownload.readyBody')}</Text>
<Text style={[styles.textHeadlineH1, styles.mb2]}>{translate('exportDownload.readyTitle')}</Text>
<Text style={styles.mb5}>{translate('exportDownload.readyBody')}</Text>
<Button
success
text={translate('exportDownload.downloadFile')}
onPress={handleDownloadFile}
onPress={downloadFile}
style={styles.w100}
/>
<Button
Expand All @@ -149,8 +163,8 @@ function ExportDownloadStatusModal({exportID, isVisible, onClose, failedBody}: E
if (isFailed) {
return (
<>
<Text style={[styles.textHeadlineH1, styles.textAlignCenter, styles.mb2]}>{translate('exportDownload.failedTitle')}</Text>
{!!failedBody && <Text style={[styles.textAlignCenter, styles.mb5]}>{failedBody}</Text>}
<Text style={[styles.textHeadlineH1, styles.mb2]}>{translate('exportDownload.failedTitle')}</Text>
{!!failedBody && <Text style={styles.mb5}>{failedBody}</Text>}
<Button
text={translate('exportDownload.close')}
onPress={handleClose}
Expand All @@ -171,7 +185,7 @@ function ExportDownloadStatusModal({exportID, isVisible, onClose, failedBody}: E
type={isSmallScreenWidth ? CONST.MODAL.MODAL_TYPE.BOTTOM_DOCKED : CONST.MODAL.MODAL_TYPE.CONFIRM}
innerContainerStyle={styles.pv0}
>
<View style={[styles.m5, styles.alignItemsCenter]}>{renderContent()}</View>
<View style={styles.m5}>{renderContent()}</View>
</Modal>
);
}
Expand Down
11 changes: 11 additions & 0 deletions src/components/Search/SearchBulkActionsButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {View} from 'react-native';
import ButtonWithDropdownMenu from '@components/ButtonWithDropdownMenu';
import DecisionModal from '@components/DecisionModal';
import {useDelegateNoAccessActions, useDelegateNoAccessState} from '@components/DelegateNoAccessModalProvider';
import ExportDownloadStatusModal from '@components/ExportDownloadStatusModal';
import HoldOrRejectEducationalModal from '@components/HoldOrRejectEducationalModal';
import HoldSubmitterEducationalModal from '@components/HoldSubmitterEducationalModal';
import KYCWall from '@components/KYCWall';
Expand Down Expand Up @@ -80,6 +81,8 @@ function SearchBulkActionsButton({queryJSON}: SearchBulkActionsButtonProps) {
allTransactions,
allReports,
searchData,
activeExportID,
handleExportModalClose,
} = useSearchBulkActions({queryJSON});
const currentSelectedPolicyID = selectedPolicyIDs?.at(0);
const currentSelectedReportID = selectedTransactionReportIDs?.at(0) ?? selectedReportIDs?.at(0);
Expand Down Expand Up @@ -273,6 +276,14 @@ function SearchBulkActionsButton({queryJSON}: SearchBulkActionsButtonProps) {
onConfirm={dismissModalAndUpdateUseHold}
/>
)}
{!!activeExportID && (
<ExportDownloadStatusModal
exportID={activeExportID}
isVisible
onClose={handleExportModalClose}
failedBody={translate('exportDownload.csvFailedBody')}
/>
)}
</>
);
}
Expand Down
Loading
Loading