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
Android - Expense - Email not found error is shown after submitting expense to new user #100777
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:#97874 Email or phone of affected tester (no customers):jsdoijnodsdisojo@gmail.com Issue reported by: Applause Internal Team Bug source: Exploratory - Significant User Experience Deterioration Device used: Samsung Galaxy S25 FE / Android 16 App Component: Workspace Settings
Action Performed:
Launch Expensify app.
Create a new account.
Open FAB > Create expense > Manual.
Enter email of non-existing user.
Enter amount and tap Create expense.
Expected Result:
Expense will be created without error.
Actual Result:
Email not found error is shown after submitting expense to new user.
👋 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: Unconfirmed — App-side exposure is confirmed, but the "Email not found" string is returned by the API, so where the fix lands is not yet proven Causing PR: #100215 - "feat: remove isNewManualExpenseFlowEnabled beta flag" by @thelullabyy (Medium confidence) Related Issues: none found — no other issue or PR in this repo mentions an "Email not found" error
Recommendation: NEEDS INVESTIGATION
#97874 was auto-assigned only because its URL was pasted into the report's "caught during regression testing" field, and verification below shows it does not plausibly cause this; the flow the repro actually exercises was switched on for everyone by #100215, which is the best explanation for staging-yes/production-no. The exact failing API call still needs to be captured before we can say whether the fix is App-side or backend-side.
Assigned: @thelullabyy (PR author), @ikevin127 and @puneetlath (approving reviewers) from the highest-confidence causing PR (#100215). Existing assignees left in place. Labels: No changes. DeployBlockerCash stays (the issue does not reproduce on production, and it is the only blocker label on the issue). DeployBlocker was never applied, so there is nothing to remove.
📋 Detailed Analysis
Evidence
1. #97874 is almost certainly not the cause.
The report's "If this was caught during regression testing, add the test name, ID and link" field contains https://github.com/Expensify/App/pull/97874. That is the QA link for the test the tester was running, not a diagnosis — and it is what Melvin keyed the auto-assignment off. On the merits, #97874 is a navigation pre-mount performance change. It does touch IOURequestStepConfirmation.tsx and useExpenseSubmission.ts, but it explicitly handles the brand-new-recipient case rather than breaking it: getReusableP2PReportID marks a recipient with no existing chat as an optimistic destination, and that flag is threaded through to the report route as isPendingCreation=true, which ReportFetchHandler uses to skip openReport for a client-generated reportID. It changes no participant field and no API payload.
2. The repro describes the new manual expense flow, which #100215 turned on for everyone.
The steps enter the recipient before the amount on a single confirmation screen. That is the new manual expense flow (participant picker owned by the confirmation page), not the legacy amount → participants → confirmation flow. #100215 deleted CONST.BETAS.NEW_MANUAL_EXPENSE_FLOW and every isNewManualExpenseFlowEnabled guard, including the one that previously wrapped <ParticipantPicker> inside IOURequestStepConfirmation and the one in MoneyRequestConfirmationList that used to navigate to the standalone participants step instead of opening the parent-owned picker.
3. This explains staging-only reproduction precisely.
Step 2 of the repro is "Create a new account." A brand-new account has no betas, so on production it still gets the legacy participants-step flow, which works. On staging the gate is gone, so the same brand-new account gets the new flow. That matches "Reproducible in staging: Yes / Reproducible in production: No" better than any other PR in this checklist.
feat: remove isNewManualExpenseFlowEnabled beta flag #100215 modifies the affected files: confirmed via gh pr view 100215 --json files — it edits MoneyRequestConfirmationList.tsx, IOURequestStepConfirmation.tsx, useDefaultParticipants.ts, DynamicIOURequestStepParticipants.tsx, IOURequestStartPage.tsx and ReportUtils.ts, and the recipient row in MoneyRequestConfirmationList now unconditionally calls onOpenParticipantPicker().
Both PRs shipped in this build:gh api repos/Expensify/App/compare/9.4.74-2-staging...9.4.75-0-staging lists both #100215 and #97874.
The error copy is not ours.Email not found (and any close variant) does not exist anywhere in src/languages/. The nearest strings are memberNotFound (Member not found.), which is not what the video shows. So the App is rendering an API error message verbatim, which is why the App diff alone cannot pin the root cause.
Root Cause
Confirmed so far: the recipient is now chosen through the confirmation page's own ParticipantPicker rather than the standalone participants step. For an email with no Expensify account, getUserToInviteOption synthesizes an option with a generated optimistic accountID and isOptimisticAccount: true; ParticipantSearchResults.sanitizedSelectedParticipant copies only a fixed field list onto the participant, and handleParticipantsAdded then generates a fresh optimistic reportID for the brand-new P2P recipient and writes it to the draft transaction. At submit, MoneyRequestBuilder derives the payer from participant.login.
Not yet confirmed: which of those hand-offs produces a payload the API rejects with "Email not found" — or whether the App payload is well-formed and the API is at fault. Nothing in the App diffs proves either way.
Concrete next steps to close this out:
Reproduce on staging with network logging and capture the failing command, its parameters (especially the participant's login/accountID) and the requestID from the response. That single data point decides App vs. backend and, if App, which hand-off drops the login.
I reproduced the issue reverting my PR locally, so it's super unlikely it is related. Not 100% reproducible, it depends on whether the metadata key is loaded before the chat screen mounts. Fresh account makes it near-deterministic.
I did some testing while checking if it's my fault, posting to speed up the fix:
RequestMoney succeeds. Chat report row is written to Onyx via SET.
App navigates to the new chat about 400 ms later. At mount, useOnyx for reportMetadata_<id> is still loading, so reportMetadata falls back to {isOptimisticReport: false}.
Guard at ReportFetchHandler.tsx:187 that skips openReport for optimistic chats fails. openReport fires with the client-generated accountID.
Backend OpenReport cannot map that accountID to a login, returns "Email not found" into errorFields.createChat. E.g. requestIDs a38d3f328e83c3ad-LAX and a38d4f7b3ae9ef99-SJC in Victoria.
The fix would likely be a few lines in ReportFetchHandler, do not call openReport while the useOnyx result status for reportMetadata_<id> is loading, or treat report.pendingFields?.createChat as optimistic since it arrives in the same SET as the report row
@MelvinBot please do a quick sanity check and what PR likely caused it
Sanity check: your mechanism is right about the outcome, wrong about the trigger — and this isn't a 9.4.75 regression
Steps 1, 3 and 4 hold. Step 2 does not — there is no loading race, and the fix you proposed wouldn't work.
RequestMoney's successData explicitly merges isOptimisticReport: false at MoneyRequestBuilder.ts:878. Since your own step 1 is "RequestMoney succeeds", by the time you navigate the value is legitimatelyfalse. The guard at ReportFetchHandler.tsx:187 doesn't fail to read a still-loading key — it correctly stops applying. Gating on useOnyx status would therefore change nothing, and pendingFields.createChat is cleared by the same successData.
The defaultReportMetadata pattern you spotted at ReportFetchHandler.tsx:69 is real — loading and loaded-but-empty are genuinely indistinguishable there, and worth fixing on its own merits. It just isn't what breaks this flow. Also worth noting: the report row and its metadata go out in the sameOnyx.update() batch (SET at L575, metadata MERGE at L666), so they can't tear.
What actually causes it
openReport passes the DM's participants through ReportUtils.getOneOnOneChatParticipants, which has no optimistic-account filter. The invited email has an optimistic personal detail with a login, so it passes, and the client-generated accountID ships as accountIDList at Report/index.ts:1791. The server can't map it and returns 666 "Email not found", which the App renders verbatim — the string exists nowhere in src/languages/.
Causing PR: #98855 ("include 1:1 DM participants in OpenReport so stale optimistic reportIDs self-heal", merged 2026-08-19, first shipped 9.4.57-0-staging). Before it, fetchReport sent no participants, so accountIDList was always empty on this path.
Your PR is clear, and your local revert result is expected
The line-187 guard predates you — it came from #86719 (9.3.51-0-staging). #97874 only added the isPendingCreationFromRoute and isPreMountedDraft skips at L179-L185, both of which make openReportless likely to fire. Reverting your PR leaves L187 intact, so reproducing after a revert is exactly what should happen. Your PR was auto-assigned only because its URL was in the report's "caught during regression testing" field.
This is a duplicate — the fix is already in review
#99276 — "Expense - New Email Not Found via Participant Option", filed 2026-08-23 against v9.4.59-0, "Reproducible in production?: Yes", currently Reviewing. Same repro, step for step.
Fix PR: #100596 by ahmdshrif, open since 2026-09-08 — a day before this issue was filed. It derives the same mechanism independently.
Recommendation: close this as a duplicate of #99276 and drop DeployBlockerCash. It reproduces in production at v9.4.59-0, 18 releases before 9.4.75, so it does not block this deploy. Review #100596 instead.
Correcting my earlier comment, and what I could not verify
My earlier comment named #100215 ("remove isNewManualExpenseFlowEnabled beta flag") as the top candidate. That was wrong. It touches confirmation-UI components and useDefaultParticipants, but no navigation timing, no ReportFetchHandler, no reportMetadata/isOptimisticReport. getOneOnOneChatParticipants was not touched anywhere in 9.4.74-2-staging...9.4.75-0-staging. The staging-only signal that pointed me there was itself wrong — #99276 records this reproducing in production.
I've removed thelullabyy, ikevin127 and puneetlath, whom I had assigned on that refuted theory.
Not verified: I had no VictoriaLogs access in this session, so I could not confirm your requestIDs a38d3f328e83c3ad-LAX and a38d4f7b3ae9ef99-SJC, the jsonCode, or the parameters sent. The accountIDList → 666 link above is empirically pinned in #99276's thread rather than by me. One detail in your step 4 is a contributor inference nobody has proven: that the string lands in report.errorFields.createChat specifically, rather than on a report action's errors. Not load-bearing for the duplicate verdict.
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/e6e3c4a66e5f1808eef59d28beb9436d4536fadc9eff3a1a39637c0a61d201ae
melvin-bot commentedon Sep 9, 2026
You have been assigned to this deploy blocker because you recently merged this PR: #97874
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
@mountiny @marufsharifi you have been assigned to this deploy blocker because you reviewed the offending PR: #97874
melvin-bot commentedon Sep 9, 2026
@JakubKorytko you have been assigned to this deploy blocker because you authored the offending PR: #97874
melvin-bot commentedon Sep 9, 2026
💬 A slack conversation has been started in #expensify-open-source
MelvinBot commentedon Sep 9, 2026
🔍 Investigation Summary
Classification: Unconfirmed — App-side exposure is confirmed, but the "Email not found" string is returned by the API, so where the fix lands is not yet proven
Causing PR: #100215 - "feat: remove isNewManualExpenseFlowEnabled beta flag" by @thelullabyy (Medium confidence)
Related Issues: none found — no other issue or PR in this repo mentions an "Email not found" error
Recommendation: NEEDS INVESTIGATION
#97874 was auto-assigned only because its URL was pasted into the report's "caught during regression testing" field, and verification below shows it does not plausibly cause this; the flow the repro actually exercises was switched on for everyone by #100215, which is the best explanation for staging-yes/production-no. The exact failing API call still needs to be captured before we can say whether the fix is App-side or backend-side.
Assigned: @thelullabyy (PR author), @ikevin127 and @puneetlath (approving reviewers) from the highest-confidence causing PR (#100215). Existing assignees left in place.
Labels: No changes.
DeployBlockerCashstays (the issue does not reproduce on production, and it is the only blocker label on the issue).DeployBlockerwas never applied, so there is nothing to remove.📋 Detailed Analysis
Evidence
1. #97874 is almost certainly not the cause.
The report's "If this was caught during regression testing, add the test name, ID and link" field contains
https://github.com/Expensify/App/pull/97874. That is the QA link for the test the tester was running, not a diagnosis — and it is what Melvin keyed the auto-assignment off. On the merits, #97874 is a navigation pre-mount performance change. It does touchIOURequestStepConfirmation.tsxanduseExpenseSubmission.ts, but it explicitly handles the brand-new-recipient case rather than breaking it:getReusableP2PReportIDmarks a recipient with no existing chat as an optimistic destination, and that flag is threaded through to the report route asisPendingCreation=true, whichReportFetchHandleruses to skipopenReportfor a client-generated reportID. It changes no participant field and no API payload.2. The repro describes the new manual expense flow, which #100215 turned on for everyone.
The steps enter the recipient before the amount on a single confirmation screen. That is the new manual expense flow (participant picker owned by the confirmation page), not the legacy amount → participants → confirmation flow. #100215 deleted
CONST.BETAS.NEW_MANUAL_EXPENSE_FLOWand everyisNewManualExpenseFlowEnabledguard, including the one that previously wrapped<ParticipantPicker>insideIOURequestStepConfirmationand the one inMoneyRequestConfirmationListthat used to navigate to the standalone participants step instead of opening the parent-owned picker.3. This explains staging-only reproduction precisely.
Step 2 of the repro is "Create a new account." A brand-new account has no betas, so on production it still gets the legacy participants-step flow, which works. On staging the gate is gone, so the same brand-new account gets the new flow. That matches "Reproducible in staging: Yes / Reproducible in production: No" better than any other PR in this checklist.
4. Candidates considered and ruled out.
useSearchSelector) — touches the invite-option derivation, but it adds the invite option for whitespace-padded logins. It is a no-op for a cleanly typed email and cannot remove a participant.Onyx.connect()forPERSONAL_DETAILS_LIST) — plausible on the surface since it reworks optimistic personal details, but every touched site keeps anallPersonalDetailsfallback, so no participant login is dropped.MoneyRequestBuilder.ts/useExpenseSubmission.ts, but only for the new-transaction highlight rail and RHP width.Verification
src/pages/iou/request/step/IOURequestStepConfirmation.tsx,src/components/MoneyRequestConfirmationList.tsx,src/pages/iou/request/ParticipantSearchResults.tsx,src/libs/actions/IOU/MoneyRequestBuilder.ts.gh pr view 100215 --json files— it editsMoneyRequestConfirmationList.tsx,IOURequestStepConfirmation.tsx,useDefaultParticipants.ts,DynamicIOURequestStepParticipants.tsx,IOURequestStartPage.tsxandReportUtils.ts, and the recipient row inMoneyRequestConfirmationListnow unconditionally callsonOpenParticipantPicker().gh api repos/Expensify/App/compare/9.4.74-2-staging...9.4.75-0-staginglists both#100215and#97874.2026-09-09T16:33:46Z; the bug was filed2026-09-09T22:24:59Zagainst9.4.75-0.Email not found(and any close variant) does not exist anywhere insrc/languages/. The nearest strings arememberNotFound(Member not found.), which is not what the video shows. So the App is rendering an API error message verbatim, which is why the App diff alone cannot pin the root cause.Root Cause
Confirmed so far: the recipient is now chosen through the confirmation page's own
ParticipantPickerrather than the standalone participants step. For an email with no Expensify account,getUserToInviteOptionsynthesizes an option with a generated optimisticaccountIDandisOptimisticAccount: true;ParticipantSearchResults.sanitizedSelectedParticipantcopies only a fixed field list onto the participant, andhandleParticipantsAddedthen generates a fresh optimisticreportIDfor the brand-new P2P recipient and writes it to the draft transaction. At submit,MoneyRequestBuilderderives the payer fromparticipant.login.Not yet confirmed: which of those hand-offs produces a payload the API rejects with "Email not found" — or whether the App payload is well-formed and the API is at fault. Nothing in the App diffs proves either way.
Concrete next steps to close this out:
login/accountID) and therequestIDfrom the response. That single data point decides App vs. backend and, if App, which hand-off drops the login.JakubKorytko commentedon Sep 10, 2026
I reproduced the issue reverting my PR locally, so it's super unlikely it is related. Not 100% reproducible, it depends on whether the metadata key is loaded before the chat screen mounts. Fresh account makes it near-deterministic.
I did some testing while checking if it's my fault, posting to speed up the fix:
RequestMoneysucceeds. Chat report row is written to Onyx via SET.useOnyxforreportMetadata_<id>is still loading, soreportMetadatafalls back to{isOptimisticReport: false}.openReportfor optimistic chats fails.openReportfires with the client-generatedaccountID.OpenReportcannot map thataccountIDto a login, returns "Email not found" intoerrorFields.createChat. E.g. requestIDsa38d3f328e83c3ad-LAXanda38d4f7b3ae9ef99-SJCin Victoria.The fix would likely be a few lines in
ReportFetchHandler, do not callopenReportwhile theuseOnyxresult status forreportMetadata_<id>is loading, or treatreport.pendingFields?.createChatas optimistic since it arrives in the same SET as the report row@MelvinBot please do a quick sanity check and what PR likely caused it
MelvinBot commentedon Sep 10, 2026
Sanity check: your mechanism is right about the outcome, wrong about the trigger — and this isn't a 9.4.75 regression
Steps 1, 3 and 4 hold. Step 2 does not — there is no loading race, and the fix you proposed wouldn't work.
RequestMoney's successData explicitly mergesisOptimisticReport: falseatMoneyRequestBuilder.ts:878. Since your own step 1 is "RequestMoneysucceeds", by the time you navigate the value is legitimatelyfalse. The guard atReportFetchHandler.tsx:187doesn't fail to read a still-loading key — it correctly stops applying. Gating onuseOnyxstatus would therefore change nothing, andpendingFields.createChatis cleared by the same successData.The
defaultReportMetadatapattern you spotted atReportFetchHandler.tsx:69is real — loading and loaded-but-empty are genuinely indistinguishable there, and worth fixing on its own merits. It just isn't what breaks this flow. Also worth noting: the report row and its metadata go out in the sameOnyx.update()batch (SET at L575, metadata MERGE at L666), so they can't tear.What actually causes it
openReportpasses the DM's participants throughReportUtils.getOneOnOneChatParticipants, which has no optimistic-account filter. The invited email has an optimistic personal detail with a login, so it passes, and the client-generatedaccountIDships asaccountIDListatReport/index.ts:1791. The server can't map it and returns666 "Email not found", which the App renders verbatim — the string exists nowhere insrc/languages/.Causing PR: #98855 ("include 1:1 DM participants in OpenReport so stale optimistic reportIDs self-heal", merged 2026-08-19, first shipped 9.4.57-0-staging). Before it,
fetchReportsent no participants, soaccountIDListwas always empty on this path.Your PR is clear, and your local revert result is expected
The line-187 guard predates you — it came from #86719 (9.3.51-0-staging). #97874 only added the
isPendingCreationFromRouteandisPreMountedDraftskips at L179-L185, both of which makeopenReportless likely to fire. Reverting your PR leaves L187 intact, so reproducing after a revert is exactly what should happen. Your PR was auto-assigned only because its URL was in the report's "caught during regression testing" field.This is a duplicate — the fix is already in review
#99276 — "Expense - New Email Not Found via Participant Option", filed 2026-08-23 against v9.4.59-0, "Reproducible in production?: Yes", currently
Reviewing. Same repro, step for step.Fix PR: #100596 by
ahmdshrif, open since 2026-09-08 — a day before this issue was filed. It derives the same mechanism independently.Recommendation: close this as a duplicate of #99276 and drop
DeployBlockerCash. It reproduces in production at v9.4.59-0, 18 releases before 9.4.75, so it does not block this deploy. Review #100596 instead.Correcting my earlier comment, and what I could not verify
My earlier comment named #100215 ("remove isNewManualExpenseFlowEnabled beta flag") as the top candidate. That was wrong. It touches confirmation-UI components and
useDefaultParticipants, but no navigation timing, noReportFetchHandler, noreportMetadata/isOptimisticReport.getOneOnOneChatParticipantswas not touched anywhere in9.4.74-2-staging...9.4.75-0-staging. The staging-only signal that pointed me there was itself wrong — #99276 records this reproducing in production.I've removed
thelullabyy,ikevin127andpuneetlath, whom I had assigned on that refuted theory.Not verified: I had no VictoriaLogs access in this session, so I could not confirm your requestIDs a38d3f328e83c3ad-LAX and a38d4f7b3ae9ef99-SJC, the jsonCode, or the parameters sent. The
accountIDList→666link above is empirically pinned in #99276's thread rather than by me. One detail in your step 4 is a contributor inference nobody has proven: that the string lands inreport.errorFields.createChatspecifically, rather than on a report action'serrors. Not load-bearing for the duplicate verdict.view run · view recording
JakubKorytko commentedon Sep 10, 2026
Ah makes sense, so @mountiny I believe we can close this one
mountiny commentedon Sep 10, 2026
Dupe #99276