You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Due for payment 2026-10-08] Web - Scan - Receipt preview is blank in expense details page #100786
Version Number: 9.4.75-0 Reproducible in staging?: Yes Reproducible in production?: No If this was caught during regression testing, add the test name, ID and link from BrowserStack: Exploratory Email or phone of affected tester (no customers):jose.sat94+10jul@gmail.com Issue reported by: Applause Internal Team Bug source: Exploratory - Significant User Experience Deterioration Device used: MacBook Pro/13.7.8 App Component: Other
👋 Friendly reminder that deploy blockers are time-sensitive ⏱ issues! Check out the open `StagingDeployCash` deploy checklist to see the list of PRs included in this release, then work quickly to do one of the following:
Identify the pull request that introduced this issue and revert it.
Classification: Frontend bug (App-side; nothing points at the API) Causing PR: Not identified with confidence — see ranked candidates below (all Low confidence) Related Issues: #100770, #100763, #100761, #100757 (same checklist, same suspect area for the top candidate)
Recommendation: NEEDS INVESTIGATION
I traced the exact code path that makes a receipt render blank only after a page refresh on web, but a direct production...staging diff shows every file on that path changed only in comments in this release, so I can't honestly pin a causing PR yet. Keeping DeployBlockerCash and asking for a reproduction with the failing image URL before anything is reverted.
Labels: No changes. DeployBlockerCash kept (staging-only regression in an App release, so the fix belongs in Expensify/App). DeployBlocker was not present, so nothing to remove.
📋 Detailed Analysis
Evidence
The mechanism that produces a blank receipt after refresh (web only)
For a Scan request the optimistic transaction stores the local receipt URI a second time under receipt.localSource:
ReportActionItemImage then prefers localSource over the server-resolved receipt URL, and the preferred value is also what suppresses the server thumbnail:
A blob: URL is revoked when the document is destroyed, so after a refresh that URI resolves to nothing and the image renders blank. On native this does not happen because ReceiptStorage (native) adopts the file into a durable receipts folder and re-derives a live file:// path, which is consistent with the issue being reported only for Chrome/Safari desktop.
MoneyRequestReceiptView also flips its internal isLoading back on whenever receipt.localSource ?? receipt.source changes and only clears it on a successful image load, so a dead source leaves the area empty rather than falling back:
This whole path is byte-identical between production and staging apart from comment deletions. I diffed the refs directly rather than trusting the (truncated) compare API:
git diff production staging over ReceiptUtils.ts, ReceiptStorage/, ReceiptImage/, ThumbnailImage.tsx, ImageWithSizeCalculation.tsx, Image/, ReportActionItemImage.tsx, MoneyRequestView.tsx, MoneyRequestReceiptView.tsx, EReceiptThumbnail.tsx, tryResolveUrlFromApiRoot.ts → comment-only, plus one unrelated distance-rate guard in MoneyRequestView.tsx.
git diff -G'localSource|ReceiptStorage|blob:|isLocalFile' production staging -- src → no matches at all.
So the trigger is most likely a timing / data-refresh change (something that used to replace the optimistic transaction row with server data before the receipt is first painted after a cold load), not a change to the rendering code itself.
Verification
Ruled out by reading the diffs:
[No QA] Remove comments that do not add context #100576 "[No QA] Remove comments that do not add context" — 877 files, 5031 deletions, and it touches ReceiptImage/index.tsx, ThumbnailImage.tsx, ReportActionItemImage.tsx, ImageWithSizeCalculation.tsx, EReceiptThumbnail.tsx. I read every one of those patches: they are pure JSDoc deletions plus one /* */ → /** */ conversion. Not the cause.
Fix green plus icon alignment in receipt empty state #100249 "Fix green plus icon alignment in receipt empty state" — single line, only changes the + badge size in the empty state; styles.moneyRequestAttachReceiptThumbnailIcon.borderWidth is defined (2), so no NaN size. Not the cause.
[Performance] Pre-mount new expense destinations behind confirmation RHP #97874 "[Performance] Pre-mount new expense destinations behind confirmation RHP" by @JakubKorytko — the one PR here that adds new cold-start behaviour (cleanupPreMountedDraftReports() in src/setup/index.ts) and changes which destination report an expense is submitted into. "Only after refresh" is exactly the window it touches.
[No QA] Bump Onyx to 3.0.111 #100681 / OnyxUpdates gap-detection change (new lastUpdateIDPendingPusherApply in src/libs/actions/OnyxUpdates.ts) — could suppress a reconnect refetch that previously overwrote the transaction row with server data.
Root Cause
Underlying defect (independent of which PR exposed it): on web the app persists a blob: object URL into transaction.receipt.localSource and then unconditionally prefers it over the server receipt URL in ReportActionItemImage. blob: URLs do not survive a page reload, so any code path that leaves localSource in Onyx after the reload renders an empty receipt. Web has no durable storage behind ReceiptStorage.resolve, so nothing re-validates the URI before it is used.
What would confirm the trigger fastest: reproduce on staging, then in the console inspect the transaction (transactions_<transactionID>) after the refresh. If receipt.localSource is still a blob:... URL, the fix is to stop preferring a stale localSource on web (validate it, or clear it once the server returns a receipt source/filename) — a roll-forward, not a revert. If localSource is absent and receipt.source is already the expensify.com/receipts/... URL, then this is a layout/sizing regression and candidate 1 becomes the prime suspect.
No causing PR could be pinned. Every file on the receipt-render path is byte-identical between production and staging in this release apart from comment deletions, so this is most likely a timing regression (something that used to replace the optimistic transaction row with server data on cold load), not a change to the rendering code. Full evidence is in the detailed analysis above.
Most likely mechanism (high confidence, needs one console check to confirm): on web, a Scan expense stores the local blob: URL in transaction.receipt.localSource (MoneyRequestBuilder.ts:432-437), and ReportActionItemImage prefers localSource over the server URL and suppresses the server thumbnail fallback (ReportActionItemImage.tsx:173-181). A blob: URL dies with the document, so after a refresh it resolves to nothing and nothing falls back. Native is unaffected because ReceiptStorage adopts the file into a durable file:// path (ReceiptStorage/index.ts:1-10) — which matches this being reported on desktop Chrome/Safari only.
One check to settle it. Reproduce, refresh, then in the console:
If localSource still holds a blob: URL after reload, the fix is to stop unconditionally preferring localSource on web — validate it, or clear it once the server returns a receipt source/filename.
Revert vs fix: roll forward. There's no clear revert target.
Automated repro attempt: blocked. I could not reproduce this in the automated web session — attaching a receipt file is out of scope for the browser automation (the Scan tab's Choose files control opens a native file picker the harness can't drive), so the expense could never be created. This needs a human repro or a mobile-path run.
The OnyxUpdates gap-detection change (new lastUpdateIDPendingPusherApply) — could suppress a reconnect refetch that previously overwrote the transaction row.
Diffs run:git diff production staging over ReceiptUtils.ts, ReceiptStorage/, ReceiptImage/, ThumbnailImage.tsx, ImageWithSizeCalculation.tsx, Image/, ReportActionItemImage.tsx, MoneyRequestView.tsx, MoneyRequestReceiptView.tsx, EReceiptThumbnail.tsx, tryResolveUrlFromApiRoot.ts → comment-only. git diff -G'localSource|ReceiptStorage|blob:|isLocalFile' production staging -- src → no matches.
Next Steps: Reply with @MelvinBot <your feedback> to refine this analysis.
Activity
applause-bot commentedon Sep 10, 2026
While Applause doesn't embed attachments for this bug, here are the links for quick view:
https://platform.applause.com/services/links/v1/external/8872319e55b68933632ec1d1728b4f0c8f077a94d4c7e2f8d6364924d24a8987
melvin-bot commentedon Sep 10, 2026
Triggered auto assignment to @mountiny (
DeployBlockerCash), see https://stackoverflowteams.com/c/expensify/questions/9980/ for more details.melvin-bot commentedon Sep 10, 2026
💬 A slack conversation has been started in #expensify-open-source
github-actions commentedon Sep 10, 2026
👋 Friendly reminder that deploy blockers are time-sensitive ⏱ issues! Check out the open `StagingDeployCash` deploy checklist to see the list of PRs included in this release, then work quickly to do one of the following:
MelvinBot commentedon Sep 10, 2026
🔍 Investigation Summary
Classification: Frontend bug (App-side; nothing points at the API)
Causing PR: Not identified with confidence — see ranked candidates below (all Low confidence)
Related Issues: #100770, #100763, #100761, #100757 (same checklist, same suspect area for the top candidate)
Recommendation: NEEDS INVESTIGATION
I traced the exact code path that makes a receipt render blank only after a page refresh on web, but a direct
production...stagingdiff shows every file on that path changed only in comments in this release, so I can't honestly pin a causing PR yet. KeepingDeployBlockerCashand asking for a reproduction with the failing image URL before anything is reverted.Labels: No changes.
DeployBlockerCashkept (staging-only regression in an App release, so the fix belongs inExpensify/App).DeployBlockerwas not present, so nothing to remove.📋 Detailed Analysis
Evidence
The mechanism that produces a blank receipt after refresh (web only)
For a Scan request the optimistic transaction stores the local receipt URI a second time under
receipt.localSource:App/src/libs/actions/IOU/MoneyRequestBuilder.ts
Lines 432 to 437 in 2b61335
isLocalFile()countsblob:as local, so on weblocalSourceis ablob:URL:App/src/libs/fileDownload/FileUtils.ts
Lines 539 to 544 in 2b61335
ReportActionItemImagethen preferslocalSourceover the server-resolved receipt URL, and the preferred value is also what suppresses the server thumbnail:App/src/components/ReportActionItem/ReportActionItemImage.tsx
Lines 173 to 181 in 2b61335
On web
ReceiptStorage.resolveis a pass-through — there is no filesystem to adopt the file into, so theblob:URL is handed back verbatim:App/src/libs/ReceiptStorage/index.ts
Lines 1 to 10 in 2b61335
A
blob:URL is revoked when the document is destroyed, so after a refresh that URI resolves to nothing and the image renders blank. On native this does not happen becauseReceiptStorage(native) adopts the file into a durable receipts folder and re-derives a livefile://path, which is consistent with the issue being reported only for Chrome/Safari desktop.MoneyRequestReceiptViewalso flips its internalisLoadingback on wheneverreceipt.localSource ?? receipt.sourcechanges and only clears it on a successful image load, so a dead source leaves the area empty rather than falling back:App/src/components/ReportActionItem/MoneyRequestReceiptView.tsx
Lines 225 to 233 in 2b61335
Why I still can't name the PR
This whole path is byte-identical between
productionandstagingapart from comment deletions. I diffed the refs directly rather than trusting the (truncated) compare API:git diff production stagingoverReceiptUtils.ts,ReceiptStorage/,ReceiptImage/,ThumbnailImage.tsx,ImageWithSizeCalculation.tsx,Image/,ReportActionItemImage.tsx,MoneyRequestView.tsx,MoneyRequestReceiptView.tsx,EReceiptThumbnail.tsx,tryResolveUrlFromApiRoot.ts→ comment-only, plus one unrelated distance-rate guard inMoneyRequestView.tsx.git diff -G'localSource|ReceiptStorage|blob:|isLocalFile' production staging -- src→ no matches at all.So the trigger is most likely a timing / data-refresh change (something that used to replace the optimistic transaction row with server data before the receipt is first painted after a cold load), not a change to the rendering code itself.
Verification
Ruled out by reading the diffs:
ReceiptImage/index.tsx,ThumbnailImage.tsx,ReportActionItemImage.tsx,ImageWithSizeCalculation.tsx,EReceiptThumbnail.tsx. I read every one of those patches: they are pure JSDoc deletions plus one/* */→/** */conversion. Not the cause.+badge size in the empty state;styles.moneyRequestAttachReceiptThumbnailIcon.borderWidthis defined (2), so noNaNsize. Not the cause.3.0.110→3.0.111— the only diff is a new, explicitly inertOnyxSubscriptionManager. Unlikely.Ranked candidates, all Low confidence, listed only as starting points:
useResponsiveLayoutOnWideRHPto auseSyncExternalStoreover a mutable store and reworkedgetVisibleRHPRouteKeysaround aseenRouteKeysset. It already looks responsible for several sibling blockers in this checklist ([Due for payment 2026-09-17] Expense - RHP transition animates outside RHP #100770, [Due for payment 2026-09-17] Expense - New expense preview is highlighted every time opening and closing the report #100763, [Due for payment 2026-09-17] Expense - Second expense is highlighted when opening the duplicated report #100761, [Due for payment 2026-09-17] Expense - Expense previews are not highlighted after splitting expense #100757), and the expense details receipt panel is sized off the RHP width model. I could not connect it to a blank image specifically.cleanupPreMountedDraftReports()insrc/setup/index.ts) and changes which destination report an expense is submitted into. "Only after refresh" is exactly the window it touches.lastUpdateIDPendingPusherApplyinsrc/libs/actions/OnyxUpdates.ts) — could suppress a reconnect refetch that previously overwrote the transaction row with server data.Root Cause
Underlying defect (independent of which PR exposed it): on web the app persists a
blob:object URL intotransaction.receipt.localSourceand then unconditionally prefers it over the server receipt URL inReportActionItemImage.blob:URLs do not survive a page reload, so any code path that leaveslocalSourcein Onyx after the reload renders an empty receipt. Web has no durable storage behindReceiptStorage.resolve, so nothing re-validates the URI before it is used.What would confirm the trigger fastest: reproduce on staging, then in the console inspect the transaction (
transactions_<transactionID>) after the refresh. Ifreceipt.localSourceis still ablob:...URL, the fix is to stop preferring a stalelocalSourceon web (validate it, or clear it once the server returns a receiptsource/filename) — a roll-forward, not a revert. IflocalSourceis absent andreceipt.sourceis already theexpensify.com/receipts/...URL, then this is a layout/sizing regression and candidate 1 becomes the prime suspect.MelvinBot commentedon Sep 10, 2026
🤖 Deploy blocker investigation — follow-up
No causing PR could be pinned. Every file on the receipt-render path is byte-identical between
productionandstagingin this release apart from comment deletions, so this is most likely a timing regression (something that used to replace the optimistic transaction row with server data on cold load), not a change to the rendering code. Full evidence is in the detailed analysis above.Most likely mechanism (high confidence, needs one console check to confirm): on web, a Scan expense stores the local
blob:URL intransaction.receipt.localSource(MoneyRequestBuilder.ts:432-437), andReportActionItemImagepreferslocalSourceover the server URL and suppresses the server thumbnail fallback (ReportActionItemImage.tsx:173-181). Ablob:URL dies with the document, so after a refresh it resolves to nothing and nothing falls back. Native is unaffected becauseReceiptStorageadopts the file into a durablefile://path (ReceiptStorage/index.ts:1-10) — which matches this being reported on desktop Chrome/Safari only.One check to settle it. Reproduce, refresh, then in the console:
If
localSourcestill holds ablob:URL after reload, the fix is to stop unconditionally preferringlocalSourceon web — validate it, or clear it once the server returns a receiptsource/filename.Revert vs fix: roll forward. There's no clear revert target.
Automated repro attempt: blocked. I could not reproduce this in the automated web session — attaching a receipt file is out of scope for the browser automation (the Scan tab's
Choose filescontrol opens a native file picker the harness can't drive), so the expense could never be created. This needs a human repro or a mobile-path run.Candidates considered
Ruled out by reading the diffs:
borderWidthis2(noNaN).3.0.110→3.0.111— only adds an explicitly inertOnyxSubscriptionManager.Ranked candidates, all low confidence:
lastUpdateIDPendingPusherApply) — could suppress a reconnect refetch that previously overwrote the transaction row.Diffs run:
git diff production stagingoverReceiptUtils.ts,ReceiptStorage/,ReceiptImage/,ThumbnailImage.tsx,ImageWithSizeCalculation.tsx,Image/,ReportActionItemImage.tsx,MoneyRequestView.tsx,MoneyRequestReceiptView.tsx,EReceiptThumbnail.tsx,tryResolveUrlFromApiRoot.ts→ comment-only.git diff -G'localSource|ReceiptStorage|blob:|isLocalFile' production staging -- src→ no matches.Next Steps: Reply with
@MelvinBot <your feedback>to refine this analysis.view run · no recording available
melvin-bot commentedon Sep 10, 2026
Triggered auto assignment to Contributor-plus team member for initial proposal review - @ahmedGaber93 (
External)33 remaining items
mountiny commentedon Oct 4, 2026
I feel like we don't need such regression test
melvin-bot commentedon Oct 4, 2026
Triggered auto assignment to @mallenexpensify (
Awaiting Payment)melvin-bot commentedon Oct 7, 2026
Payment Summary
Resolving PRs:
fix: Receipt preview is blank in expense details page #101404
Reviewer: @mkhutornyi owed $175 via NewDot
Reviewer: @nkdengineer owed $175 via NewDot
BugZero Checklist (@mallenexpensify)
mallenexpensify commentedon Oct 13, 2026
@mkhutornyi who is due what here? I see a lot of mentions of deploy blockers. Thx
mkhutornyi commentedon Oct 13, 2026
This was false positive. The bug existed in production, not actually deploy blocker.
Contributor:
@nkdengineer, Contributor+:@mkhutornyiOur PR didn't cause any regressions.
mallenexpensify commentedon Oct 14, 2026
Payment Summary
Contributor: @nkdengineer due $250 via NewDot
Contributor+: @mkhutornyi due $250 via NewDot
trjExpensify commentedon Oct 15, 2026
approved $250 for @mkhutornyi