Skip to content

Commit 7d76b9f

Browse files
authored
Merge pull request Expensify#72299 from TaduJR/fix-Android-Multi-scan-App-closes-when-scanning-multiple-receipt-at-once
fix: Android - Multi scan - App closes when scanning multiple receipt at once
2 parents 32b670d + fd6e0f2 commit 7d76b9f

12 files changed

Lines changed: 167 additions & 46 deletions

File tree

src/components/AttachmentPicker/index.native.tsx

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,26 @@ type Item = {
4242
pickAttachment: () => Promise<Asset[] | void | LocalCopy[]>;
4343
};
4444

45+
/**
46+
* Ensures asset has proper fileName and type properties
47+
*/
48+
const processAssetWithFallbacks = (asset: Asset): Asset => {
49+
// Generate fallback name: extract from URI if available, otherwise use timestamped default
50+
const fallbackName = asset.uri
51+
? asset.uri
52+
.substring(asset.uri.lastIndexOf('/') + 1)
53+
.split('?')
54+
.at(0)
55+
: `image_${Date.now()}.jpeg`;
56+
const fileName = asset.fileName ?? fallbackName;
57+
return {
58+
...asset,
59+
fileName,
60+
// Default to JPEG if no type specified
61+
type: asset.type ?? 'image/jpeg',
62+
};
63+
};
64+
4565
/**
4666
* Return imagePickerOptions based on the type
4767
*/
@@ -202,7 +222,9 @@ function AttachmentPicker({
202222
checkAllProcessed();
203223
});
204224
} else {
205-
processedAssets.push(asset);
225+
// Ensure the asset has proper fileName and type for non-HEIC images
226+
const processedAsset = processAssetWithFallbacks(asset);
227+
processedAssets.push(processedAsset);
206228
checkAllProcessed();
207229
}
208230
})
@@ -211,7 +233,9 @@ function AttachmentPicker({
211233
checkAllProcessed();
212234
});
213235
} else {
214-
processedAssets.push(asset);
236+
// Ensure the asset has proper fileName and type
237+
const processedAsset = processAssetWithFallbacks(asset);
238+
processedAssets.push(processedAsset);
215239
checkAllProcessed();
216240
}
217241
});

src/libs/actions/IOU.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12096,7 +12096,7 @@ function navigateToStartStepIfScanFileCannotBeRead(
1209612096
}
1209712097

1209812098
const onFailure = () => {
12099-
setMoneyRequestReceipt(transactionID, '', '', true);
12099+
setMoneyRequestReceipt(transactionID, '', '', true, '');
1210012100
if (requestType === CONST.IOU.REQUEST_TYPE.MANUAL) {
1210112101
if (onFailureCallback) {
1210212102
onFailureCallback();
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import RNFS from 'react-native-fs';
2+
3+
/**
4+
* Checks if a file exists at the given path without loading it into memory.
5+
* This is a memory-safe alternative to readFileAsync for validation.
6+
*
7+
* @param path - The file path to check (typically starts with file://)
8+
* @returns Promise that resolves to true if file exists, false otherwise
9+
*/
10+
function checkFileExists(path: string | undefined): Promise<boolean> {
11+
if (!path) {
12+
return Promise.resolve(false);
13+
}
14+
15+
// Decode URI if it's URL-encoded (handles special characters in filenames)
16+
let decodedPath = path;
17+
try {
18+
decodedPath = decodeURI(path);
19+
} catch (e) {
20+
// If decoding fails, use the original path
21+
decodedPath = path;
22+
}
23+
24+
// RNFS.stat() returns file info without loading the file content
25+
return RNFS.stat(decodedPath)
26+
.then((fileStat) => {
27+
// File exists if we get stats and it's actually a file (not directory)
28+
return fileStat.isFile();
29+
})
30+
.catch(() => {
31+
// File doesn't exist or can't be accessed
32+
return false;
33+
});
34+
}
35+
36+
export default checkFileExists;
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import checkFileExists from '@libs/fileDownload/checkFileExists';
2+
import type {ReceiptSource} from '@src/types/onyx/Transaction';
3+
4+
/**
5+
* Validates a receipt file and processes it for upload
6+
* Uses checkFileExists for memory-efficient file validation without loading the entire file
7+
*/
8+
function validateReceiptFile(
9+
receiptFilename: string | undefined,
10+
receiptPath: ReceiptSource | undefined,
11+
receiptType: string | undefined,
12+
onSuccess: (file: File) => void,
13+
onFailure: () => void,
14+
): Promise<void> {
15+
const receiptPathString = receiptPath?.toString();
16+
return checkFileExists(receiptPathString).then((exists) => {
17+
if (!exists) {
18+
onFailure();
19+
return;
20+
}
21+
22+
onSuccess({uri: receiptPathString, name: receiptFilename, type: receiptType, source: receiptPathString} as File);
23+
});
24+
}
25+
26+
export default validateReceiptFile;
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import {checkIfScanFileCanBeRead} from '@libs/actions/IOU';
2+
import type {ReceiptSource} from '@src/types/onyx/Transaction';
3+
4+
/**
5+
* Validates a receipt file and processes it for upload
6+
* Uses readFileAsync to load the file into memory for processing
7+
*/
8+
function validateReceiptFile(
9+
receiptFilename: string | undefined,
10+
receiptPath: ReceiptSource | undefined,
11+
receiptType: string | undefined,
12+
onSuccess: (file: File) => void,
13+
onFailure: () => void,
14+
): Promise<void | File> | undefined {
15+
return checkIfScanFileCanBeRead(receiptFilename, receiptPath, receiptType, onSuccess, onFailure);
16+
}
17+
18+
export default validateReceiptFile;

src/libs/prepareRequestPayload/index.native.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import checkFileExists from '@libs/fileDownload/checkFileExists';
12
import {readFileAsync} from '@libs/fileDownload/FileUtils';
23
import validateFormDataParameter from '@libs/validateFormDataParameter';
34
import type PrepareRequestPayload from './types';
@@ -18,7 +19,25 @@ const prepareRequestPayload: PrepareRequestPayload = (command, data, initiatedOf
1819
return Promise.resolve();
1920
}
2021

21-
if ((key === 'receipt' || key === 'file') && initiatedOffline) {
22+
if (key === 'receipt') {
23+
const {source, name, type, uri} = value as File;
24+
if (source) {
25+
return checkFileExists(source).then((exists) => {
26+
if (!exists) {
27+
return;
28+
}
29+
const receiptFormData = {
30+
uri,
31+
name,
32+
type,
33+
};
34+
validateFormDataParameter(command, key, receiptFormData);
35+
formData.append(key, receiptFormData as File);
36+
});
37+
}
38+
}
39+
40+
if (key === 'file' && initiatedOffline) {
2241
const {uri: path = '', source} = value as File;
2342
if (!source) {
2443
validateFormDataParameter(command, key, value);

src/pages/Search/SearchPage.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,7 @@ function SearchPage({route}: SearchPageProps) {
255255
) as PaymentData[];
256256

257257
payMoneyRequestOnSearch(hash, paymentData, transactionIDList);
258+
// eslint-disable-next-line @typescript-eslint/no-deprecated
258259
InteractionManager.runAfterInteractions(() => {
259260
clearSelectedTransactions();
260261
});
@@ -614,7 +615,7 @@ function SearchPage({route}: SearchPageProps) {
614615
source,
615616
transactionID,
616617
});
617-
setMoneyRequestReceipt(transactionID, source, file.name ?? '', true);
618+
setMoneyRequestReceipt(transactionID, source, file.name ?? '', true, file.type);
618619
});
619620

620621
if (isPaidGroupPolicy(activePolicy) && activePolicy?.isPolicyExpenseChatEnabled && !shouldRestrictUserBillableActions(activePolicy.id)) {

src/pages/home/report/ReportActionCompose/useAttachmentUploadValidation.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ function useAttachmentUploadValidation({
103103
reportID,
104104
});
105105
const newTransactionID = newTransaction?.transactionID ?? CONST.IOU.OPTIMISTIC_TRANSACTION_ID;
106-
setMoneyRequestReceipt(newTransactionID, source, file.name ?? '', true);
106+
setMoneyRequestReceipt(newTransactionID, source, file.name ?? '', true, file.type);
107107
setMoneyRequestParticipantsFromReport(newTransactionID, report);
108108
});
109109
Navigation.navigate(

src/pages/iou/request/step/IOURequestStepConfirmation.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {completeTestDriveTask} from '@libs/actions/Task';
3232
import DateUtils from '@libs/DateUtils';
3333
import {canUseTouchScreen} from '@libs/DeviceCapabilities';
3434
import {isLocalFile as isLocalFileFileUtils} from '@libs/fileDownload/FileUtils';
35+
import validateReceiptFile from '@libs/fileDownload/validateReceiptFile';
3536
import getCurrentPosition from '@libs/getCurrentPosition';
3637
import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
3738
import getReceiptFilenameFromTransaction from '@libs/getReceiptFilenameFromTransaction';
@@ -62,7 +63,6 @@ import {
6263
} from '@libs/TransactionUtils';
6364
import type {GpsPoint} from '@userActions/IOU';
6465
import {
65-
checkIfScanFileCanBeRead,
6666
createDistanceRequest as createDistanceRequestIOUActions,
6767
getIOURequestPolicyID,
6868
getReceiverType,
@@ -452,11 +452,11 @@ function IOURequestStepConfirmation({
452452
const onFailure = () => {
453453
isScanFilesCanBeRead = false;
454454
if (initialTransactionID === item.transactionID) {
455-
setMoneyRequestReceipt(item.transactionID, '', '', true);
455+
setMoneyRequestReceipt(item.transactionID, '', '', true, '');
456456
}
457457
};
458458

459-
return checkIfScanFileCanBeRead(itemReceiptFilename, itemReceiptPath, itemReceiptType, onSuccess, onFailure);
459+
return validateReceiptFile(itemReceiptFilename, itemReceiptPath, itemReceiptType, onSuccess, onFailure) ?? Promise.resolve();
460460
}),
461461
).then(() => {
462462
if (isScanFilesCanBeRead) {
@@ -1107,7 +1107,7 @@ function IOURequestStepConfirmation({
11071107
return;
11081108
}
11091109
const source = URL.createObjectURL(file as Blob);
1110-
setMoneyRequestReceipt(currentTransactionID, source, file.name ?? '', true);
1110+
setMoneyRequestReceipt(currentTransactionID, source, file.name ?? '', true, file.type);
11111111
};
11121112

11131113
const {validateFiles, PDFValidationComponent, ErrorModal} = useFilesValidation(setReceiptOnDrop);

src/pages/iou/request/step/IOURequestStepScan/cropImageToAspectRatio.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ function calculateCropRect(imageWidth: number, imageHeight: number, aspectRatioW
3232
return {width, height, originX, originY};
3333
}
3434

35-
const IMAGE_TYPE = 'png';
35+
const IMAGE_TYPE = 'image/jpeg';
3636

3737
function cropImageToAspectRatio(
3838
/** Source image */
@@ -58,7 +58,7 @@ function cropImageToAspectRatio(
5858
}
5959

6060
const crop = calculateCropRect(imageWidth, imageHeight, aspectRatioWidth, aspectRatioHeight, shouldAlignTop);
61-
const croppedFilename = `receipt_cropped_${Date.now()}.${IMAGE_TYPE}`;
61+
const croppedFilename = `receipt_cropped_${Date.now()}.jpeg`;
6262

6363
return cropOrRotateImage(image.source, [{crop}], {compress: 1, name: croppedFilename, type: IMAGE_TYPE}).then((croppedImage) => {
6464
if (!croppedImage?.uri || !croppedImage?.name) {

0 commit comments

Comments
 (0)