From fb62a47172d6c44628728a43bd205400b07129f7 Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Wed, 17 Jul 2024 13:42:38 +0200 Subject: [PATCH 1/8] Add possibility to download file on iOS and Android devices --- src/libs/fileDownload/index.android.ts | 34 +++++++++++++++++++++++- src/libs/fileDownload/index.ios.ts | 36 +++++++++++++++++++++++++- 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/src/libs/fileDownload/index.android.ts b/src/libs/fileDownload/index.android.ts index abd116565922..6b3c22454793 100644 --- a/src/libs/fileDownload/index.android.ts +++ b/src/libs/fileDownload/index.android.ts @@ -1,6 +1,8 @@ import {PermissionsAndroid, Platform} from 'react-native'; import type {FetchBlobResponse} from 'react-native-blob-util'; import RNFetchBlob from 'react-native-blob-util'; +import RNFS from 'react-native-fs'; +import CONST from '@src/CONST'; import * as FileUtils from './FileUtils'; import type {FileDownload} from './types'; @@ -94,14 +96,44 @@ function handleDownload(url: string, fileName?: string, successMessage?: string) }); } +const postDownloadFile = (url: string, fileName?: string, formData?: FormData) => { + const fetchOptions: RequestInit = { + method: 'POST', + body: formData, + }; + + return fetch(url, fetchOptions) + .then((response) => { + if (!response.ok) { + throw new Error('Failed to download file'); + } + return response.text(); + }) + .then((fileData) => { + const finalFileName = FileUtils.appendTimeToFileName(fileName ?? 'Expensify'); + const downloadPath = `${RNFS.DownloadDirectoryPath}/Expensify/${finalFileName}`; + + return RNFS.writeFile(downloadPath, fileData, 'utf8').then(() => downloadPath); + }) + .then(() => { + FileUtils.showSuccessAlert(); + }) + .catch(() => { + FileUtils.showGeneralErrorAlert(); + }); +}; + /** * Checks permission and downloads the file for Android */ -const fileDownload: FileDownload = (url, fileName, successMessage) => +const fileDownload: FileDownload = (url, fileName, successMessage, _, formData, requestType) => new Promise((resolve) => { hasAndroidPermission() .then((hasPermission) => { if (hasPermission) { + if (requestType === CONST.NETWORK.METHOD.POST) { + return postDownloadFile(url, fileName, formData); + } return handleDownload(url, fileName, successMessage); } FileUtils.showPermissionErrorAlert(); diff --git a/src/libs/fileDownload/index.ios.ts b/src/libs/fileDownload/index.ios.ts index b1617bb440d0..61811073a83f 100644 --- a/src/libs/fileDownload/index.ios.ts +++ b/src/libs/fileDownload/index.ios.ts @@ -1,6 +1,7 @@ import {CameraRoll} from '@react-native-camera-roll/camera-roll'; import type {PhotoIdentifier} from '@react-native-camera-roll/camera-roll'; import RNFetchBlob from 'react-native-blob-util'; +import RNFS from 'react-native-fs'; import CONST from '@src/CONST'; import * as FileUtils from './FileUtils'; import type {FileDownload} from './types'; @@ -26,6 +27,34 @@ function downloadFile(fileUrl: string, fileName: string) { }).fetch('GET', fileUrl); } +const postDownloadFile = (url: string, fileName?: string, formData?: FormData) => { + const fetchOptions: RequestInit = { + method: 'POST', + body: formData, + }; + + return fetch(url, fetchOptions) + .then((response) => { + if (!response.ok) { + throw new Error('Failed to download file'); + } + return response.text(); + }) + .then((fileData) => { + const finalFileName = FileUtils.appendTimeToFileName(fileName ?? 'Expensify'); + // TODO - check why file path can't include Expensify folder name + const localPath = `${RNFS.DocumentDirectoryPath}/${finalFileName}`; + + return RNFS.writeFile(localPath, fileData, 'utf8').then(() => localPath); + }) + .then(() => { + FileUtils.showSuccessAlert(); + }) + .catch(() => { + FileUtils.showGeneralErrorAlert(); + }); +}; + /** * Download the image to photo lib in iOS */ @@ -67,7 +96,7 @@ function downloadVideo(fileUrl: string, fileName: string): Promise +const fileDownload: FileDownload = (fileUrl, fileName, successMessage, _, formData, requestType) => new Promise((resolve) => { let fileDownloadPromise; const fileType = FileUtils.getFileType(fileUrl); @@ -82,6 +111,11 @@ const fileDownload: FileDownload = (fileUrl, fileName, successMessage) => fileDownloadPromise = downloadVideo(fileUrl, attachmentName); break; default: + if (requestType === CONST.NETWORK.METHOD.POST) { + fileDownloadPromise = postDownloadFile(fileUrl, fileName, formData); + break; + } + fileDownloadPromise = downloadFile(fileUrl, attachmentName); break; } From b073f0e7c330d14645744f95caf725422b5acc5e Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Thu, 18 Jul 2024 13:55:42 +0200 Subject: [PATCH 2/8] Add desktop POST file download and fix iOS download path --- .../SearchActionOptionsUtils.desktop.tsx | 8 --- .../SearchActionOptionsUtils.native.tsx | 8 --- src/libs/fileDownload/DownloadUtils.ts | 67 +++++++++++++++++++ src/libs/fileDownload/index.desktop.ts | 8 ++- src/libs/fileDownload/index.ios.ts | 8 ++- src/libs/fileDownload/index.ts | 63 +---------------- 6 files changed, 82 insertions(+), 80 deletions(-) delete mode 100644 src/components/Search/SearchActionOptionsUtils.desktop.tsx delete mode 100644 src/components/Search/SearchActionOptionsUtils.native.tsx create mode 100644 src/libs/fileDownload/DownloadUtils.ts diff --git a/src/components/Search/SearchActionOptionsUtils.desktop.tsx b/src/components/Search/SearchActionOptionsUtils.desktop.tsx deleted file mode 100644 index 1e59543721e0..000000000000 --- a/src/components/Search/SearchActionOptionsUtils.desktop.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types'; -import type {SearchHeaderOptionValue} from './SearchPageHeader'; - -function getDownloadOption(): DropdownOption | undefined { - return undefined; -} - -export default getDownloadOption; diff --git a/src/components/Search/SearchActionOptionsUtils.native.tsx b/src/components/Search/SearchActionOptionsUtils.native.tsx deleted file mode 100644 index 1e59543721e0..000000000000 --- a/src/components/Search/SearchActionOptionsUtils.native.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types'; -import type {SearchHeaderOptionValue} from './SearchPageHeader'; - -function getDownloadOption(): DropdownOption | undefined { - return undefined; -} - -export default getDownloadOption; diff --git a/src/libs/fileDownload/DownloadUtils.ts b/src/libs/fileDownload/DownloadUtils.ts new file mode 100644 index 000000000000..5bbe112659b4 --- /dev/null +++ b/src/libs/fileDownload/DownloadUtils.ts @@ -0,0 +1,67 @@ +import * as ApiUtils from '@libs/ApiUtils'; +import tryResolveUrlFromApiRoot from '@libs/tryResolveUrlFromApiRoot'; +import * as Link from '@userActions/Link'; +import CONST from '@src/CONST'; +import * as FileUtils from './FileUtils'; +import type {FileDownload} from './types'; + +/** + * The function downloads an attachment on web/desktop platforms. + */ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const fetchFileDownload: FileDownload = (url, fileName, successMessage = '', shouldOpenExternalLink = false, formData = undefined, requestType = 'get', onDownloadFailed?: () => void) => { + const resolvedUrl = tryResolveUrlFromApiRoot(url); + if ( + // we have two file download cases that we should allow 1. dowloading attachments 2. downloading Expensify package for Sage Intacct + shouldOpenExternalLink || + (!resolvedUrl.startsWith(ApiUtils.getApiRoot()) && + !CONST.ATTACHMENT_LOCAL_URL_PREFIX.some((prefix) => resolvedUrl.startsWith(prefix)) && + url !== CONST.EXPENSIFY_PACKAGE_FOR_SAGE_INTACCT) + ) { + // Different origin URLs might pose a CORS issue during direct downloads. + // Opening in a new tab avoids this limitation, letting the browser handle the download. + Link.openExternalLink(url); + return Promise.resolve(); + } + + const fetchOptions: RequestInit = { + method: requestType, + body: formData, + }; + + return fetch(url, fetchOptions) + .then((response) => response.blob()) + .then((blob) => { + // Create blob link to download + const href = URL.createObjectURL(new Blob([blob])); + + // creating anchor tag to initiate download + const link = document.createElement('a'); + + // adding href to anchor + link.href = href; + link.style.display = 'none'; + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- Disabling this line for safeness as nullish coalescing works only if the value is undefined or null, and since fileName can be an empty string we want to default to `FileUtils.getFileName(url)` + link.download = FileUtils.appendTimeToFileName(fileName || FileUtils.getFileName(url)); + + // Append to html link element page + document.body.appendChild(link); + + // Start download + link.click(); + + // Clean up and remove the link + URL.revokeObjectURL(link.href); + link.parentNode?.removeChild(link); + }) + .catch(() => { + if (onDownloadFailed) { + onDownloadFailed(); + } else { + // file could not be downloaded, open sourceURL in new tab + Link.openExternalLink(url); + } + }); +}; + +export default fetchFileDownload; diff --git a/src/libs/fileDownload/index.desktop.ts b/src/libs/fileDownload/index.desktop.ts index 8e682225b79a..c37868b89dea 100644 --- a/src/libs/fileDownload/index.desktop.ts +++ b/src/libs/fileDownload/index.desktop.ts @@ -1,12 +1,18 @@ import ELECTRON_EVENTS from '@desktop/ELECTRON_EVENTS'; import type Options from '@desktop/electronDownloadManagerType'; import CONST from '@src/CONST'; +import fetchFileDownload from './DownloadUtils'; import type {FileDownload} from './types'; /** * The function downloads an attachment on desktop platforms. */ -const fileDownload: FileDownload = (url, fileName) => { +const fileDownload: FileDownload = (url, fileName, successMessage, shouldOpenExternalLink, formData, requestType) => { + if (requestType === 'post') { + window.electron.send(ELECTRON_EVENTS.DOWNLOAD); + return fetchFileDownload(url, fileName, successMessage, shouldOpenExternalLink, formData, requestType); + } + const options: Options = { filename: fileName, saveAs: true, diff --git a/src/libs/fileDownload/index.ios.ts b/src/libs/fileDownload/index.ios.ts index 61811073a83f..06f461fa1360 100644 --- a/src/libs/fileDownload/index.ios.ts +++ b/src/libs/fileDownload/index.ios.ts @@ -42,10 +42,12 @@ const postDownloadFile = (url: string, fileName?: string, formData?: FormData) = }) .then((fileData) => { const finalFileName = FileUtils.appendTimeToFileName(fileName ?? 'Expensify'); - // TODO - check why file path can't include Expensify folder name - const localPath = `${RNFS.DocumentDirectoryPath}/${finalFileName}`; + const expensifyDir = `${RNFS.DocumentDirectoryPath}/Expensify`; - return RNFS.writeFile(localPath, fileData, 'utf8').then(() => localPath); + return RNFS.mkdir(expensifyDir).then(() => { + const localPath = `${expensifyDir}/${finalFileName}`; + return RNFS.writeFile(localPath, fileData, 'utf8').then(() => localPath); + }); }) .then(() => { FileUtils.showSuccessAlert(); diff --git a/src/libs/fileDownload/index.ts b/src/libs/fileDownload/index.ts index 133a18e146a5..fc1ea6f74d9b 100644 --- a/src/libs/fileDownload/index.ts +++ b/src/libs/fileDownload/index.ts @@ -1,67 +1,10 @@ -import * as ApiUtils from '@libs/ApiUtils'; -import tryResolveUrlFromApiRoot from '@libs/tryResolveUrlFromApiRoot'; -import * as Link from '@userActions/Link'; -import CONST from '@src/CONST'; -import * as FileUtils from './FileUtils'; +import fetchFileDownload from './DownloadUtils'; import type {FileDownload} from './types'; /** * The function downloads an attachment on web/desktop platforms. */ -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const fileDownload: FileDownload = (url, fileName, successMessage = '', shouldOpenExternalLink = false, formData = undefined, requestType = 'get', onDownloadFailed?: () => void) => { - const resolvedUrl = tryResolveUrlFromApiRoot(url); - if ( - // we have two file download cases that we should allow 1. dowloading attachments 2. downloading Expensify package for Sage Intacct - shouldOpenExternalLink || - (!resolvedUrl.startsWith(ApiUtils.getApiRoot()) && - !CONST.ATTACHMENT_LOCAL_URL_PREFIX.some((prefix) => resolvedUrl.startsWith(prefix)) && - url !== CONST.EXPENSIFY_PACKAGE_FOR_SAGE_INTACCT) - ) { - // Different origin URLs might pose a CORS issue during direct downloads. - // Opening in a new tab avoids this limitation, letting the browser handle the download. - Link.openExternalLink(url); - return Promise.resolve(); - } - - const fetchOptions: RequestInit = { - method: requestType, - body: formData, - }; - - return fetch(url, fetchOptions) - .then((response) => response.blob()) - .then((blob) => { - // Create blob link to download - const href = URL.createObjectURL(new Blob([blob])); - - // creating anchor tag to initiate download - const link = document.createElement('a'); - - // adding href to anchor - link.href = href; - link.style.display = 'none'; - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- Disabling this line for safeness as nullish coalescing works only if the value is undefined or null, and since fileName can be an empty string we want to default to `FileUtils.getFileName(url)` - link.download = FileUtils.appendTimeToFileName(fileName || FileUtils.getFileName(url)); - - // Append to html link element page - document.body.appendChild(link); - - // Start download - link.click(); - - // Clean up and remove the link - URL.revokeObjectURL(link.href); - link.parentNode?.removeChild(link); - }) - .catch(() => { - if (onDownloadFailed) { - onDownloadFailed(); - } else { - // file could not be downloaded, open sourceURL in new tab - Link.openExternalLink(url); - } - }); -}; +const fileDownload: FileDownload = (url, fileName, successMessage = '', shouldOpenExternalLink = false, formData = undefined, requestType = 'get', onDownloadFailed?: () => void) => + fetchFileDownload(url, fileName, successMessage, shouldOpenExternalLink, formData, requestType, onDownloadFailed); export default fileDownload; From f92d91eb2b4f79fc2a0c2432c847d8a2ac5f15ed Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Thu, 18 Jul 2024 15:08:25 +0200 Subject: [PATCH 3/8] Use custom onDownloadFailed function --- src/libs/fileDownload/index.android.ts | 11 +++++++---- src/libs/fileDownload/index.ios.ts | 11 +++++++---- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/libs/fileDownload/index.android.ts b/src/libs/fileDownload/index.android.ts index 6b3c22454793..adfb75bc28c4 100644 --- a/src/libs/fileDownload/index.android.ts +++ b/src/libs/fileDownload/index.android.ts @@ -96,7 +96,7 @@ function handleDownload(url: string, fileName?: string, successMessage?: string) }); } -const postDownloadFile = (url: string, fileName?: string, formData?: FormData) => { +const postDownloadFile = (url: string, fileName?: string, formData?: FormData, onDownloadFailed?: () => void) => { const fetchOptions: RequestInit = { method: 'POST', body: formData, @@ -119,20 +119,23 @@ const postDownloadFile = (url: string, fileName?: string, formData?: FormData) = FileUtils.showSuccessAlert(); }) .catch(() => { - FileUtils.showGeneralErrorAlert(); + if (!onDownloadFailed) { + FileUtils.showGeneralErrorAlert(); + } + onDownloadFailed?.(); }); }; /** * Checks permission and downloads the file for Android */ -const fileDownload: FileDownload = (url, fileName, successMessage, _, formData, requestType) => +const fileDownload: FileDownload = (url, fileName, successMessage, _, formData, requestType, onDownloadFailed) => new Promise((resolve) => { hasAndroidPermission() .then((hasPermission) => { if (hasPermission) { if (requestType === CONST.NETWORK.METHOD.POST) { - return postDownloadFile(url, fileName, formData); + return postDownloadFile(url, fileName, formData, onDownloadFailed); } return handleDownload(url, fileName, successMessage); } diff --git a/src/libs/fileDownload/index.ios.ts b/src/libs/fileDownload/index.ios.ts index 06f461fa1360..1fff9fb998e6 100644 --- a/src/libs/fileDownload/index.ios.ts +++ b/src/libs/fileDownload/index.ios.ts @@ -27,7 +27,7 @@ function downloadFile(fileUrl: string, fileName: string) { }).fetch('GET', fileUrl); } -const postDownloadFile = (url: string, fileName?: string, formData?: FormData) => { +const postDownloadFile = (url: string, fileName?: string, formData?: FormData, onDownloadFailed?: () => void) => { const fetchOptions: RequestInit = { method: 'POST', body: formData, @@ -53,7 +53,10 @@ const postDownloadFile = (url: string, fileName?: string, formData?: FormData) = FileUtils.showSuccessAlert(); }) .catch(() => { - FileUtils.showGeneralErrorAlert(); + if (!onDownloadFailed) { + FileUtils.showGeneralErrorAlert(); + } + onDownloadFailed?.(); }); }; @@ -98,7 +101,7 @@ function downloadVideo(fileUrl: string, fileName: string): Promise +const fileDownload: FileDownload = (fileUrl, fileName, successMessage, _, formData, requestType, onDownloadFailed) => new Promise((resolve) => { let fileDownloadPromise; const fileType = FileUtils.getFileType(fileUrl); @@ -114,7 +117,7 @@ const fileDownload: FileDownload = (fileUrl, fileName, successMessage, _, formDa break; default: if (requestType === CONST.NETWORK.METHOD.POST) { - fileDownloadPromise = postDownloadFile(fileUrl, fileName, formData); + fileDownloadPromise = postDownloadFile(fileUrl, fileName, formData, onDownloadFailed); break; } From 646bd1bc2de52e466701a8314fd21ffe3b58692d Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Fri, 19 Jul 2024 07:34:58 +0200 Subject: [PATCH 4/8] Use requestType from CONST --- src/libs/fileDownload/index.desktop.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/fileDownload/index.desktop.ts b/src/libs/fileDownload/index.desktop.ts index c37868b89dea..de000f61b41b 100644 --- a/src/libs/fileDownload/index.desktop.ts +++ b/src/libs/fileDownload/index.desktop.ts @@ -8,7 +8,7 @@ import type {FileDownload} from './types'; * The function downloads an attachment on desktop platforms. */ const fileDownload: FileDownload = (url, fileName, successMessage, shouldOpenExternalLink, formData, requestType) => { - if (requestType === 'post') { + if (requestType === CONST.NETWORK.METHOD.POST) { window.electron.send(ELECTRON_EVENTS.DOWNLOAD); return fetchFileDownload(url, fileName, successMessage, shouldOpenExternalLink, formData, requestType); } From 13ee7a8bcffe12f31ed8824df90023dd857009b5 Mon Sep 17 00:00:00 2001 From: Filip <153545991+filip-solecki@users.noreply.github.com> Date: Fri, 19 Jul 2024 11:23:10 +0200 Subject: [PATCH 5/8] Fix comment Co-authored-by: Mateusz Titz --- src/libs/fileDownload/DownloadUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/fileDownload/DownloadUtils.ts b/src/libs/fileDownload/DownloadUtils.ts index 5bbe112659b4..05442c437458 100644 --- a/src/libs/fileDownload/DownloadUtils.ts +++ b/src/libs/fileDownload/DownloadUtils.ts @@ -12,7 +12,7 @@ import type {FileDownload} from './types'; const fetchFileDownload: FileDownload = (url, fileName, successMessage = '', shouldOpenExternalLink = false, formData = undefined, requestType = 'get', onDownloadFailed?: () => void) => { const resolvedUrl = tryResolveUrlFromApiRoot(url); if ( - // we have two file download cases that we should allow 1. dowloading attachments 2. downloading Expensify package for Sage Intacct + // We have two file download cases that we should allow: 1. downloading attachments 2. downloading Expensify package for Sage Intacct shouldOpenExternalLink || (!resolvedUrl.startsWith(ApiUtils.getApiRoot()) && !CONST.ATTACHMENT_LOCAL_URL_PREFIX.some((prefix) => resolvedUrl.startsWith(prefix)) && From 77efcef671ac34bcaca9088f2d6b17ebbb4850bf Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Fri, 19 Jul 2024 12:04:04 +0200 Subject: [PATCH 6/8] CR fixes --- src/libs/fileDownload/DownloadUtils.ts | 50 ++++++++++++++------------ 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/src/libs/fileDownload/DownloadUtils.ts b/src/libs/fileDownload/DownloadUtils.ts index 05442c437458..a09b0aa38c75 100644 --- a/src/libs/fileDownload/DownloadUtils.ts +++ b/src/libs/fileDownload/DownloadUtils.ts @@ -5,18 +5,41 @@ import CONST from '@src/CONST'; import * as FileUtils from './FileUtils'; import type {FileDownload} from './types'; +const createDownloadLink = (href: string, fileName: string) => { + // creating anchor tag to initiate download + const link = document.createElement('a'); + // adding href to anchor + link.href = href; + link.style.display = 'none'; + + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- Disabling this line for safeness as nullish coalescing works only if the value is undefined or null, and since fileName can be an empty string we want to default to `FileUtils.getFileName(url)` + link.download = fileName; + + // Append to html link element page + document.body.appendChild(link); + + // Start download + link.click(); + + // Clean up and remove the link + URL.revokeObjectURL(link.href); + link.parentNode?.removeChild(link); +}; + /** * The function downloads an attachment on web/desktop platforms. */ // eslint-disable-next-line @typescript-eslint/no-unused-vars const fetchFileDownload: FileDownload = (url, fileName, successMessage = '', shouldOpenExternalLink = false, formData = undefined, requestType = 'get', onDownloadFailed?: () => void) => { const resolvedUrl = tryResolveUrlFromApiRoot(url); + + const isApiUrl = resolvedUrl.startsWith(ApiUtils.getApiRoot()); + const isAttachmentUrl = CONST.ATTACHMENT_LOCAL_URL_PREFIX.some((prefix) => resolvedUrl.startsWith(prefix)); + const isSageUrl = url === CONST.EXPENSIFY_PACKAGE_FOR_SAGE_INTACCT; if ( // We have two file download cases that we should allow: 1. downloading attachments 2. downloading Expensify package for Sage Intacct shouldOpenExternalLink || - (!resolvedUrl.startsWith(ApiUtils.getApiRoot()) && - !CONST.ATTACHMENT_LOCAL_URL_PREFIX.some((prefix) => resolvedUrl.startsWith(prefix)) && - url !== CONST.EXPENSIFY_PACKAGE_FOR_SAGE_INTACCT) + (!isApiUrl && !isAttachmentUrl && !isSageUrl) ) { // Different origin URLs might pose a CORS issue during direct downloads. // Opening in a new tab avoids this limitation, letting the browser handle the download. @@ -34,25 +57,8 @@ const fetchFileDownload: FileDownload = (url, fileName, successMessage = '', sho .then((blob) => { // Create blob link to download const href = URL.createObjectURL(new Blob([blob])); - - // creating anchor tag to initiate download - const link = document.createElement('a'); - - // adding href to anchor - link.href = href; - link.style.display = 'none'; - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- Disabling this line for safeness as nullish coalescing works only if the value is undefined or null, and since fileName can be an empty string we want to default to `FileUtils.getFileName(url)` - link.download = FileUtils.appendTimeToFileName(fileName || FileUtils.getFileName(url)); - - // Append to html link element page - document.body.appendChild(link); - - // Start download - link.click(); - - // Clean up and remove the link - URL.revokeObjectURL(link.href); - link.parentNode?.removeChild(link); + const completeFileName = FileUtils.appendTimeToFileName(fileName ?? FileUtils.getFileName(url)); + createDownloadLink(href, completeFileName); }) .catch(() => { if (onDownloadFailed) { From fdb5105eeb648eda5d53af6d2e7afbbbb788a5b5 Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Fri, 19 Jul 2024 12:56:02 +0200 Subject: [PATCH 7/8] Fix small screen Badge border color --- src/components/SelectionList/Search/ExpenseItemHeaderNarrow.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/SelectionList/Search/ExpenseItemHeaderNarrow.tsx b/src/components/SelectionList/Search/ExpenseItemHeaderNarrow.tsx index 60f278998f0d..e4702734fcd0 100644 --- a/src/components/SelectionList/Search/ExpenseItemHeaderNarrow.tsx +++ b/src/components/SelectionList/Search/ExpenseItemHeaderNarrow.tsx @@ -92,6 +92,7 @@ function ExpenseItemHeaderNarrow({ action={action} goToItem={onButtonPress} isLargeScreenWidth={false} + isSelected={isSelected} /> From 093398c16215d77edb7c89e1960e90d6fd431592 Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Fri, 19 Jul 2024 16:25:15 +0200 Subject: [PATCH 8/8] Fix Android downloading location --- src/libs/fileDownload/index.android.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/libs/fileDownload/index.android.ts b/src/libs/fileDownload/index.android.ts index adfb75bc28c4..83255231d26b 100644 --- a/src/libs/fileDownload/index.android.ts +++ b/src/libs/fileDownload/index.android.ts @@ -96,7 +96,7 @@ function handleDownload(url: string, fileName?: string, successMessage?: string) }); } -const postDownloadFile = (url: string, fileName?: string, formData?: FormData, onDownloadFailed?: () => void) => { +const postDownloadFile = (url: string, fileName?: string, formData?: FormData, onDownloadFailed?: () => void): Promise => { const fetchOptions: RequestInit = { method: 'POST', body: formData, @@ -115,7 +115,19 @@ const postDownloadFile = (url: string, fileName?: string, formData?: FormData, o return RNFS.writeFile(downloadPath, fileData, 'utf8').then(() => downloadPath); }) - .then(() => { + .then((downloadPath) => + RNFetchBlob.MediaCollection.copyToMediaStore( + { + name: FileUtils.getFileName(downloadPath), + parentFolder: 'Expensify', + mimeType: null, + }, + 'Download', + downloadPath, + ).then(() => downloadPath), + ) + .then((downloadPath) => { + RNFetchBlob.fs.unlink(downloadPath); FileUtils.showSuccessAlert(); }) .catch(() => {