Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
c0ce58a
Improve scanned receipt quality without slowing the shutter
OlimpiaZurek Sep 8, 2026
858832d
address review comments
OlimpiaZurek Sep 8, 2026
95ad174
restore a receipt left behind by an interrupted swap
OlimpiaZurek Sep 8, 2026
f5a8fcd
fix backup restore racing
OlimpiaZurek Sep 10, 2026
bb30963
Upgrade the receipt on every scan flow
OlimpiaZurek Sep 10, 2026
1f66b8c
Merge branch 'main' into fix/receipt-capture-quality-99615
OlimpiaZurek Sep 10, 2026
0e02461
Measure the receipt upgrade and change naming
OlimpiaZurek Sep 10, 2026
8a5679e
Register the upgrade before handing over the receipt
OlimpiaZurek Sep 10, 2026
3c10373
Keep the receipt upgrade off the request queue
OlimpiaZurek Sep 11, 2026
caf613e
Merge branch 'main' into fix/receipt-capture-quality-99615
OlimpiaZurek Sep 14, 2026
64b1ad3
Keep the photo as captured when it needs no turn
OlimpiaZurek Sep 14, 2026
ed7189b
fix spell check
OlimpiaZurek Sep 14, 2026
ba1aae1
Merge branch 'main' into fix/receipt-capture-quality-99615
OlimpiaZurek Sep 15, 2026
6524a69
fix:recover the receipt without waiting for an upload
OlimpiaZurek Sep 15, 2026
16838e3
fix unit tests
OlimpiaZurek Sep 15, 2026
170a2a9
Merge branch 'main' into fix/receipt-capture-quality-99615
OlimpiaZurek Sep 16, 2026
47b2611
add telemtry and tests
OlimpiaZurek Sep 17, 2026
c74076b
Fix spelling in a comment
OlimpiaZurek Sep 17, 2026
6760c65
Merge branch 'main' into fix/receipt-capture-quality-99615
OlimpiaZurek Sep 21, 2026
96fb6d6
Merge branch 'main' into fix/receipt-capture-quality-99615
OlimpiaZurek Sep 21, 2026
238d47b
Drop unnecessary comments
OlimpiaZurek Sep 21, 2026
b2300b1
address review comments
OlimpiaZurek Sep 24, 2026
7ae0d59
Merge branch 'main' into fix/receipt-capture-quality-99615
OlimpiaZurek Sep 24, 2026
5370d05
Skip the sweep without a receipts folder and use swap-specific suffixes
OlimpiaZurek Sep 25, 2026
12a191c
Merge branch 'main' into fix/receipt-capture-quality-99615
OlimpiaZurek Sep 28, 2026
7af3dd5
Address review comments
OlimpiaZurek Sep 29, 2026
e09d96f
fix: receipt old preview and blurry image
OlimpiaZurek Sep 30, 2026
302954d
Merge remote-tracking branch 'upstream/main' into fix/receipt-capture…
OlimpiaZurek Oct 1, 2026
175246e
Fix the receipt preview flash when the upgrade lands
OlimpiaZurek Oct 5, 2026
8f60872
Merge branch 'main' into fix/receipt-capture-quality-99615
OlimpiaZurek Oct 5, 2026
cf82799
Address review comments
OlimpiaZurek Oct 7, 2026
4e9da83
Merge branch 'main' into fix/receipt-capture-quality-99615
OlimpiaZurek Oct 7, 2026
465281c
Final code cleanups
OlimpiaZurek Oct 7, 2026
fc19aed
Merge branch 'main' into fix/receipt-capture-quality-99615
OlimpiaZurek Oct 7, 2026
b509df7
Merge branch 'main' into fix/receipt-capture-quality-99615
OlimpiaZurek Oct 8, 2026
cd951a9
fix: mock fetch in SearchEditMultipleTagPageTest
OlimpiaZurek Oct 8, 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
1 change: 1 addition & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@
"EUVAT",
"elif",
"evals",
"EXIF",
"EXPENSIDEV",
"EXPENSIFYAPI",
"EXPENSIFYPDBUSINESS",
Expand Down
16 changes: 16 additions & 0 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,9 @@ const CONST = {
RECEIPT_CAMERA: {
PHOTO_WIDTH: 2880,
PHOTO_HEIGHT: 2160,
// iPhones offer no 4:3 photo size between 3 MP and 12 MP, so the 6 MP target above selects 2016x1512
IOS_STILL_WIDTH: 4032,
IOS_STILL_HEIGHT: 3024,
PHOTO_ASPECT_RATIO: 4 / 3,
},

Expand Down Expand Up @@ -2425,6 +2428,7 @@ const CONST = {
SPAN_SHUTTER_TO_CONFIRMATION: 'ManualShutterToConfirmation',
SPAN_RECEIPT_CAPTURE: 'ManualReceiptCapture',
SPAN_RECEIPT_PREPARE: 'ManualReceiptPrepare',
SPAN_RECEIPT_UPGRADE: 'ManualReceiptUpgrade',
SPAN_SCAN_PROCESS_AND_NAVIGATE: 'ManualScanProcessAndNavigate',
SPAN_CONFIRMATION_MOUNT: 'ManualConfirmationMount',
SPAN_CONFIRMATION_LIST_READY: 'ManualConfirmationListReady',
Expand Down Expand Up @@ -2535,6 +2539,8 @@ const CONST = {
ATTRIBUTE_IS_MULTI_SCAN: 'is_multi_scan',
ATTRIBUTE_CAPTURE_METHOD: 'capture_method',
ATTRIBUTE_FLASH_USED: 'flash_used',
ATTRIBUTE_UPGRADE_ATTEMPTED: 'upgrade_attempted',
ATTRIBUTE_UPGRADE_OUTCOME: 'upgrade_outcome',
ATTRIBUTE_PHOTO_WIDTH: 'photo_width',
ATTRIBUTE_PHOTO_HEIGHT: 'photo_height',
ATTRIBUTE_SOURCE: 'source',
Expand All @@ -2550,6 +2556,16 @@ const CONST = {
PHOTO: 'photo',
SNAPSHOT: 'snapshot',
},
UPGRADE_OUTCOME: {
UPGRADED: 'upgraded',
CAPTURE_FAILED: 'capture_failed',
CAPTURE_TIMED_OUT: 'capture_timed_out',
ROTATE_TIMED_OUT: 'rotate_timed_out',
ROTATE_FAILED: 'rotate_failed',
CLAIMED_FOR_UPLOAD: 'claimed_for_upload',
SWAP_FAILED: 'swap_failed',
STACKED_CAPTURE_SKIPPED: 'stacked_capture_skipped',
},
SPAN_PLATFORM: {
NATIVE: 'native',
WEB: 'web',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import useLocalReceiptThumbnail from '@hooks/useLocalReceiptThumbnail';
import useReceiptUpgradeCount from '@hooks/useReceiptUpgradeCount';

import type {ThumbnailAndImageURI} from '@libs/ReceiptUtils';
import {getThumbnailAndImageURIs} from '@libs/ReceiptUtils';
Expand Down Expand Up @@ -42,10 +43,16 @@ function useReceiptThumbnailSource({transaction, receiptPath, receiptFilename}:
// (react-hooks/refs). React handles this synchronously before painting, so there is no visible
// double-render or layout thrash.
const resolvedReceiptImageStr = resolvedReceiptImage != null ? String(resolvedReceiptImage) : undefined;
const [initialLocalSource, setInitialLocalSource] = useState<{source: string | undefined; resolvedImage: string | undefined}>({source: undefined, resolvedImage: undefined});
if (isLocalFile && (initialLocalSource.source === undefined || initialLocalSource.resolvedImage !== resolvedReceiptImageStr)) {

const upgradeCount = useReceiptUpgradeCount(resolvedReceiptImageStr);
const [initialLocalSource, setInitialLocalSource] = useState<{source: string | undefined; resolvedImage: string | undefined; upgradeCount: number}>({
source: undefined,
resolvedImage: undefined,
upgradeCount: 0,
});
if (isLocalFile && (initialLocalSource.source === undefined || initialLocalSource.resolvedImage !== resolvedReceiptImageStr || initialLocalSource.upgradeCount !== upgradeCount)) {
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- empty string fallback is intentional: we want to skip the empty-string case in the OR chain, not treat it as a valid source
setInitialLocalSource({source: thumbnailUri || resolvedReceiptImageStr || '', resolvedImage: resolvedReceiptImageStr});
setInitialLocalSource({source: thumbnailUri || resolvedReceiptImageStr || '', resolvedImage: resolvedReceiptImageStr, upgradeCount});
}

// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- empty string fallback is intentional: we want to skip the empty-string case in the OR chain, not treat it as a valid source
Expand Down
7 changes: 6 additions & 1 deletion src/hooks/useLocalReceiptThumbnail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import {generateThumbnail} from '@pages/iou/request/step/IOURequestStepScan/crop

import {useEffect, useRef, useState, useTransition} from 'react';

import useReceiptUpgradeCount from './useReceiptUpgradeCount';

const thumbnailCache = new Map<string, string>();
/** Track how many mounted hook instances reference each sourceUri */
const thumbnailRefCount = new Map<string, number>();
Expand Down Expand Up @@ -42,10 +44,13 @@ function useLocalReceiptThumbnail(sourceUri: string | undefined, isLocalFile: bo
const [, startTransition] = useTransition();
const retainedUriRef = useRef<string | undefined>(undefined);

const upgradeCount = useReceiptUpgradeCount(sourceUri);

// Resolve cached thumbnails synchronously during render (fast path)
const cachedUri = sourceUri ? thumbnailCache.get(sourceUri) : undefined;
const resultForCurrentSource = asyncResult?.source === sourceUri ? asyncResult : undefined;
const thumbnailUri = cachedUri ?? resultForCurrentSource?.uri;
const resolvedUri = cachedUri ?? resultForCurrentSource?.uri;
const thumbnailUri = resolvedUri && upgradeCount > 0 ? `${resolvedUri.split('#').at(0) ?? resolvedUri}#upgraded${upgradeCount}` : resolvedUri;

const shouldGenerate = !!sourceUri && isLocalFile && !cachedUri;
const isGenerating = shouldGenerate && !resultForCurrentSource?.done;
Expand Down
16 changes: 16 additions & 0 deletions src/hooks/useReceiptUpgradeCount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import {getUpgradeCount, subscribe} from '@libs/ReceiptStorage/receiptUpgrades';

import {useSyncExternalStore} from 'react';

function toUpgradeKey(sourceUri: unknown): string {
if (typeof sourceUri !== 'string') {
return '';
}
return sourceUri.split('/').pop()?.split('#').at(0)?.split('?').at(0) ?? '';
}

function useReceiptUpgradeCount(sourceUri: string | number | undefined): number {
return useSyncExternalStore(subscribe, () => getUpgradeCount(toUpgradeKey(sourceUri)));
}

export default useReceiptUpgradeCount;
Loading
Loading