Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
f59d792
refactor: move odometer image stitching to confirmation page
jakubkalinski0 Mar 12, 2026
4fdf7a8
Merge branch 'main' into jakubkalinski0/Odometer_move_odometer_image_…
jakubkalinski0 Mar 13, 2026
d39c77b
improvement: extract stitched odometer filename prefix to constants
jakubkalinski0 Mar 13, 2026
2307c1f
fix: guard stale async state updates in odometer image stitching effect
jakubkalinski0 Mar 13, 2026
16b0064
feat: add util to detect stitched odometer receipt filenames
jakubkalinski0 Mar 13, 2026
2c73b29
Merge branch 'main' into jakubkalinski0/Odometer_move_odometer_image_…
jakubkalinski0 Mar 16, 2026
0d018ec
Merge branch 'main' into jakubkalinski0/Odometer_move_odometer_image_…
jakubkalinski0 Mar 17, 2026
1211833
refactor: skip stitching when only one odometer image is present
jakubkalinski0 Mar 17, 2026
e35804a
refactor: remove unused isStitchedOdometerReceiptFilename function
jakubkalinski0 Mar 17, 2026
c75f917
Merge branch 'main' into jakubkalinski0/Odometer_move_odometer_image_…
jakubkalinski0 Mar 18, 2026
9e28da6
fix: remove image preview flickering by moving activity indicator to …
jakubkalinski0 Mar 18, 2026
e96e8e2
chore: prettier run
jakubkalinski0 Mar 18, 2026
0ea5970
fix: revert faulty removal of props from memo comparison
jakubkalinski0 Mar 18, 2026
1244c1e
Merge branch 'main' into jakubkalinski0/Odometer_move_odometer_image_…
jakubkalinski0 Mar 21, 2026
90808ab
fix: odometer receipt type helpers scope and MIME type resolution
jakubkalinski0 Mar 23, 2026
11f0d76
fix: add required reasonAttributes to ActivityIndicator in MoneyReque…
jakubkalinski0 Mar 23, 2026
f0f2558
fix: disable submit button while odometer receipt is stitching
jakubkalinski0 Mar 23, 2026
108e2f3
fix: add isLoadingReceipt to footerContent dependency array
jakubkalinski0 Mar 23, 2026
62a8d41
fix: pass iouType to shouldUseTransactionDraft in IOURequestStepConfi…
jakubkalinski0 Mar 23, 2026
05858e7
fix: add iouType to useEffect dependency array in IOURequestStepConfi…
jakubkalinski0 Mar 23, 2026
ca6fc8f
fix: navigate to correct screen after delete/rotate/crop in odometer …
jakubkalinski0 Mar 23, 2026
b1b14fd
Merge branch 'main' into jakubkalinski0/Odometer_move_odometer_image_…
jakubkalinski0 Mar 23, 2026
5009c79
fix: guard odometerGoBackRoute behind isOdometerImage check
jakubkalinski0 Mar 24, 2026
79d2fca
Merge branch 'main' into jakubkalinski0/Odometer_move_odometer_image_…
jakubkalinski0 Mar 24, 2026
68e2260
Merge branch 'main' into jakubkalinski0/Odometer_move_odometer_image_…
jakubkalinski0 Mar 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions src/components/MoneyRequestConfirmationList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,9 @@
/** Whether the expense is an odometer distance expense */
isOdometerDistanceRequest?: boolean;

/** Whether the odometer receipt is currently being stitched */
isLoadingReceipt?: boolean;

/** Whether the expense is a GPS distance expense */
isGPSDistanceRequest: boolean;

Expand Down Expand Up @@ -237,6 +240,7 @@
isDistanceRequest,
isManualDistanceRequest,
isOdometerDistanceRequest = false,
isLoadingReceipt = false,
isGPSDistanceRequest,
isPerDiemRequest = false,
isPolicyExpenseChat = false,
Expand Down Expand Up @@ -1138,7 +1142,7 @@
onSendMoney?.(paymentMethod);
}
},
[

Check warning on line 1145 in src/components/MoneyRequestConfirmationList.tsx

View workflow job for this annotation

GitHub Actions / ESLint check

React Hook useCallback has a missing dependency: 'isNewManualExpenseFlowEnabled'. Either include it or remove the dependency array

Check warning on line 1145 in src/components/MoneyRequestConfirmationList.tsx

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

React Hook useCallback has a missing dependency: 'isNewManualExpenseFlowEnabled'. Either include it or remove the dependency array
routeError,
transactionID,
iouType,
Expand Down Expand Up @@ -1263,7 +1267,7 @@
enterKeyEventListenerPriority={1}
useKeyboardShortcuts
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
isLoading={isConfirmed || isConfirming}
isLoading={isConfirmed || isConfirming || isLoadingReceipt}
Comment thread
jakubkalinski0 marked this conversation as resolved.
sentryLabel={CONST.SENTRY_LABEL.MONEY_REQUEST.CONFIRMATION_SUBMIT_BUTTON}
/>
</View>
Expand Down Expand Up @@ -1303,6 +1307,7 @@
styles.productTrainingTooltipWrapper,
shouldShowProductTrainingTooltip,
renderProductTrainingTooltip,
isLoadingReceipt,
]);

const isCompactMode = useMemo(() => !showMoreFields && isScanRequest, [isScanRequest, showMoreFields]);
Expand Down Expand Up @@ -1341,6 +1346,7 @@
isDistanceRequest={isDistanceRequest}
isManualDistanceRequest={isManualDistanceRequest}
isOdometerDistanceRequest={isOdometerDistanceRequest}
isLoadingReceipt={isLoadingReceipt}
isGPSDistanceRequest={isGPSDistanceRequest}
isPerDiemRequest={isPerDiemRequest}
isTimeRequest={isTimeRequest}
Expand Down Expand Up @@ -1436,5 +1442,6 @@
prevProps.isTimeRequest === nextProps.isTimeRequest &&
prevProps.iouTimeCount === nextProps.iouTimeCount &&
prevProps.iouTimeRate === nextProps.iouTimeRate &&
prevProps.shouldHideToSection === nextProps.shouldHideToSection,
prevProps.shouldHideToSection === nextProps.shouldHideToSection &&
prevProps.isLoadingReceipt === nextProps.isLoadingReceipt,
);
159 changes: 85 additions & 74 deletions src/components/MoneyRequestConfirmationListFooter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import type * as OnyxTypes from '@src/types/onyx';
import type {Attendee, Participant} from '@src/types/onyx/IOU';
import type {Unit} from '@src/types/onyx/Policy';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import ActivityIndicator from './ActivityIndicator';
import Badge from './Badge';
import Button from './Button';
import ConfirmedRoute from './ConfirmedRoute';
Expand Down Expand Up @@ -146,6 +147,9 @@ type MoneyRequestConfirmationListFooterProps = {
/** Flag indicating if it is an odometer distance request */
isOdometerDistanceRequest?: boolean;

/** Whether the receipt is currently being stitched */
isLoadingReceipt?: boolean;

/** Flag indicating if it is a GPS distance request */
isGPSDistanceRequest: boolean;

Expand Down Expand Up @@ -288,6 +292,7 @@ function MoneyRequestConfirmationListFooter({
isDistanceRequest,
isManualDistanceRequest,
isOdometerDistanceRequest = false,
isLoadingReceipt = false,
isGPSDistanceRequest,
isPerDiemRequest,
isTimeRequest,
Expand Down Expand Up @@ -1208,91 +1213,92 @@ function MoneyRequestConfirmationListFooter({

return (
<View
style={[styles.moneyRequestImage, receiptContainerStyle]}
style={[styles.moneyRequestImage, receiptContainerStyle, isLoadingReceipt && [styles.justifyContentCenter, styles.alignItemsCenter]]}
onLayout={isCompactMode ? handleCompactReceiptContainerLayout : undefined}
>
{isLocalFile && Str.isPDF(receiptFilename) ? (
<PressableWithoutFocus
onPress={() => {
if (!transactionID) {
return;
}
{isLoadingReceipt && <ActivityIndicator reasonAttributes={{context: 'MoneyRequestConfirmationListFooter.receiptThumbnail'}} />}
{!isLoadingReceipt &&
(isLocalFile && Str.isPDF(receiptFilename) ? (
<PressableWithoutFocus
onPress={() => {
if (!transactionID) {
return;
}

Navigation.navigate(
isReceiptEditable
? ROUTES.MONEY_REQUEST_RECEIPT_PREVIEW.getRoute(reportID, transactionID, action, iouType)
: ROUTES.TRANSACTION_RECEIPT.getRoute(reportID, transactionID),
);
}}
accessibilityRole={CONST.ROLE.BUTTON}
accessibilityLabel={translate('accessibilityHints.viewAttachment')}
sentryLabel={CONST.SENTRY_LABEL.REQUEST_CONFIRMATION_LIST.PDF_RECEIPT_THUMBNAIL}
disabled={!shouldDisplayReceipt}
disabledStyle={styles.cursorDefault}
style={styles.h100}
>
<PDFThumbnail
// eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style
previewSourceURL={resolvedReceiptImage as string}
Navigation.navigate(
isReceiptEditable
? ROUTES.MONEY_REQUEST_RECEIPT_PREVIEW.getRoute(reportID, transactionID, action, iouType)
: ROUTES.TRANSACTION_RECEIPT.getRoute(reportID, transactionID),
);
}}
accessibilityRole={CONST.ROLE.BUTTON}
accessibilityLabel={translate('accessibilityHints.viewAttachment')}
sentryLabel={CONST.SENTRY_LABEL.REQUEST_CONFIRMATION_LIST.PDF_RECEIPT_THUMBNAIL}
disabled={!shouldDisplayReceipt}
disabledStyle={styles.cursorDefault}
style={styles.h100}
onLoadError={onPDFLoadError}
onPassword={onPDFPassword}
/>
</PressableWithoutFocus>
) : (
<PressableWithoutFocus
onPress={() => {
if (!transactionID) {
return;
}
>
<PDFThumbnail
// eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style
previewSourceURL={resolvedReceiptImage as string}
style={styles.h100}
onLoadError={onPDFLoadError}
onPassword={onPDFPassword}
/>
</PressableWithoutFocus>
) : (
<PressableWithoutFocus
onPress={() => {
if (!transactionID) {
return;
}

Navigation.navigate(
isReceiptEditable
? ROUTES.MONEY_REQUEST_RECEIPT_PREVIEW.getRoute(reportID, transactionID, action, iouType)
: ROUTES.TRANSACTION_RECEIPT.getRoute(reportID, transactionID),
);
}}
disabled={!shouldDisplayReceipt || isThumbnail}
accessibilityRole={CONST.ROLE.BUTTON}
accessibilityLabel={translate('accessibilityHints.viewAttachment')}
sentryLabel={CONST.SENTRY_LABEL.REQUEST_CONFIRMATION_LIST.RECEIPT_THUMBNAIL}
disabledStyle={styles.cursorDefault}
style={receiptThumbnailStyle}
>
<ReceiptImage
isThumbnail={isThumbnail}
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
source={resolvedThumbnail || resolvedReceiptImage || ''}
// AuthToken is required when retrieving the image from the server
// but we don't need it to load the blob:// or file:// image when starting an expense/split
// So if we have a thumbnail, it means we're retrieving the image from the server
isAuthTokenRequired={!!receiptThumbnail && !isLocalFile}
fileExtension={fileExtension}
shouldUseThumbnailImage
shouldUseInitialObjectPosition={isDistanceRequest}
shouldUseFullHeight={isCompactMode}
onLoad={handleReceiptLoad}
resizeMode={isOdometerDistanceRequest ? 'contain' : undefined}
/>
</PressableWithoutFocus>
)}
Navigation.navigate(
isReceiptEditable
? ROUTES.MONEY_REQUEST_RECEIPT_PREVIEW.getRoute(reportID, transactionID, action, iouType)
: ROUTES.TRANSACTION_RECEIPT.getRoute(reportID, transactionID),
);
}}
disabled={!shouldDisplayReceipt || isThumbnail}
accessibilityRole={CONST.ROLE.BUTTON}
accessibilityLabel={translate('accessibilityHints.viewAttachment')}
sentryLabel={CONST.SENTRY_LABEL.REQUEST_CONFIRMATION_LIST.RECEIPT_THUMBNAIL}
disabledStyle={styles.cursorDefault}
style={receiptThumbnailStyle}
>
<ReceiptImage
isThumbnail={isThumbnail}
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
source={resolvedThumbnail || resolvedReceiptImage || ''}
// AuthToken is required when retrieving the image from the server
// but we don't need it to load the blob:// or file:// image when starting an expense/split
// So if we have a thumbnail, it means we're retrieving the image from the server
isAuthTokenRequired={!!receiptThumbnail && !isLocalFile}
fileExtension={fileExtension}
shouldUseThumbnailImage
shouldUseInitialObjectPosition={isDistanceRequest}
shouldUseFullHeight={isCompactMode}
onLoad={handleReceiptLoad}
resizeMode={isOdometerDistanceRequest ? 'contain' : undefined}
/>
</PressableWithoutFocus>
))}
</View>
);
}, [
isCompactMode,
compactReceiptContainerStyle,
styles.expenseViewImageSmall,
styles.moneyRequestImage,
styles.flex1,
styles.h100,
styles.flex1,
styles.moneyRequestImage,
styles.justifyContentCenter,
styles.alignItemsCenter,
styles.cursorDefault,
isLoadingReceipt,
handleCompactReceiptContainerLayout,
isLocalFile,
receiptFilename,
transactionID,
isReceiptEditable,
reportID,
action,
iouType,
translate,
shouldDisplayReceipt,
resolvedReceiptImage,
Expand All @@ -1303,9 +1309,13 @@ function MoneyRequestConfirmationListFooter({
receiptThumbnail,
fileExtension,
isDistanceRequest,
isOdometerDistanceRequest,
handleReceiptLoad,
handleCompactReceiptContainerLayout,
isOdometerDistanceRequest,
transactionID,
isReceiptEditable,
reportID,
action,
iouType,
]);

const visibleFields = fields.filter((field) => field.shouldShow);
Expand Down Expand Up @@ -1396,7 +1406,7 @@ function MoneyRequestConfirmationListFooter({
)}
</View>
{(!shouldShowMap || isManualDistanceRequest || isOdometerDistanceRequest) &&
(hasReceiptImageOrThumbnail
(hasReceiptImageOrThumbnail || isLoadingReceipt
? receiptThumbnailContent
: showReceiptEmptyState && (
<ReceiptEmptyState
Expand Down Expand Up @@ -1490,5 +1500,6 @@ export default memo(
prevProps.showMoreFields === nextProps.showMoreFields &&
prevProps.isTimeRequest === nextProps.isTimeRequest &&
prevProps.iouTimeCount === nextProps.iouTimeCount &&
prevProps.iouTimeRate === nextProps.iouTimeRate,
prevProps.iouTimeRate === nextProps.iouTimeRate &&
prevProps.isLoadingReceipt === nextProps.isLoadingReceipt,
);
3 changes: 3 additions & 0 deletions src/libs/stitchOdometerImages/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const STITCHED_ODOMETER_FILENAME_PREFIX = 'stitched_odometer';

export default STITCHED_ODOMETER_FILENAME_PREFIX;
5 changes: 3 additions & 2 deletions src/libs/stitchOdometerImages/index.native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {ImageFormat, Skia} from '@shopify/react-native-skia';
import RNFS from 'react-native-fs';
import Log from '@libs/Log';
import type {FileObject} from '@src/types/utils/Attachment';
import STITCHED_ODOMETER_FILENAME_PREFIX from './constants';
import calculateStitchLayout from './stitchLayout';

async function stitchOdometerImages(image1: FileObject | string | undefined, image2: FileObject | string | undefined): Promise<FileObject | null> {
Expand Down Expand Up @@ -46,13 +47,13 @@ async function stitchOdometerImages(image1: FileObject | string | undefined, ima
// Delete any previously stitched files before creating a new one
try {
const tempDirContents = await RNFS.readDir(RNFS.TemporaryDirectoryPath);
const oldStitchedFiles = tempDirContents.filter((f) => f.name.startsWith('stitched_odometer_') && f.name.endsWith('.jpg'));
const oldStitchedFiles = tempDirContents.filter((f) => f.name.startsWith(`${STITCHED_ODOMETER_FILENAME_PREFIX}_`) && f.name.endsWith('.jpg'));
await Promise.all(oldStitchedFiles.map((f) => RNFS.unlink(f.path)));
} catch (error) {
Log.warn('stitchOdometerImages (native) failed to clean up old stitched files', {error});
}

const filename = `stitched_odometer_${Date.now()}.jpg`;
const filename = `${STITCHED_ODOMETER_FILENAME_PREFIX}_${Date.now()}.jpg`;
const tempPath = `${RNFS.TemporaryDirectoryPath}/${filename}`;
await RNFS.writeFile(tempPath, base64, 'base64');

Expand Down
3 changes: 2 additions & 1 deletion src/libs/stitchOdometerImages/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type {FileObject} from '@src/types/utils/Attachment';
import STITCHED_ODOMETER_FILENAME_PREFIX from './constants';
import calculateStitchLayout from './stitchLayout';

// Tracks the single active stitched blob URL so that we can revoke it on the next call so at most one blob URL exists at a time
Expand Down Expand Up @@ -45,7 +46,7 @@ function stitchOdometerImages(image1: FileObject | string | undefined, image2: F
}
const uri = URL.createObjectURL(blob);
previousBlobUrl = uri;
resolve({uri, name: 'stitched_odometer.jpg', type: 'image/jpeg'});
resolve({uri, name: `${STITCHED_ODOMETER_FILENAME_PREFIX}.jpg`, type: 'image/jpeg'});
}, 'image/jpeg');
});
});
Expand Down
Loading
Loading