diff --git a/src/libs/actions/Policy/Category.ts b/src/libs/actions/Policy/Category.ts index f561dd9293cc..c342fe6eedb6 100644 --- a/src/libs/actions/Policy/Category.ts +++ b/src/libs/actions/Policy/Category.ts @@ -1041,7 +1041,7 @@ function setPolicyDistanceRatesDefaultCategory(policyID: string, currentCustomUn API.write(WRITE_COMMANDS.SET_POLICY_DISTANCE_RATES_DEFAULT_CATEGORY, params, {optimisticData, successData, failureData}); } -function downloadCategoriesCSV(policyID: string) { +function downloadCategoriesCSV(policyID: string, onDownloadFailed: () => void) { const finalParameters = enhanceParameters(WRITE_COMMANDS.EXPORT_CATEGORIES_CSV, { policyID, }); @@ -1053,7 +1053,7 @@ function downloadCategoriesCSV(policyID: string) { formData.append(key, String(value)); }); - fileDownload(ApiUtils.getCommandURL({command: WRITE_COMMANDS.EXPORT_CATEGORIES_CSV}), fileName, '', false, formData, CONST.NETWORK.METHOD.POST); + fileDownload(ApiUtils.getCommandURL({command: WRITE_COMMANDS.EXPORT_CATEGORIES_CSV}), fileName, '', false, formData, CONST.NETWORK.METHOD.POST, onDownloadFailed); } function setWorkspaceCategoryDescriptionHint(policyID: string, categoryName: string, commentHint: string) { diff --git a/src/libs/fileDownload/DownloadUtils.ts b/src/libs/fileDownload/DownloadUtils.ts index a09b0aa38c75..4f0ce6ab0c92 100644 --- a/src/libs/fileDownload/DownloadUtils.ts +++ b/src/libs/fileDownload/DownloadUtils.ts @@ -53,7 +53,13 @@ const fetchFileDownload: FileDownload = (url, fileName, successMessage = '', sho }; return fetch(url, fetchOptions) - .then((response) => response.blob()) + .then((response) => { + const contentType = response.headers.get('content-type'); + if (contentType === 'application/json' && fileName?.includes('.csv')) { + throw new Error(); + } + return response.blob(); + }) .then((blob) => { // Create blob link to download const href = URL.createObjectURL(new Blob([blob])); diff --git a/src/libs/fileDownload/index.android.ts b/src/libs/fileDownload/index.android.ts index a1e81e47994d..8426e20a33f7 100644 --- a/src/libs/fileDownload/index.android.ts +++ b/src/libs/fileDownload/index.android.ts @@ -107,6 +107,10 @@ const postDownloadFile = (url: string, fileName?: string, formData?: FormData, o if (!response.ok) { throw new Error('Failed to download file'); } + const contentType = response.headers.get('content-type'); + if (contentType === 'application/json' && fileName?.includes('.csv')) { + throw new Error(); + } return response.text(); }) .then((fileData) => { diff --git a/src/libs/fileDownload/index.desktop.ts b/src/libs/fileDownload/index.desktop.ts index de000f61b41b..6a601a4af249 100644 --- a/src/libs/fileDownload/index.desktop.ts +++ b/src/libs/fileDownload/index.desktop.ts @@ -7,10 +7,10 @@ import type {FileDownload} from './types'; /** * The function downloads an attachment on desktop platforms. */ -const fileDownload: FileDownload = (url, fileName, successMessage, shouldOpenExternalLink, formData, requestType) => { +const fileDownload: FileDownload = (url, fileName, successMessage, shouldOpenExternalLink, formData, requestType, onDownloadFailed?: () => void) => { if (requestType === CONST.NETWORK.METHOD.POST) { window.electron.send(ELECTRON_EVENTS.DOWNLOAD); - return fetchFileDownload(url, fileName, successMessage, shouldOpenExternalLink, formData, requestType); + return fetchFileDownload(url, fileName, successMessage, shouldOpenExternalLink, formData, requestType, onDownloadFailed); } const options: Options = { diff --git a/src/libs/fileDownload/index.ios.ts b/src/libs/fileDownload/index.ios.ts index 1fff9fb998e6..fb2e1c2c146a 100644 --- a/src/libs/fileDownload/index.ios.ts +++ b/src/libs/fileDownload/index.ios.ts @@ -38,6 +38,10 @@ const postDownloadFile = (url: string, fileName?: string, formData?: FormData, o if (!response.ok) { throw new Error('Failed to download file'); } + const contentType = response.headers.get('content-type'); + if (contentType === 'application/json' && fileName?.includes('.csv')) { + throw new Error(); + } return response.text(); }) .then((fileData) => { diff --git a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx index 35ff78adba00..340bd991c609 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx @@ -8,6 +8,7 @@ import Button from '@components/Button'; import ButtonWithDropdownMenu from '@components/ButtonWithDropdownMenu'; import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types'; import ConfirmModal from '@components/ConfirmModal'; +import DecisionModal from '@components/DecisionModal'; import EmptyStateComponent from '@components/EmptyStateComponent'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -55,13 +56,14 @@ type PolicyOption = ListItem & { type WorkspaceCategoriesPageProps = StackScreenProps; function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) { - const {shouldUseNarrowLayout} = useResponsiveLayout(); + const {shouldUseNarrowLayout, isSmallScreenWidth} = useResponsiveLayout(); const {windowWidth} = useWindowDimensions(); const styles = useThemeStyles(); const theme = useTheme(); const {translate} = useLocalize(); const [isOfflineModalVisible, setIsOfflineModalVisible] = useState(false); const [selectedCategories, setSelectedCategories] = useState>({}); + const [isDownloadFailureModalVisible, setIsDownloadFailureModalVisible] = useState(false); const [deleteCategoriesConfirmModalVisible, setDeleteCategoriesConfirmModalVisible] = useState(false); const isFocused = useIsFocused(); const {environmentURL} = useEnvironment(); @@ -311,7 +313,9 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) { Navigation.navigate(ROUTES.WORKSPACE_CATEGORIES_IMPORT.getRoute(policyId)); }, }, - { + ]; + if (hasVisibleCategories) { + menuItems.push({ icon: Expensicons.Download, text: translate('spreadsheet.downloadCSV'), onSelected: () => { @@ -319,13 +323,17 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) { Modal.close(() => setIsOfflineModalVisible(true)); return; } - Category.downloadCategoriesCSV(policyId); + Modal.close(() => { + Category.downloadCategoriesCSV(policyId, () => { + setIsDownloadFailureModalVisible(true); + }); + }); }, - }, - ]; + }); + } return menuItems; - }, [policyId, translate, isOffline]); + }, [policyId, translate, isOffline, hasVisibleCategories]); const selectionModeHeader = selectionMode?.isEnabled && shouldUseNarrowLayout; @@ -418,6 +426,15 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) { confirmText={translate('common.buttonConfirm')} shouldShowCancelButton={false} /> + setIsDownloadFailureModalVisible(false)} + secondOptionText={translate('common.buttonConfirm')} + isVisible={isDownloadFailureModalVisible} + onClose={() => setIsDownloadFailureModalVisible(false)} + /> );