diff --git a/android/app/build.gradle b/android/app/build.gradle
index f224d895e2fa..f7a83db89d9a 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -91,8 +91,8 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled rootProject.ext.multiDexEnabled
- versionCode 1001040801
- versionName "1.4.8-1"
+ versionCode 1001040802
+ versionName "1.4.8-2"
}
flavorDimensions "default"
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index 7c3fbf13697a..9465f768afe1 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -40,7 +40,7 @@
CFBundleVersion
- 1.4.8.1
+ 1.4.8.2
ITSAppUsesNonExemptEncryption
LSApplicationQueriesSchemes
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index 0d2561b67b74..b4ae3184e624 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -19,6 +19,6 @@
CFBundleSignature
????
CFBundleVersion
- 1.4.8.1
+ 1.4.8.2
diff --git a/package-lock.json b/package-lock.json
index 0bc56cb5d907..8ba5f7f109c5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "new.expensify",
- "version": "1.4.8-1",
+ "version": "1.4.8-2",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "1.4.8-1",
+ "version": "1.4.8-2",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
diff --git a/package.json b/package.json
index f0b48a32e5e7..875dc572a3fd 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "1.4.8-1",
+ "version": "1.4.8-2",
"author": "Expensify, Inc.",
"homepage": "https://new.expensify.com",
"description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.",
diff --git a/src/components/Attachments/AttachmentCarousel/CarouselItem.js b/src/components/Attachments/AttachmentCarousel/CarouselItem.js
index b6cc0cbf21a4..fbc49590d5ae 100644
--- a/src/components/Attachments/AttachmentCarousel/CarouselItem.js
+++ b/src/components/Attachments/AttachmentCarousel/CarouselItem.js
@@ -27,8 +27,8 @@ const propTypes = {
/** Additional information about the attachment file */
file: PropTypes.shape({
/** File name of the attachment */
- name: PropTypes.string,
- }),
+ name: PropTypes.string.isRequired,
+ }).isRequired,
/** Whether the attachment has been flagged */
hasBeenFlagged: PropTypes.bool,
diff --git a/src/components/Attachments/AttachmentCarousel/extractAttachmentsFromReport.js b/src/components/Attachments/AttachmentCarousel/extractAttachmentsFromReport.js
index 0f1fa15c99ca..6cd85e4243b2 100644
--- a/src/components/Attachments/AttachmentCarousel/extractAttachmentsFromReport.js
+++ b/src/components/Attachments/AttachmentCarousel/extractAttachmentsFromReport.js
@@ -1,6 +1,7 @@
import {Parser as HtmlParser} from 'htmlparser2';
import lodashGet from 'lodash/get';
import _ from 'underscore';
+import * as FileUtils from '@libs/fileDownload/FileUtils';
import * as ReceiptUtils from '@libs/ReceiptUtils';
import * as ReportActionsUtils from '@libs/ReportActionsUtils';
import * as TransactionUtils from '@libs/TransactionUtils';
@@ -25,14 +26,16 @@ function extractAttachmentsFromReport(parentReportAction, reportActions, transac
}
const expensifySource = attribs[CONST.ATTACHMENT_SOURCE_ATTRIBUTE];
+ const source = tryResolveUrlFromApiRoot(expensifySource || attribs.src);
+ const fileName = attribs[CONST.ATTACHMENT_ORIGINAL_FILENAME_ATTRIBUTE] || FileUtils.getFileName(`${source}`);
// By iterating actions in chronological order and prepending each attachment
// we ensure correct order of attachments even across actions with multiple attachments.
attachments.unshift({
+ source,
reportActionID: attribs['data-id'],
- source: tryResolveUrlFromApiRoot(expensifySource || attribs.src),
isAuthTokenRequired: Boolean(expensifySource),
- file: {name: attribs[CONST.ATTACHMENT_ORIGINAL_FILENAME_ATTRIBUTE]},
+ file: {name: fileName},
isReceipt: false,
hasBeenFlagged: attribs['data-flagged'] === 'true',
});
diff --git a/src/components/Attachments/propTypes.js b/src/components/Attachments/propTypes.js
index 698a41de9648..13adc468ce64 100644
--- a/src/components/Attachments/propTypes.js
+++ b/src/components/Attachments/propTypes.js
@@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
const attachmentSourcePropType = PropTypes.oneOfType([PropTypes.string, PropTypes.func, PropTypes.number]);
const attachmentFilePropType = PropTypes.shape({
- name: PropTypes.string,
+ name: PropTypes.string.isRequired,
});
const attachmentPropType = PropTypes.shape({
@@ -13,7 +13,7 @@ const attachmentPropType = PropTypes.shape({
source: attachmentSourcePropType.isRequired,
/** File object can be an instance of File or Object */
- file: attachmentFilePropType,
+ file: attachmentFilePropType.isRequired,
});
const attachmentsPropType = PropTypes.arrayOf(attachmentPropType);
diff --git a/src/libs/fileDownload/FileUtils.ts b/src/libs/fileDownload/FileUtils.ts
index 618571ddf400..09cc1222310f 100644
--- a/src/libs/fileDownload/FileUtils.ts
+++ b/src/libs/fileDownload/FileUtils.ts
@@ -1,6 +1,7 @@
import {Alert, Linking, Platform} from 'react-native';
import DateUtils from '@libs/DateUtils';
import * as Localize from '@libs/Localize';
+import Log from '@libs/Log';
import CONST from '@src/CONST';
import type {ReadFileAsync, SplitExtensionFromFileName} from './types';
@@ -75,13 +76,22 @@ function showCameraPermissionsAlert() {
}
/**
- * Generate a random file name with timestamp and file extension
+ * Extracts a filename from a given URL and sanitizes it for file system usage.
+ *
+ * This function takes a URL as input and performs the following operations:
+ * 1. Extracts the last segment of the URL, which could be a file name, a path segment,
+ * or a query string parameter.
+ * 2. Decodes the extracted segment from URL encoding to a plain string for better readability.
+ * 3. Replaces any characters in the decoded string that are illegal in file names
+ * with underscores.
*/
-function getAttachmentName(url: string): string {
- if (!url) {
- return '';
+function getFileName(url: string): string {
+ const fileName = url.split(/[#?/]/).pop() ?? '';
+ if (!fileName) {
+ Log.warn('[FileUtils] Could not get attachment name', {url});
}
- return `${DateUtils.getDBTime()}.${url.split(/[#?]/)[0].split('.').pop()?.trim()}`;
+
+ return decodeURIComponent(fileName).replace(CONST.REGEX.ILLEGAL_FILENAME_CHARACTERS, '_');
}
function isImage(fileName: string): boolean {
@@ -231,7 +241,7 @@ export {
showPermissionErrorAlert,
showCameraPermissionsAlert,
splitExtensionFromFileName,
- getAttachmentName,
+ getFileName,
getFileType,
cleanFileName,
appendTimeToFileName,
diff --git a/src/libs/fileDownload/index.android.ts b/src/libs/fileDownload/index.android.ts
index 41c7cb29550a..8496c1cb6cf5 100644
--- a/src/libs/fileDownload/index.android.ts
+++ b/src/libs/fileDownload/index.android.ts
@@ -38,7 +38,7 @@ function handleDownload(url: string, fileName: string): Promise {
// Android files will download to Download directory
const path = dirs.DownloadDir;
- const attachmentName = FileUtils.appendTimeToFileName(fileName) || FileUtils.getAttachmentName(url);
+ const attachmentName = FileUtils.appendTimeToFileName(fileName || FileUtils.getFileName(url));
const isLocalFile = url.startsWith('file://');
diff --git a/src/libs/fileDownload/index.ios.ts b/src/libs/fileDownload/index.ios.ts
index fdc4a78e0b9b..7672b4b14926 100644
--- a/src/libs/fileDownload/index.ios.ts
+++ b/src/libs/fileDownload/index.ios.ts
@@ -73,7 +73,7 @@ const fileDownload: FileDownload = (fileUrl, fileName) =>
new Promise((resolve) => {
let fileDownloadPromise;
const fileType = FileUtils.getFileType(fileUrl);
- const attachmentName = FileUtils.appendTimeToFileName(fileName) || FileUtils.getAttachmentName(fileUrl);
+ const attachmentName = FileUtils.appendTimeToFileName(fileName || FileUtils.getFileName(fileUrl));
switch (fileType) {
case CONST.ATTACHMENT_FILE_TYPE.IMAGE:
diff --git a/src/libs/fileDownload/index.ts b/src/libs/fileDownload/index.ts
index ef36647e549d..4f43b215925f 100644
--- a/src/libs/fileDownload/index.ts
+++ b/src/libs/fileDownload/index.ts
@@ -29,10 +29,7 @@ const fileDownload: FileDownload = (url, fileName) => {
// adding href to anchor
link.href = href;
link.style.display = 'none';
- link.setAttribute(
- 'download',
- FileUtils.appendTimeToFileName(fileName) || FileUtils.getAttachmentName(url), // generating the file name
- );
+ link.download = FileUtils.appendTimeToFileName(fileName || FileUtils.getFileName(url));
// Append to html link element page
document.body.appendChild(link);