diff --git a/src/libs/actions/PersonalDetails.ts b/src/libs/actions/PersonalDetails.ts index 12da798177ab..e26cee71dc67 100644 --- a/src/libs/actions/PersonalDetails.ts +++ b/src/libs/actions/PersonalDetails.ts @@ -1,7 +1,7 @@ import Str from 'expensify-common/lib/str'; import Onyx, {OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import * as API from '@libs/API'; -import {CustomRNImageManipulatorResult, FileWithUri} from '@libs/cropOrRotateImage/types'; +import {CustomRNImageManipulatorResult} from '@libs/cropOrRotateImage/types'; import * as LocalePhoneNumber from '@libs/LocalePhoneNumber'; import Navigation from '@libs/Navigation/Navigation'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; @@ -445,7 +445,7 @@ function openPublicProfilePage(accountID: number) { /** * Updates the user's avatar image */ -function updateAvatar(file: FileWithUri | CustomRNImageManipulatorResult) { +function updateAvatar(file: File | CustomRNImageManipulatorResult) { if (!currentUserAccountID) { return; } @@ -501,7 +501,7 @@ function updateAvatar(file: FileWithUri | CustomRNImageManipulatorResult) { ]; type UpdateUserAvatarParams = { - file: FileWithUri | CustomRNImageManipulatorResult; + file: File | CustomRNImageManipulatorResult; }; const parameters: UpdateUserAvatarParams = {file}; diff --git a/src/libs/cropOrRotateImage/index.ts b/src/libs/cropOrRotateImage/index.ts index 6b222c9759b5..a66ddbb40b00 100644 --- a/src/libs/cropOrRotateImage/index.ts +++ b/src/libs/cropOrRotateImage/index.ts @@ -1,4 +1,4 @@ -import {CropOptions, CropOrRotateImage, CropOrRotateImageOptions, FileWithUri} from './types'; +import {CropOptions, CropOrRotateImage, CropOrRotateImageOptions} from './types'; type SizeFromAngle = { width: number; @@ -71,13 +71,13 @@ function cropCanvas(canvas: HTMLCanvasElement, options: CropOptions) { return result; } -function convertCanvasToFile(canvas: HTMLCanvasElement, options: CropOrRotateImageOptions): Promise { +function convertCanvasToFile(canvas: HTMLCanvasElement, options: CropOrRotateImageOptions): Promise { return new Promise((resolve) => { canvas.toBlob((blob) => { if (!blob) { return; } - const file = new File([blob], options.name || 'fileName.jpeg', {type: options.type || 'image/jpeg'}) as FileWithUri; + const file = new File([blob], options.name || 'fileName.jpeg', {type: options.type || 'image/jpeg'}); file.uri = URL.createObjectURL(file); resolve(file); }); diff --git a/src/libs/cropOrRotateImage/types.ts b/src/libs/cropOrRotateImage/types.ts index 09f441bd9324..188d557a1258 100644 --- a/src/libs/cropOrRotateImage/types.ts +++ b/src/libs/cropOrRotateImage/types.ts @@ -18,12 +18,8 @@ type Action = { rotate?: number; }; -type FileWithUri = File & { - uri: string; -}; - type CustomRNImageManipulatorResult = RNImageManipulatorResult & {size: number; type: string; name: string}; -type CropOrRotateImage = (uri: string, actions: Action[], options: CropOrRotateImageOptions) => Promise; +type CropOrRotateImage = (uri: string, actions: Action[], options: CropOrRotateImageOptions) => Promise; -export type {CropOrRotateImage, CropOptions, Action, FileWithUri, CropOrRotateImageOptions, CustomRNImageManipulatorResult}; +export type {CropOrRotateImage, CropOptions, Action, CropOrRotateImageOptions, CustomRNImageManipulatorResult}; diff --git a/src/libs/fileDownload/FileUtils.js b/src/libs/fileDownload/FileUtils.ts similarity index 71% rename from src/libs/fileDownload/FileUtils.js rename to src/libs/fileDownload/FileUtils.ts index b838a81ea550..5bac47fb63ec 100644 --- a/src/libs/fileDownload/FileUtils.js +++ b/src/libs/fileDownload/FileUtils.ts @@ -2,6 +2,7 @@ import {Alert, Linking, Platform} from 'react-native'; import DateUtils from '@libs/DateUtils'; import * as Localize from '@libs/Localize'; import CONST from '@src/CONST'; +import type {ReadFileAsync, SplitExtensionFromFileName} from './types'; /** * Show alert on successful attachment download @@ -43,7 +44,9 @@ function showPermissionErrorAlert() { }, { text: Localize.translateLocal('common.settings'), - onPress: () => Linking.openSettings(), + onPress: () => { + Linking.openSettings(); + }, }, ]); } @@ -62,7 +65,9 @@ function showCameraPermissionsAlert() { }, { text: Localize.translateLocal('common.settings'), - onPress: () => Linking.openSettings(), + onPress: () => { + Linking.openSettings(); + }, }, ], {cancelable: false}, @@ -71,42 +76,36 @@ function showCameraPermissionsAlert() { /** * Generate a random file name with timestamp and file extension - * @param {String} url - * @returns {String} */ -function getAttachmentName(url) { +function getAttachmentName(url: string): string { if (!url) { return ''; } - return `${DateUtils.getDBTime()}.${url.split(/[#?]/)[0].split('.').pop().trim()}`; + return `${DateUtils.getDBTime()}.${url.split(/[#?]/)[0].split('.').pop()?.trim()}`; } -/** - * @param {String} fileName - * @returns {Boolean} - */ -function isImage(fileName) { +function isImage(fileName: string): boolean { return CONST.FILE_TYPE_REGEX.IMAGE.test(fileName); } -/** - * @param {String} fileName - * @returns {Boolean} - */ -function isVideo(fileName) { +function isVideo(fileName: string): boolean { return CONST.FILE_TYPE_REGEX.VIDEO.test(fileName); } /** * Returns file type based on the uri - * @param {String} fileUrl - * @returns {String} */ -function getFileType(fileUrl) { +function getFileType(fileUrl: string): string | undefined { if (!fileUrl) { return; } - const fileName = fileUrl.split('/').pop().split('?')[0].split('#')[0]; + + const fileName = fileUrl.split('/').pop()?.split('?')[0].split('#')[0]; + + if (!fileName) { + return; + } + if (isImage(fileName)) { return CONST.ATTACHMENT_FILE_TYPE.IMAGE; } @@ -118,32 +117,22 @@ function getFileType(fileUrl) { /** * Returns the filename split into fileName and fileExtension - * - * @param {String} fullFileName - * @returns {Object} */ -function splitExtensionFromFileName(fullFileName) { +const splitExtensionFromFileName: SplitExtensionFromFileName = (fullFileName) => { const fileName = fullFileName.trim(); const splitFileName = fileName.split('.'); const fileExtension = splitFileName.length > 1 ? splitFileName.pop() : ''; - return {fileName: splitFileName.join('.'), fileExtension}; -} + return {fileName: splitFileName.join('.'), fileExtension: fileExtension ?? ''}; +}; /** * Returns the filename replacing special characters with underscore - * - * @param {String} fileName - * @returns {String} */ -function cleanFileName(fileName) { +function cleanFileName(fileName: string): string { return fileName.replace(/[^a-zA-Z0-9\-._]/g, '_'); } -/** - * @param {String} fileName - * @returns {String} - */ -function appendTimeToFileName(fileName) { +function appendTimeToFileName(fileName: string): string { const file = splitExtensionFromFileName(fileName); let newFileName = `${file.fileName}-${DateUtils.getDBTime()}`; // Replace illegal characters before trying to download the attachment. @@ -156,21 +145,17 @@ function appendTimeToFileName(fileName) { /** * Reads a locally uploaded file - * - * @param {String} path - the blob url of the locally uplodaded file - * @param {String} fileName - * @param {Function} onSuccess - * @param {Function} onFailure - * - * @returns {Promise} + * @param path - the blob url of the locally uploaded file + * @param fileName - name of the file to read */ -const readFileAsync = (path, fileName, onSuccess, onFailure = () => {}) => +const readFileAsync: ReadFileAsync = (path, fileName, onSuccess, onFailure = () => {}) => new Promise((resolve) => { if (!path) { resolve(); + onFailure('[FileUtils] Path not specified'); + return; } - - return fetch(path) + fetch(path) .then((res) => { // For some reason, fetch is "Unable to read uploaded file" // on Android even though the blob is returned, so we'll ignore @@ -178,19 +163,26 @@ const readFileAsync = (path, fileName, onSuccess, onFailure = () => {}) => if (!res.ok && Platform.OS !== 'android') { throw Error(res.statusText); } - return res.blob(); - }) - .then((blob) => { - const file = new File([blob], cleanFileName(fileName), {type: blob.type}); - file.source = path; - // For some reason, the File object on iOS does not have a uri property - // so images aren't uploaded correctly to the backend - file.uri = path; - onSuccess(file); + res.blob() + .then((blob) => { + const file = new File([blob], cleanFileName(fileName)); + file.source = path; + // For some reason, the File object on iOS does not have a uri property + // so images aren't uploaded correctly to the backend + file.uri = path; + onSuccess(file); + resolve(file); + }) + .catch((e) => { + console.debug('[FileUtils] Could not read uploaded file', e); + onFailure(e); + resolve(); + }); }) .catch((e) => { console.debug('[FileUtils] Could not read uploaded file', e); onFailure(e); + resolve(); }); }); @@ -198,16 +190,16 @@ const readFileAsync = (path, fileName, onSuccess, onFailure = () => {}) => * Converts a base64 encoded image string to a File instance. * Adds a `uri` property to the File instance for accessing the blob as a URI. * - * @param {string} base64 - The base64 encoded image string. - * @param {string} filename - Desired filename for the File instance. - * @returns {File} The File instance created from the base64 string with an additional `uri` property. + * @param base64 - The base64 encoded image string. + * @param filename - Desired filename for the File instance. + * @returns The File instance created from the base64 string with an additional `uri` property. * * @example * const base64Image = "data:image/png;base64,..."; // your base64 encoded image * const imageFile = base64ToFile(base64Image, "example.png"); * console.log(imageFile.uri); // Blob URI */ -function base64ToFile(base64, filename) { +function base64ToFile(base64: string, filename: string): File { // Decode the base64 string const byteString = atob(base64.split(',')[1]); diff --git a/src/libs/fileDownload/getAttachmentDetails.js b/src/libs/fileDownload/getAttachmentDetails.ts similarity index 81% rename from src/libs/fileDownload/getAttachmentDetails.js rename to src/libs/fileDownload/getAttachmentDetails.ts index 28b678ffb651..5787979a3795 100644 --- a/src/libs/fileDownload/getAttachmentDetails.js +++ b/src/libs/fileDownload/getAttachmentDetails.ts @@ -1,12 +1,11 @@ import tryResolveUrlFromApiRoot from '@libs/tryResolveUrlFromApiRoot'; import CONST from '@src/CONST'; +import type {GetAttachmentDetails} from './types'; /** * Extract the thumbnail URL, source URL and the original filename from the HTML. - * @param {String} html - * @returns {Object} */ -export default function getAttachmentDetails(html) { +const getAttachmentDetails: GetAttachmentDetails = (html) => { // Files can be rendered either as anchor tag or as an image so based on that we have to form regex. const IS_IMAGE_TAG = //i.test(html); const PREVIEW_SOURCE_REGEX = new RegExp(`${CONST.ATTACHMENT_PREVIEW_ATTRIBUTE}*=*"(.+?)"`, 'i'); @@ -21,10 +20,10 @@ export default function getAttachmentDetails(html) { } // Files created/uploaded/hosted by App should resolve from API ROOT. Other URLs aren't modified - const sourceURL = tryResolveUrlFromApiRoot(html.match(SOURCE_REGEX)[1]); - const imageURL = IS_IMAGE_TAG && tryResolveUrlFromApiRoot(html.match(PREVIEW_SOURCE_REGEX)[1]); + const sourceURL = tryResolveUrlFromApiRoot(html.match(SOURCE_REGEX)?.[1] ?? ''); + const imageURL = IS_IMAGE_TAG ? tryResolveUrlFromApiRoot(html.match(PREVIEW_SOURCE_REGEX)?.[1] ?? '') : null; const previewSourceURL = IS_IMAGE_TAG ? imageURL : sourceURL; - const originalFileName = html.match(ORIGINAL_FILENAME_REGEX)[1]; + const originalFileName = html.match(ORIGINAL_FILENAME_REGEX)?.[1] ?? null; // Update the image URL so the images can be accessed depending on the config environment return { @@ -32,4 +31,6 @@ export default function getAttachmentDetails(html) { sourceURL, originalFileName, }; -} +}; + +export default getAttachmentDetails; diff --git a/src/libs/fileDownload/getImageResolution.native.js b/src/libs/fileDownload/getImageResolution.native.ts similarity index 61% rename from src/libs/fileDownload/getImageResolution.native.js rename to src/libs/fileDownload/getImageResolution.native.ts index f291886f4665..3bdff78a93ed 100644 --- a/src/libs/fileDownload/getImageResolution.native.js +++ b/src/libs/fileDownload/getImageResolution.native.ts @@ -1,14 +1,13 @@ +import {Asset} from 'react-native-image-picker'; +import type {GetImageResolution} from './types'; + /** * Get image resolution * Image object is returned as a result of a user selecting image using the react-native-image-picker * Image already has width and height properties coming from library so we just need to return them on native * Opposite to web where we need to create a new Image object and get dimensions from it * - * @param {*} file Picked file blob - * @returns {Promise} */ -function getImageResolution(file) { - return Promise.resolve({width: file.width, height: file.height}); -} +const getImageResolution: GetImageResolution = (file: Asset) => Promise.resolve({width: file.width ?? 0, height: file.height ?? 0}); export default getImageResolution; diff --git a/src/libs/fileDownload/getImageResolution.js b/src/libs/fileDownload/getImageResolution.ts similarity index 80% rename from src/libs/fileDownload/getImageResolution.js rename to src/libs/fileDownload/getImageResolution.ts index 2f9a6d4fbdb4..74dc7401d801 100644 --- a/src/libs/fileDownload/getImageResolution.js +++ b/src/libs/fileDownload/getImageResolution.ts @@ -1,3 +1,5 @@ +import type {GetImageResolution} from './types'; + /** * Get image resolution * File object is returned as a result of a user selecting image using the @@ -7,10 +9,8 @@ * new Image() is used specifically for performance reasons, opposed to using FileReader (5ms vs +100ms) * because FileReader is slow and causes a noticeable delay in the UI when selecting an image. * - * @param {*} file Picked file blob - * @returns {Promise} */ -function getImageResolution(file) { +const getImageResolution: GetImageResolution = (file) => { if (!(file instanceof File)) { return Promise.reject(new Error('Object is not an instance of File')); } @@ -20,14 +20,14 @@ function getImageResolution(file) { const objectUrl = URL.createObjectURL(file); image.onload = function () { resolve({ - width: this.naturalWidth, - height: this.naturalHeight, + width: (this as HTMLImageElement).naturalWidth, + height: (this as HTMLImageElement).naturalHeight, }); URL.revokeObjectURL(objectUrl); }; image.onerror = reject; image.src = objectUrl; }); -} +}; export default getImageResolution; diff --git a/src/libs/fileDownload/index.android.js b/src/libs/fileDownload/index.android.ts similarity index 82% rename from src/libs/fileDownload/index.android.js rename to src/libs/fileDownload/index.android.ts index c3528b579f67..41c7cb29550a 100644 --- a/src/libs/fileDownload/index.android.js +++ b/src/libs/fileDownload/index.android.ts @@ -1,15 +1,15 @@ import {PermissionsAndroid, Platform} from 'react-native'; -import RNFetchBlob from 'react-native-blob-util'; +import RNFetchBlob, {FetchBlobResponse} from 'react-native-blob-util'; import * as FileUtils from './FileUtils'; +import type {FileDownload} from './types'; /** * Android permission check to store images - * @returns {Promise} */ -function hasAndroidPermission() { +function hasAndroidPermission(): Promise { // On Android API Level 33 and above, these permissions do nothing and always return 'never_ask_again' // More info here: https://stackoverflow.com/a/74296799 - if (Platform.Version >= 33) { + if (Number(Platform.Version) >= 33) { return Promise.resolve(true); } @@ -31,11 +31,8 @@ function hasAndroidPermission() { /** * Handling the download - * @param {String} url - * @param {String} fileName - * @returns {Promise} */ -function handleDownload(url, fileName) { +function handleDownload(url: string, fileName: string): Promise { return new Promise((resolve) => { const dirs = RNFetchBlob.fs.dirs; @@ -46,7 +43,7 @@ function handleDownload(url, fileName) { const isLocalFile = url.startsWith('file://'); let attachmentPath = isLocalFile ? url : undefined; - let fetchedAttachment = Promise.resolve(); + let fetchedAttachment: Promise = Promise.resolve(); if (!isLocalFile) { // Fetching the attachment @@ -69,7 +66,7 @@ function handleDownload(url, fileName) { } if (!isLocalFile) { - attachmentPath = attachment.path(); + attachmentPath = (attachment as FetchBlobResponse).path(); } return RNFetchBlob.MediaCollection.copyToMediaStore( @@ -79,11 +76,13 @@ function handleDownload(url, fileName) { mimeType: null, }, 'Download', - attachmentPath, + attachmentPath ?? '', ); }) .then(() => { - RNFetchBlob.fs.unlink(attachmentPath); + if (attachmentPath) { + RNFetchBlob.fs.unlink(attachmentPath); + } FileUtils.showSuccessAlert(); }) .catch(() => { @@ -95,12 +94,9 @@ function handleDownload(url, fileName) { /** * Checks permission and downloads the file for Android - * @param {String} url - * @param {String} fileName - * @returns {Promise} */ -export default function fileDownload(url, fileName) { - return new Promise((resolve) => { +const fileDownload: FileDownload = (url, fileName) => + new Promise((resolve) => { hasAndroidPermission() .then((hasPermission) => { if (hasPermission) { @@ -113,4 +109,5 @@ export default function fileDownload(url, fileName) { }) .finally(() => resolve()); }); -} + +export default fileDownload; diff --git a/src/libs/fileDownload/index.ios.js b/src/libs/fileDownload/index.ios.ts similarity index 72% rename from src/libs/fileDownload/index.ios.js rename to src/libs/fileDownload/index.ios.ts index 1599e919d28a..fdc4a78e0b9b 100644 --- a/src/libs/fileDownload/index.ios.js +++ b/src/libs/fileDownload/index.ios.ts @@ -1,23 +1,20 @@ import {CameraRoll} from '@react-native-camera-roll/camera-roll'; -import lodashGet from 'lodash/get'; import RNFetchBlob from 'react-native-blob-util'; import CONST from '@src/CONST'; import * as FileUtils from './FileUtils'; +import type {FileDownload} from './types'; /** * Downloads the file to Documents section in iOS - * @param {String} fileUrl - * @param {String} fileName - * @returns {Promise} */ -function downloadFile(fileUrl, fileName) { +function downloadFile(fileUrl: string, fileName: string) { const dirs = RNFetchBlob.fs.dirs; // The iOS files will download to documents directory const path = dirs.DocumentDir; // Fetching the attachment - const fetchedAttachment = RNFetchBlob.config({ + return RNFetchBlob.config({ fileCache: true, path: `${path}/${fileName}`, addAndroidDownloads: { @@ -26,60 +23,61 @@ function downloadFile(fileUrl, fileName) { path: `${path}/Expensify/${fileName}`, }, }).fetch('GET', fileUrl); - return fetchedAttachment; } /** * Download the image to photo lib in iOS - * @param {String} fileUrl - * @param {String} fileName - * @returns {String} URI */ -function downloadImage(fileUrl) { +function downloadImage(fileUrl: string) { return CameraRoll.save(fileUrl); } /** * Download the video to photo lib in iOS - * @param {String} fileUrl - * @param {String} fileName - * @returns {String} URI */ -function downloadVideo(fileUrl, fileName) { +function downloadVideo(fileUrl: string, fileName: string): Promise { return new Promise((resolve, reject) => { - let documentPathUri = null; - let cameraRollUri = null; + let documentPathUri: string | null = null; + let cameraRollUri: string | null = null; // Because CameraRoll doesn't allow direct downloads of video with remote URIs, we first download as documents, then copy to photo lib and unlink the original file. downloadFile(fileUrl, fileName) .then((attachment) => { - documentPathUri = lodashGet(attachment, 'data'); + documentPathUri = attachment.data; + if (!documentPathUri) { + throw new Error('Error downloading video'); + } return CameraRoll.save(documentPathUri); }) .then((attachment) => { cameraRollUri = attachment; + if (!documentPathUri) { + throw new Error('Error downloading video'); + } return RNFetchBlob.fs.unlink(documentPathUri); }) - .then(() => resolve(cameraRollUri)) + .then(() => { + if (!cameraRollUri) { + throw new Error('Error downloading video'); + } + resolve(cameraRollUri); + }) .catch((err) => reject(err)); }); } /** * Download the file based on type(image, video, other file types)for iOS - * @param {String} fileUrl - * @param {String} fileName - * @returns {Promise} */ -export default function fileDownload(fileUrl, fileName) { - return new Promise((resolve) => { - let fileDownloadPromise = null; +const fileDownload: FileDownload = (fileUrl, fileName) => + new Promise((resolve) => { + let fileDownloadPromise; const fileType = FileUtils.getFileType(fileUrl); const attachmentName = FileUtils.appendTimeToFileName(fileName) || FileUtils.getAttachmentName(fileUrl); switch (fileType) { case CONST.ATTACHMENT_FILE_TYPE.IMAGE: - fileDownloadPromise = downloadImage(fileUrl, attachmentName); + fileDownloadPromise = downloadImage(fileUrl); break; case CONST.ATTACHMENT_FILE_TYPE.VIDEO: fileDownloadPromise = downloadVideo(fileUrl, attachmentName); @@ -108,4 +106,5 @@ export default function fileDownload(fileUrl, fileName) { }) .finally(() => resolve()); }); -} + +export default fileDownload; diff --git a/src/libs/fileDownload/index.js b/src/libs/fileDownload/index.js deleted file mode 100644 index 002594244def..000000000000 --- a/src/libs/fileDownload/index.js +++ /dev/null @@ -1,54 +0,0 @@ -import _ from 'lodash'; -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'; - -/** - * Downloading attachment in web, desktop - * @param {String} url - * @param {String} fileName - * @returns {Promise} - */ -export default function fileDownload(url, fileName) { - const resolvedUrl = tryResolveUrlFromApiRoot(url); - if (!resolvedUrl.startsWith(ApiUtils.getApiRoot()) && !_.some(CONST.ATTACHMENT_LOCAL_URL_PREFIX, (prefix) => resolvedUrl.startsWith(prefix))) { - // 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(); - } - - return ( - fetch(url) - .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'; - link.setAttribute( - 'download', - FileUtils.appendTimeToFileName(fileName) || FileUtils.getAttachmentName(url), // generating the file name - ); - - // 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); - }) - // file could not be downloaded, open sourceURL in new tab - .catch(() => Link.openExternalLink(url)) - ); -} diff --git a/src/libs/fileDownload/index.ts b/src/libs/fileDownload/index.ts new file mode 100644 index 000000000000..ef36647e549d --- /dev/null +++ b/src/libs/fileDownload/index.ts @@ -0,0 +1,53 @@ +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. + */ +const fileDownload: FileDownload = (url, fileName) => { + const resolvedUrl = tryResolveUrlFromApiRoot(url); + if (!resolvedUrl.startsWith(ApiUtils.getApiRoot()) && !CONST.ATTACHMENT_LOCAL_URL_PREFIX.some((prefix) => resolvedUrl.startsWith(prefix))) { + // 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(); + } + + return fetch(url) + .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'; + link.setAttribute( + 'download', + FileUtils.appendTimeToFileName(fileName) || FileUtils.getAttachmentName(url), // generating the file name + ); + + // 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(() => { + // file could not be downloaded, open sourceURL in new tab + Link.openExternalLink(url); + }); +}; + +export default fileDownload; diff --git a/src/libs/fileDownload/types.ts b/src/libs/fileDownload/types.ts new file mode 100644 index 000000000000..c7388f2e52a2 --- /dev/null +++ b/src/libs/fileDownload/types.ts @@ -0,0 +1,20 @@ +import {Asset} from 'react-native-image-picker'; + +type FileDownload = (url: string, fileName: string) => Promise; + +type ImageResolution = {width: number; height: number}; +type GetImageResolution = (url: File | Asset) => Promise; + +type ExtensionAndFileName = {fileName: string; fileExtension: string}; +type SplitExtensionFromFileName = (fileName: string) => ExtensionAndFileName; + +type ReadFileAsync = (path: string, fileName: string, onSuccess: (file: File) => void, onFailure: (error?: unknown) => void) => Promise; + +type AttachmentDetails = { + previewSourceURL: null | string; + sourceURL: null | string; + originalFileName: null | string; +}; +type GetAttachmentDetails = (html: string) => AttachmentDetails; + +export type {SplitExtensionFromFileName, GetAttachmentDetails, ReadFileAsync, FileDownload, GetImageResolution}; diff --git a/src/types/modules/pusher.d.ts b/src/types/modules/pusher.d.ts index b54a0508c309..fb7bbaa97f79 100644 --- a/src/types/modules/pusher.d.ts +++ b/src/types/modules/pusher.d.ts @@ -5,4 +5,11 @@ declare global { interface Window { getPusherInstance: () => Pusher | null; } + + // eslint-disable-next-line @typescript-eslint/consistent-type-definitions + interface File { + source?: string; + + uri?: string; + } }