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
Web - Report - Drag multiple receipts later does not appear in the report #100698
Version Number: v9.4.74-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:#98847 Email or phone of affected tester (no customers):leungbratt6+09093@gmail.com Issue reported by: Applause Internal Team Bug source: Pull Request QA execution Device used: Windows 10/Chrome App Component: Chat Report View
Action Performed:
Open the New Dot app
Navigate to the Workspace chat
Create an empty report and drag a receipt into it
Drag multiple receipts into the same report again
Expected Result:
New receipts appear as expenses in the same report like in Prod
Actual Result:
The attached receipt for the first expense has been changed to another receipt
👋 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 Causing PR: None found in this release. #98847 - "fix: remove unsafe assertions from receipt and settings flows" by @KJ21-ENG is refuted (Low confidence — it does not touch this code path) Related Issues: Deploy checklist #100624
Recommendation: ROLL FORWARD
The behavior comes from the report composer's "replace receipt" mode, which is byte-identical between production and staging, so a revert of #98847 (or of anything else on the checklist) cannot fix it — a small targeted change in useReceiptDrop/useShouldAddOrReplaceReceipt is needed. Please also re-verify the "Reproducible in production: No" result, since no code in this path changed this release.
Assigned: No new assignees — no causing PR was identified at medium or higher confidence, so the existing #98847 assignment is not corroborated by code evidence. Labels: No changes. DeployBlockerCash kept (the fix is in Expensify/App); DeployBlocker was not present.
📋 Detailed Analysis
Evidence
#98847 is not on this code path. Its only receipt-related change is to src/hooks/useReceiptScanDrop.tsx, and that hook is imported by exactly three screens — src/pages/home/HomePage.tsx, src/pages/Search/SearchPageWide.tsx and src/pages/Search/SearchPageNarrow/index.tsx (via src/components/ReceiptScanDropZone). It is the global create drop handler. Dropping onto a report goes through ReportScreen → ReportActionCompose.DropZone (ComposerDropZone) → useReceiptDrop, a different hook that #98847 never touched.
Its actual diff there is also incapable of producing this symptom: it adds a files.every((file) => file instanceof Blob) type guard, an if (!initialTransaction) return; early exit, and replaces two as Partial<Transaction> casts. Nothing in it changes which transaction a receipt is attached to, and it always creates a brand-new draft per file exactly as before.
Nothing else in the release touches the report drop path either. I pulled the file list of all 51 App PRs on the checklist (#100624); none of them modify useReceiptDrop.ts, useShouldAddOrReplaceReceipt.ts, ComposerDropZone.tsx, DualDropZone.tsx, useFilesValidation.tsx or MoneyRequestReportUtils.ts. The blob SHAs for all six files are identical on production and staging (e.g. useShouldAddOrReplaceReceipt.ts is 9036d2c on both), so this path ships unchanged in v9.4.74. The useShouldAddOrReplaceReceipt logic itself has not been meaningfully edited since April/May 2026.
That makes this a pre-existing bug that surfaced during PR QA of #98847, not a regression from it. The "Reproducible in production: No" result is worth re-running, because the client code that produces the symptom is identical in production.
Verification
Affected files (permalinks at the #98847 merge commit 7a0eec8, identical on production):
Confirmation that #98847 does not modify any of these: its changed files are src/hooks/useReceiptScanDrop.tsx, src/libs/actions/BankAccounts.ts, src/pages/ReimbursementAccount/USD/*, src/pages/Search/SearchSavePage.tsx, src/pages/settings/Report/ReportDetailsColumnsPage.tsx, src/pages/workspace/accounting/certinia/* and one new test file.
Root Cause
The report composer switches its receipt drop zone into single-receipt replace mode as soon as the report contains exactly one expense:
Step 3 of the repro (drop one receipt into the empty report) creates that one expense. On step 4 the report now has reportTransactions.length === 1, so shouldAddOrReplaceReceipt flips to true (the drop zone label even changes from "Scan receipts" to "Add receipt"/"Replace receipt"). useReceiptDrop then takes the replace branch, which discards every dropped file except the first and overwrites the existing expense's receipt:
// useReceiptDrop.ts:117-124 (drop handler)if(shouldAddOrReplaceReceipt&&transactionID){constfile=files.at(0);
...
validateFiles([file],items);// only ONE file survivesreturn;}// useReceiptDrop.ts:57-69 (post-validation)if(shouldAddOrReplaceReceipt&&transactionID){replaceReceipt({transaction,file: files.at(0)asFile, source, ...});return;// no new transactions are ever created}
That is exactly the reported behavior: the first expense's receipt is swapped for one of the newly dropped receipts, and the remaining receipts never become expenses. The multi-file initMoneyRequest + buildOptimisticTransactionAndCreateDraft loop below it (useReceiptDrop.ts:72-95), which is what should create one draft transaction per dropped file, is never reached.
Suggested targeted fix — only treat a drop as a receipt replacement when a single file is dropped, unless we are genuinely in a transaction thread (where adding expenses is not an option). Return isTransactionThreadView from useShouldAddOrReplaceReceipt and gate both branches in useReceiptDrop:
then use shouldReplaceExistingReceipt in place of shouldAddOrReplaceReceipt && transactionID at both useReceiptDrop.ts:57 and useReceiptDrop.ts:117, so a multi-file drop on a one-expense report falls through to the "create one expense per receipt" loop.
I re-checked the three load-bearing claims from my investigation comment directly against the code. All three hold.
Verdict: don't revert. #98847 is not the cause, and this is not a v9.4.74 regression.
Root cause: once a report holds exactly one expense, the composer's drop zone flips into single-receipt replace mode. The next drop discards every file but the first and overwrites the existing expense's receipt instead of creating new expenses — exactly the reported symptom.
Recommended fix (roll forward): only replace when the drop is genuinely single-receipt. Return isTransactionThreadView from useShouldAddOrReplaceReceipt and gate both branches of useReceiptDrop on shouldAddOrReplaceReceipt && !!transactionID && (isTransactionThreadView || files.length === 1), so a multi-file drop on a one-expense report falls through to the create-per-receipt loop.
What I verified, and how
1. #98847 is not on this code path. Its only receipt change is to src/hooks/useReceiptScanDrop.tsx. Grepping every importer of that hook:
src/components/ReceiptScanDropZone/index.tsx → used only by:
src/pages/home/HomePage.tsx
src/pages/Search/SearchPageWide.tsx
src/pages/Search/SearchPageNarrow/index.tsx
No report screen imports it. The report drop instead runs ReportActionCompose → ComposerDropZone.tsx:121 → useReceiptDrop, a hook #98847 never touched.
2. The path is byte-identical on production and staging. Blob SHAs via the GitHub API:
File
production
staging
useReceiptDrop.ts
79c7e7b
79c7e7b
useShouldAddOrReplaceReceipt.ts
9036d2c
9036d2c
Nothing in this release changed the code that produces the symptom, so no revert on the checklist can fix it. Please re-run the "Reproducible in production: No" check — the client code is the same in production, so I'd expect it to reproduce there too.
3. The mechanism, read from the current files. The gate flips as soon as the report has one transaction — which step 3 of the repro creates:
Then the drop handler throws away every file except the first, and the post-validation branch overwrites the existing transaction's receipt and returns — so the initMoneyRequest + buildOptimisticTransactionAndCreateDraft loop that would create one expense per file is never reached:
I tried to confirm the behavior in a real browser session and could not, for two independent reasons:
Native OS file drag-and-drop is out of scope for the web automation tooling — there's no drag or upload primitive, and file drops require real File objects fed in from outside the page. Substituting the "+" / attach-receipt menu button would exercise a different code path, so I didn't fall back to it.
The browser session also never painted (blank page, 0 accessibility nodes) despite the dev server serving 200 for index.html and all three bundles.
So the analysis above rests on code evidence only — it has not been confirmed by a live reproduction.
Next Steps for Contributor+ team: To accept:@MelvinBot implement [this](https://github.com/Expensify/App/issues/100698#issuecomment-5600743069) to create a draft PR. To refine:@MelvinBot <your feedback> To reject: Explain why you are rejecting Melvin's proposal.
Classification: Frontend bug Causing PR: None found in this release. #98847 - "fix: remove unsafe assertions from receipt and settings flows" by @KJ21-ENG is refuted (Low confidence — it does not touch this code path) Related Issues: Deploy checklist #100624
Recommendation: ROLL FORWARD
The behavior comes from the report composer's "replace receipt" mode, which is byte-identical between production and staging, so a revert of #98847 (or of anything else on the checklist) cannot fix it — a small targeted change in useReceiptDrop/useShouldAddOrReplaceReceipt is needed. Please also re-verify the "Reproducible in production: No" result, since no code in this path changed this release.
Assigned: No new assignees — no causing PR was identified at medium or higher confidence, so the existing #98847 assignment is not corroborated by code evidence. Labels: No changes. DeployBlockerCash kept (the fix is in Expensify/App); DeployBlocker was not present.
Activity
applause-bot commentedon Sep 9, 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/5d7bd4abbfe8d5aa51ae2bde33c0ac2134a8aafa0dbea8dea430e9293d01b720
melvin-bot commentedon Sep 9, 2026
You have been assigned to this deploy blocker because you recently merged this PR: #98847
github-actions commentedon Sep 9, 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:
melvin-bot commentedon Sep 9, 2026
@blimpich @ikevin127 you have been assigned to this deploy blocker because you reviewed the offending PR: #98847
melvin-bot commentedon Sep 9, 2026
💬 A slack conversation has been started in #expensify-open-source
MelvinBot commentedon Sep 9, 2026
🔍 Investigation Summary
Classification: Frontend bug
Causing PR: None found in this release. #98847 - "fix: remove unsafe assertions from receipt and settings flows" by @KJ21-ENG is refuted (Low confidence — it does not touch this code path)
Related Issues: Deploy checklist #100624
Recommendation: ROLL FORWARD
The behavior comes from the report composer's "replace receipt" mode, which is byte-identical between
productionandstaging, so a revert of #98847 (or of anything else on the checklist) cannot fix it — a small targeted change inuseReceiptDrop/useShouldAddOrReplaceReceiptis needed. Please also re-verify the "Reproducible in production: No" result, since no code in this path changed this release.Assigned: No new assignees — no causing PR was identified at medium or higher confidence, so the existing #98847 assignment is not corroborated by code evidence.
Labels: No changes.
DeployBlockerCashkept (the fix is inExpensify/App);DeployBlockerwas not present.📋 Detailed Analysis
Evidence
#98847 is not on this code path. Its only receipt-related change is to
src/hooks/useReceiptScanDrop.tsx, and that hook is imported by exactly three screens —src/pages/home/HomePage.tsx,src/pages/Search/SearchPageWide.tsxandsrc/pages/Search/SearchPageNarrow/index.tsx(viasrc/components/ReceiptScanDropZone). It is the global create drop handler. Dropping onto a report goes throughReportScreen→ReportActionCompose.DropZone(ComposerDropZone) →useReceiptDrop, a different hook that #98847 never touched.Its actual diff there is also incapable of producing this symptom: it adds a
files.every((file) => file instanceof Blob)type guard, anif (!initialTransaction) return;early exit, and replaces twoas Partial<Transaction>casts. Nothing in it changes which transaction a receipt is attached to, and it always creates a brand-new draft per file exactly as before.Nothing else in the release touches the report drop path either. I pulled the file list of all 51 App PRs on the checklist (#100624); none of them modify
useReceiptDrop.ts,useShouldAddOrReplaceReceipt.ts,ComposerDropZone.tsx,DualDropZone.tsx,useFilesValidation.tsxorMoneyRequestReportUtils.ts. The blob SHAs for all six files are identical onproductionandstaging(e.g.useShouldAddOrReplaceReceipt.tsis9036d2con both), so this path ships unchanged in v9.4.74. TheuseShouldAddOrReplaceReceiptlogic itself has not been meaningfully edited since April/May 2026.That makes this a pre-existing bug that surfaced during PR QA of #98847, not a regression from it. The "Reproducible in production: No" result is worth re-running, because the client code that produces the symptom is identical in production.
Verification
Affected files (permalinks at the #98847 merge commit
7a0eec8, identical on production):src/pages/inbox/report/ReportActionCompose/useShouldAddOrReplaceReceipt.ts#L26-L41src/pages/inbox/report/ReportActionCompose/useReceiptDrop.ts#L52-L70and#L108-L128src/pages/inbox/report/ReportActionCompose/ComposerDropZone.tsx#L116-L126Confirmation that #98847 does not modify any of these: its changed files are
src/hooks/useReceiptScanDrop.tsx,src/libs/actions/BankAccounts.ts,src/pages/ReimbursementAccount/USD/*,src/pages/Search/SearchSavePage.tsx,src/pages/settings/Report/ReportDetailsColumnsPage.tsx,src/pages/workspace/accounting/certinia/*and one new test file.Root Cause
The report composer switches its receipt drop zone into single-receipt replace mode as soon as the report contains exactly one expense:
Step 3 of the repro (drop one receipt into the empty report) creates that one expense. On step 4 the report now has
reportTransactions.length === 1, soshouldAddOrReplaceReceiptflips totrue(the drop zone label even changes from "Scan receipts" to "Add receipt"/"Replace receipt").useReceiptDropthen takes the replace branch, which discards every dropped file except the first and overwrites the existing expense's receipt:That is exactly the reported behavior: the first expense's receipt is swapped for one of the newly dropped receipts, and the remaining receipts never become expenses. The multi-file
initMoneyRequest+buildOptimisticTransactionAndCreateDraftloop below it (useReceiptDrop.ts:72-95), which is what should create one draft transaction per dropped file, is never reached.Suggested targeted fix — only treat a drop as a receipt replacement when a single file is dropped, unless we are genuinely in a transaction thread (where adding expenses is not an option). Return
isTransactionThreadViewfromuseShouldAddOrReplaceReceiptand gate both branches inuseReceiptDrop:then use
shouldReplaceExistingReceiptin place ofshouldAddOrReplaceReceipt && transactionIDat bothuseReceiptDrop.ts:57anduseReceiptDrop.ts:117, so a multi-file drop on a one-expense report falls through to the "create one expense per receipt" loop.MelvinBot commentedon Sep 9, 2026
✅ Independent verification of the analysis above
I re-checked the three load-bearing claims from my investigation comment directly against the code. All three hold.
Verdict: don't revert. #98847 is not the cause, and this is not a v9.4.74 regression.
Root cause: once a report holds exactly one expense, the composer's drop zone flips into single-receipt replace mode. The next drop discards every file but the first and overwrites the existing expense's receipt instead of creating new expenses — exactly the reported symptom.
Recommended fix (roll forward): only replace when the drop is genuinely single-receipt. Return
isTransactionThreadViewfromuseShouldAddOrReplaceReceiptand gate both branches ofuseReceiptDroponshouldAddOrReplaceReceipt && !!transactionID && (isTransactionThreadView || files.length === 1), so a multi-file drop on a one-expense report falls through to the create-per-receipt loop.What I verified, and how
1. #98847 is not on this code path. Its only receipt change is to
src/hooks/useReceiptScanDrop.tsx. Grepping every importer of that hook:No report screen imports it. The report drop instead runs
ReportActionCompose→ComposerDropZone.tsx:121→useReceiptDrop, a hook #98847 never touched.2. The path is byte-identical on production and staging. Blob SHAs via the GitHub API:
productionstaginguseReceiptDrop.ts79c7e7b79c7e7buseShouldAddOrReplaceReceipt.ts9036d2c9036d2cNothing in this release changed the code that produces the symptom, so no revert on the checklist can fix it. Please re-run the "Reproducible in production: No" check — the client code is the same in production, so I'd expect it to reproduce there too.
3. The mechanism, read from the current files. The gate flips as soon as the report has one transaction — which step 3 of the repro creates:
useShouldAddOrReplaceReceipt.ts:34-41Then the drop handler throws away every file except the first, and the post-validation branch overwrites the existing transaction's receipt and returns — so the
initMoneyRequest+buildOptimisticTransactionAndCreateDraftloop that would create one expense per file is never reached:useReceiptDrop.ts:117-124—validateFiles([file], items); return;useReceiptDrop.ts:57-70—replaceReceipt({transaction, file: files.at(0) as File, ...}); return;Live repro was attempted but could not be run
I tried to confirm the behavior in a real browser session and could not, for two independent reasons:
Fileobjects fed in from outside the page. Substituting the "+" / attach-receipt menu button would exercise a different code path, so I didn't fall back to it.200forindex.htmland all three bundles.So the analysis above rests on code evidence only — it has not been confirmed by a live reproduction.
Next Steps for Contributor+ team:
To accept:
@MelvinBot implement [this](https://github.com/Expensify/App/issues/100698#issuecomment-5600743069)to create a draft PR.To refine:
@MelvinBot <your feedback>To reject: Explain why you are rejecting Melvin's proposal.
view run · view recording
KJ21-ENG commentedon Sep 9, 2026
It's not caused by #98847
cc: @blimpich @ikevin127
m-natarajan commentedon Sep 9, 2026
Not repro in latest staging v9.4.74-1
bandicam.2026-09-09.17-41-40-777.mp4