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-27] Deeplink - New user is not navigated to Concierge chat after deeplinking to Concierge #99940
Version Number: v9.4.64-2 Reproducible in staging?: Yes Reproducible in production?: Yes If this was caught during regression testing, add the test name, ID and link from BrowserStack:https://test-management.browserstack.com/projects/2219752/folder/13176964/test-cases/41237739 Email or phone of affected tester (no customers):htad26+werweuiy@gmail.com Issue reported by: Applause Internal Team Bug source: Regression TC Execution Device used: Windows 10/Chrome App Component: Other
The /concierge deeplink is captured correctly, then deliberately thrown away because the account had not finished onboarding when the link was captured.
openReportFromDeepLink parks the pending route behind sign-in, latching whether onboarding was already complete at capture time (src/libs/actions/Link.ts:562). For a brand-new sign-up that value is false, and when the deeplink finally replays it hits an unconditional early return (src/libs/actions/Link.ts:600-604):
// Drop a deep link captured before onboarding finished: navigateAfterOnboarding owns the// post-onboarding destination and overrides it anyway, so honoring it only risks the flash (#91437).if(initialHasCompletedGuidedSetupFlow===false){return;}
That comment's premise is wrong. navigateAfterOnboarding has no access to the deeplink — with no admins chat on a wide screen it goes straight to ROUTES.HOME (src/libs/navigateAfterOnboarding.ts:109-114). So nothing "overrides" the destination; it is simply lost, and because the guard sits after Onyx.disconnect(connection) (Link.ts:582) there is no retry.
This is why existing accounts work: their latched value is true, so the guard is skipped and Navigation.navigate('concierge') fires (Link.ts:628).
The guard was added by #92839 (merged 2026-06-25) to fix the post-onboarding "Not here" flash in #91437.
Note this is not specific to Concierge — any deeplink captured before a fresh sign-up is dropped the same way. ConciergePage itself is fine; it correctly waits on onServerDataReady() and creates the DM when conciergeReportID is missing (ConciergePage.tsx:35-49). It is simply never mounted.
What changes do you think we should make in order to solve the problem?
Stop discarding the pending route and instead hand it to the code that owns the post-onboarding destination, which is what the comment already assumes happens.
Replace the blanket return at Link.ts:600-604 with a persisted "pending post-onboarding route" (a new Onyx key), rather than dropping the route.
Also revisit the earlier drop at Link.ts:587-590. On web this branch fires first, because the replay gate opens on completeOnboarding's optimistic NVP merge while the onboarding modal is still focused (dismissOnboardingModalBeforeExit is a no-op on web). It currently discards the link and sets a spurious onboarding.purpose.errorBackButton message. It should persist the pending route too.
Because SCREENS.CONCIERGE is in MODAL_ROUTES_TO_DISMISS (GetStateForActionHandlers.ts:56), the replay must happen after onboarding tears down its modal, not during it — otherwise RootStackRouter strips the route out of the stack again.
What alternative solutions did you explore? (Optional)
Narrow the guard to an allow-list (let ROUTES.CONCIERGE through). Smaller change, but it only fixes Concierge and leaves every other deeplink broken for new users.
Investigation details, alternatives ruled out, and test evidence
Ordered path for a new sign-up (verified against d178dd2):
Signed out at /concierge → DeepLinkHandler reads Linking.getInitialURL() with hasAuthToken() === false and calls openReportFromDeepLink(..., isAuthenticated=false).
route = 'concierge'; reportID is empty (not an /r/ link) and concierge is not in PUBLIC_SCREENS_ROUTES, so it survives the early bail-outs and parks in waitForUserSignIn() (Link.ts:525). A JS closure, not the URL or navigation state, is the sole carrier.
Sign-up completes → NVP_ONBOARDING arrives as {hasCompletedGuidedSetupFlow: false} → latched at Link.ts:562, and the replay gate at Link.ts:648 stays shut for the whole onboarding flow.
Onboarding finishes → optimistic merge flips the NVP to true → gate opens → handleDeeplinkNavigation() disconnects, then returns at :587 (web) or :602. Link destroyed, no retry.
navigateAfterOnboarding → Navigation.navigate(ROUTES.HOME). Home rendered.
Ruled out with evidence: a CONCIERGE_REPORT_ID load race (ConciergePage handles the missing ID and retries via its focus effect); NavigationRoot.initialState (bails at !account on a signed-out cold start and has [] deps, so it never recomputes after sign-in); LAST_VISITED_PATH (only consumed when already authenticated on first render); AuthScreensInitHandler and setUpPoliciesAndNavigate (both no-op — the former only handles /r/<reportID> links, the latter requires an exitTo param).
Remaining uncertainty (does not change the root cause): whether the backend ever returns an empty nvp_onboarding before {hasCompletedGuidedSetupFlow: false}. If it does, hasCompletedGuidedSetupFlowSelector({}) returns true, the navigate does fire, and the loss shifts to the OnboardingGuard redirect + RootStackRouter stack rebuild instead. Both paths end on Home, and the fix above covers both.
Test evidence: a web tester run could not exercise this flow. The already-authenticated CI session has no way to perform a cold URL load or reload (both are disallowed for that session, since a reload can drop client auth into an unclearable magic-code wall), and clicking through the LHN would only exercise client-side routing, not a deeplink. The analysis above rests on code reading, not a live repro.
Step
Status
Observation
Session reaches Home before driving task
pass
Home/Inbox/Spend/Workspaces tabs and Concierge composer visible after dismissing the welcome screen
Navigate browser directly to /concierge URL
fail
No permitted agent-device verb performs a cold URL navigation for this session
Reload while on /concierge and observe landing screen
fail
Reload disallowed — risks dropping client auth
Evidence (1)
App at Home after completing prestart setup
Possibly related (different flow, same theme of a post-sign-in destination being lost):#96346 and #95445, both about public rooms not opening after an anonymous user signs in. Neither passes through the initialHasCompletedGuidedSetupFlow guard, so I do not think they share this root cause.
Next Steps for Contributor+ team: To accept:@MelvinBot implement this proposal to create a draft PR. To refine:@MelvinBot <your feedback> To reject: Explain why you are rejecting Melvin's proposal.
[Contributor] The offending PR and associated issue have been commented on, pointing out the bug it caused and why, so the author and reviewers can learn from the mistake.
Link to the comment on the PR: NA. I believe this is a new feature because deeplink only works for existing users.
Link to the comment on the Issue: NA
[Contributor] If the regression was CRITICAL (e.g. interrupts a core flow) A discussion in #expensify-open-source has been started about whether any other steps should be taken (e.g. updating the PR review checklist) in order to catch this type of bug sooner.
Link to discussion: No need.
[Contributor] If it was decided to create a regression test for the bug, please propose the regression test steps using the template below to ensure the same bug will not reach production again.
Regression Test Proposal
Precondition:
Sign in with a new user
Test:
Sign out so you land on the sign-in page.
In the browser address bar, append /concierge to the app URL and load it.
Enter an unused email address, click Continue, then click Join.
Choose "Submit expenses to my employer" and finish onboarding.
Verify the Concierge chat opens when onboarding finishes, with no Concierge loading panel sliding in first.
Assuming so, that seems like a classic example of applying the 50% regression cut. Am I missing something? If you want to propose a price increase, ie. to request the full amount of the job, please post one @eh2077 (Standard price was $250 when this issue was created)
well correct! I myself am bit confused here. After checking more thoroughly, I think it makes sense to pay $175 here if #102956 is gonna be paid separately. @eh2077 what do you think? cc @mallenexpensify
$125 each to @mukhrr (Contributor) and @eh2077 (C+), ie. 50% of the $250 job price. One regression: #102954 from #101558, which caused the revert. #102956 and #104391 existed on production, so they do not count.
Activity
applause-bot commentedon Aug 31, 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/8ce52a25149947e58bca9b78606f07b7cae5abd202fcd2fa02af96f7578852fa
MelvinBot commentedon Aug 31, 2026
Proposal
What is the root cause of that problem?
The
/conciergedeeplink is captured correctly, then deliberately thrown away because the account had not finished onboarding when the link was captured.openReportFromDeepLinkparks the pending route behind sign-in, latching whether onboarding was already complete at capture time (src/libs/actions/Link.ts:562). For a brand-new sign-up that value isfalse, and when the deeplink finally replays it hits an unconditional early return (src/libs/actions/Link.ts:600-604):That comment's premise is wrong.
navigateAfterOnboardinghas no access to the deeplink — with no admins chat on a wide screen it goes straight toROUTES.HOME(src/libs/navigateAfterOnboarding.ts:109-114). So nothing "overrides" the destination; it is simply lost, and because the guard sits afterOnyx.disconnect(connection)(Link.ts:582) there is no retry.This is why existing accounts work: their latched value is
true, so the guard is skipped andNavigation.navigate('concierge')fires (Link.ts:628).The guard was added by #92839 (merged 2026-06-25) to fix the post-onboarding "Not here" flash in #91437.
Note this is not specific to Concierge — any deeplink captured before a fresh sign-up is dropped the same way.
ConciergePageitself is fine; it correctly waits ononServerDataReady()and creates the DM whenconciergeReportIDis missing (ConciergePage.tsx:35-49). It is simply never mounted.What changes do you think we should make in order to solve the problem?
Stop discarding the pending route and instead hand it to the code that owns the post-onboarding destination, which is what the comment already assumes happens.
returnatLink.ts:600-604with a persisted "pending post-onboarding route" (a new Onyx key), rather than dropping the route.navigateAfterOnboarding, prefer that pending route overROUTES.HOME/the admins room when one is set, then clear the key. Because the navigation now happens after onboarding completes rather than during it, the [Due for payment 2026-07-03] [$250] Onboarding - Not here page appears briefly after completing onboarding #91437 flash does not return.Link.ts:587-590. On web this branch fires first, because the replay gate opens oncompleteOnboarding's optimistic NVP merge while the onboarding modal is still focused (dismissOnboardingModalBeforeExitis a no-op on web). It currently discards the link and sets a spuriousonboarding.purpose.errorBackButtonmessage. It should persist the pending route too.Because
SCREENS.CONCIERGEis inMODAL_ROUTES_TO_DISMISS(GetStateForActionHandlers.ts:56), the replay must happen after onboarding tears down its modal, not during it — otherwiseRootStackRouterstrips the route out of the stack again.What alternative solutions did you explore? (Optional)
ROUTES.CONCIERGEthrough). Smaller change, but it only fixes Concierge and leaves every other deeplink broken for new users.Investigation details, alternatives ruled out, and test evidence
Ordered path for a new sign-up (verified against
d178dd2):/concierge→DeepLinkHandlerreadsLinking.getInitialURL()withhasAuthToken() === falseand callsopenReportFromDeepLink(..., isAuthenticated=false).route = 'concierge';reportIDis empty (not an/r/link) andconciergeis not inPUBLIC_SCREENS_ROUTES, so it survives the early bail-outs and parks inwaitForUserSignIn()(Link.ts:525). A JS closure, not the URL or navigation state, is the sole carrier.NVP_ONBOARDINGarrives as{hasCompletedGuidedSetupFlow: false}→ latched atLink.ts:562, and the replay gate atLink.ts:648stays shut for the whole onboarding flow.true→ gate opens →handleDeeplinkNavigation()disconnects, then returns at:587(web) or:602. Link destroyed, no retry.navigateAfterOnboarding→Navigation.navigate(ROUTES.HOME). Home rendered.Ruled out with evidence: a
CONCIERGE_REPORT_IDload race (ConciergePagehandles the missing ID and retries via its focus effect);NavigationRoot.initialState(bails at!accounton a signed-out cold start and has[]deps, so it never recomputes after sign-in);LAST_VISITED_PATH(only consumed when already authenticated on first render);AuthScreensInitHandlerandsetUpPoliciesAndNavigate(both no-op — the former only handles/r/<reportID>links, the latter requires anexitToparam).Remaining uncertainty (does not change the root cause): whether the backend ever returns an empty
nvp_onboardingbefore{hasCompletedGuidedSetupFlow: false}. If it does,hasCompletedGuidedSetupFlowSelector({})returnstrue, the navigate does fire, and the loss shifts to theOnboardingGuardredirect +RootStackRouterstack rebuild instead. Both paths end on Home, and the fix above covers both.Test evidence: a web tester run could not exercise this flow. The already-authenticated CI session has no way to perform a cold URL load or reload (both are disallowed for that session, since a reload can drop client auth into an unclearable magic-code wall), and clicking through the LHN would only exercise client-side routing, not a deeplink. The analysis above rests on code reading, not a live repro.
/conciergeURL/conciergeand observe landing screenEvidence (1)
App at Home after completing prestart setup
Possibly related (different flow, same theme of a post-sign-in destination being lost): #96346 and #95445, both about public rooms not opening after an anonymous user signs in. Neither passes through the
initialHasCompletedGuidedSetupFlowguard, so I do not think they share this root cause.Next Steps for Contributor+ team:
To accept:
@MelvinBot implement this proposalto create a draft PR.To refine:
@MelvinBot <your feedback>To reject: Explain why you are rejecting Melvin's proposal.
view run
melvin-bot commentedon Aug 31, 2026
Triggered auto assignment to Contributor-plus team member for initial proposal review - @eh2077 (
External)melvin-bot commentedon Aug 31, 2026
Unable to auto-create job on Upwork. The BZ team member should create it manually for this issue.
melvin-bot commentedon Sep 3, 2026
@eh2077 Whoops! This issue is 2 days overdue. Let's get this updated quick!
melvin-bot commentedon Sep 4, 2026
@eh2077 Whoops! This issue is 2 days overdue. Let's get this updated quick!
melvin-bot commentedon Sep 4, 2026
@eh2077 Whoops! This issue is 2 days overdue. Let's get this updated quick!
eh2077 commentedon Sep 4, 2026
checking
87 remaining items
eh2077 commentedon Oct 29, 2026
Contributor+ Checklist:
[Contributor] The offending PR and associated issue have been commented on, pointing out the bug it caused and why, so the author and reviewers can learn from the mistake.
Link to the comment on the PR: NA. I believe this is a new feature because deeplink only works for existing users.
Link to the comment on the Issue: NA
[Contributor] If the regression was CRITICAL (e.g. interrupts a core flow) A discussion in #expensify-open-source has been started about whether any other steps should be taken (e.g. updating the PR review checklist) in order to catch this type of bug sooner.
Link to discussion: No need.
[Contributor] If it was decided to create a regression test for the bug, please propose the regression test steps using the template below to ensure the same bug will not reach production again.
Regression Test Proposal
Precondition:
Test:
Do we agree 👍 or 👎
eh2077 commentedon Oct 29, 2026
@mallenexpensify I also think payment should be $250 because
mallenexpensify commentedon Oct 30, 2026
@eh2077 what about the above,
eh2077 commentedon Oct 30, 2026
good catch, I think it's a regression from the first PR and was fixed by the second PR.
mukhrr commentedon Oct 30, 2026
well keeping in mind original price was $250, I think it still would be fair to keep it based on this
mallenexpensify commentedon Oct 30, 2026
@mukhrr is the below correct
Assuming so, that seems like a classic example of applying the 50% regression cut. Am I missing something? If you want to propose a price increase, ie. to request the full amount of the job, please post one @eh2077 (Standard price was $250 when this issue was created)
mukhrr commentedon Oct 31, 2026
well correct! I myself am bit confused here. After checking more thoroughly, I think it makes sense to pay $175 here if #102956 is gonna be paid separately. @eh2077 what do you think? cc @mallenexpensify
mallenexpensify commentedon Nov 2, 2026
Claude posting on behalf of Matt:
$125 each to @mukhrr (Contributor) and @eh2077 (C+), ie. 50% of the $250 job price. One regression: #102954 from #101558, which caused the revert. #102956 and #104391 existed on production, so they do not count.
Evidence
Claude posting on behalf of Matt.
PAYRUBRICTEST v1mallenexpensify commentedon Nov 2, 2026
Payment Summary
Contributor: @mukhrr due $125 via NewDot
Contributor+: @eh2077 due $125 via NewDot
Paid $175 on other
flaviadefaria commentedon Nov 9, 2026
Approved: @mukhrr due $125 via NewDot