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
[$175] iOS - Log in - Online magic link does not open the linked report when signed out #105778
Version Number: 9.5.28-5 Reproducible in staging?: Yes Reproducible in production?: Yes If this was caught during regression testing, add the test name, ID and link from BrowserStack:#102261 Email or phone of affected tester (no customers):jose.sat94+ndndjul@gmail.com Issue reported by: Applause Internal Team Bug source: Pull Request QA execution Device used: iPhone 13/18.6.2 App Component: Other
Action Performed:
Launch Expensify app
Log in with valid credentials
Sign out of New Expensify.
Open a real Expensify email containing an action link, such as an expense report email.
Tap View on the expense report email.
Expected Result:
The app signs in and opens the linked report without displaying the offline message.
Actual Result:
Tapping the action link does not successfully sign in and open the linked report.
I think the HybridApp sign-in gate is the likely cause. On HybridApp, AuthScreens only mount when hybridApp.readyToShowAuthScreens is true (AppNavigator.native.tsx:17). That flag is only set after credentials.autoGeneratedLogin, autoGeneratedPassword, account.requiresTwoFactorAuth, needsTwoFactorAuthSetup, and nvp_tryNewDot all arrive in Onyx (HybridApp.ts:98-125). A magic-link sign-in skips the normal email and code steps. If the SigninUserWithLink response leaves out any of those values, the app stays on the public screens, and the exitTo navigation never runs. Separately, if nvp_tryNewDot sends the user to Classic, NewDot closes without opening the report (HybridApp.ts:120-121).
That gate is shared with Android, so the iOS-only part may be in the native iOS HybridApp layer, outside the App repo. For example, the universal link may not reach NewDot while it is signed out.
I wasn't able to verify this because log search was unavailable in this run and I couldn't review the video. This bug only happens in the iOS app, so I didn't test it on web.
To confirm, check the logs for the affected tester around the repro time:
Was SigninUserWithLink sent, and did it succeed? If it wasn't sent, iOS never handed the link to NewDot.
If it succeeded, does [HybridApp] Performing sign-in on OldDot side follow? If not, the gate above is blocking.
Does the video end on the sign-in page, a spinner, Classic, or Safari?
Smaller issues found along the way
isPublicScreenRoute.ts builds an anchored regex such as ^v/\w+/\w+$. A v/...?exitTo=... route fails that match, so openReportFromDeepLink (Link.ts:489-510) also calls waitForUserSignIn().
waitForUserSignIn() keeps only one resolver (Session/index.ts:100). With two callers, one promise never resolves. That may drop the exitTo navigation from handleExitToNavigation. It is fragile, but probably not enough to cause this bug alone.
Next Steps: Reply with @MelvinBot <your feedback> to refine this analysis.
changed the title [-]iOS - Log in - Online magic link does not open the linked report when signed out[/-][+][$175] iOS - Log in - Online magic link does not open the linked report when signed out[/+]on Nov 3, 2026
Please re-state the problem that we are trying to solve in this issue.
After signing out on iOS, opening a valid report action link from an Expensify email reportedly fails to complete sign-in and display the linked report. The required behavior is successful authentication followed by navigation to that specific report while online.
What is the root cause of that problem?
Unverified. This case does not yet meet the required evidence standard for a submission-ready fix.
The supplied material contains no implementation, authentication trace, sanitized link structure, or failing automated test. No Expensify checkout exists in the workspace, and attempts to retrieve the relevant source and PR diff failed.
The official release notes identify PR #102261 as changing the magic-link page’s offline presentation. That establishes an investigation lead, not proof that it caused the authentication or navigation failure. Expensify release notes
The following remain unresolved:
Does link validation execute, fail, or never start?
Is connectivity actually reported as offline when the link opens?
Does authentication succeed while the report destination is lost?
Does the email URL contain authentication material, or require a separate authentication handoff?
Exact file paths, line numbers, and a causal explanation cannot responsibly be asserted without inspecting the affected revision.
What changes do you think we should make in order to solve the problem?
No defensible TypeScript diff can be specified from the available evidence. Inventing function names or changing an assumed connectivity guard would violate the audit requirements.
Before selecting a patch:
Reproduce on version 9.5.28-5 with a fresh, valid link and record whether the app was running or terminated.
Capture the sanitized URL structure, connectivity transitions, validation request/result, session transition, and final navigation destination. Redact authentication secrets.
Encode the observed failure in a regression test, then change only the responsible branch.
A fix must demonstrate both successful authentication and navigation to the linked report. Removing an offline message alone does not establish either outcome.
What alternative solutions did you explore? (Optional)
No MelvinBot proposal was supplied, so there is no proposal to dismantle. Its authorship of the referenced PR does not establish a defect.
Reject these unsupported approaches:
Remove the offline UI: could conceal the failure while leaving authentication stalled.
Force connectivity to online: could break legitimate offline handling.
Add unconditional retries: could duplicate validation attempts; token-consumption behavior must first be established.
Navigate immediately: does not prove authentication or report access succeeded.
Rewrite authentication or deep-link routing: introduces unjustified regression risk before the failing boundary is identified.
Duplicate and existing-fix status also remain unverified because the issue URL and current discussion were not supplied or retrieved.
What automated tests can you add to prevent this from re-occurring?
Proposed regression contract, not implemented or executed:
it('authenticates a signed-out user from a valid online report link and opens that report')
Using the affected revision’s existing Jest navigation/authentication harness:
Initialize signed-out state after the normal sign-out flow.
Deliver a report action-link fixture matching the real URL structure.
Provide online connectivity and controlled successful authentication responses.
Assert validation executes, the session becomes authenticated, and navigation resolves to the exact report ID.
Assert no offline message appears while connectivity remains online.
Add cases for offline-to-online recovery, expired links, duplicate URL delivery, and authenticated users. The principal regression test must fail before the patch and pass afterward.
An iOS device reproduction is additionally required to verify universal-link delivery and app lifecycle behavior that mocked Jest tests cannot establish.
Please re-state the problem that we are trying to solve in this issue.
On the iOS app, after logging in and signing out of New Expensify, tapping View on a report action magic link from an email (.../v/<accountID>/<validateCode>?exitTo=r/<reportID>) does not complete navigation to the linked report after signing in.
What is the root cause of that problem?
When a signed-out user opens a magic link containing an exitTo query parameter (e.g. v/<accountID>/<validateCode>?exitTo=r/<reportID>), two bugs in the deep-link and session sign-in flow collide:
isPublicScreenRoute fails on public routes with query parameters (src/libs/isPublicScreenRoute.ts:3-8):
When the deep link arrives via DeepLinkHandler.tsx, openReportFromDeepLink (src/libs/actions/Link.ts:484-500) extracts route = getRouteFromLink(url) ("v/<accountID>/<validateCode>?exitTo=r/<reportID>") and checks:
Because PUBLIC_SCREENS_ROUTES.VALIDATE_LOGIN is 'v/:accountID/:validateCode', ^v/\w+/\w+$ fails to match 'v/<accountID>/<validateCode>?exitTo=r/<reportID>'. As a result, openReportFromDeepLink does not return early at line 499 and instead proceeds to line 519 (waitForUserSignIn().then(...)).
waitForUserSignIn() stores only a single resolver (src/libs/actions/Session/index.ts:100, 1657-1665):
At the same time, ValidateLoginPage (src/pages/ValidateLoginPage/index.tsx:37) calls signInWithValidateCodeAndNavigate(Number(accountID), validateCode, preferredLocale, exitTo), which invokes handleExitToNavigation(exitTo) (src/libs/actions/Session/index.ts:1667-1677) and calls waitForUserSignIn().
Because Session/index.ts:100 stores only a single callback (let authPromiseResolver: ((value: boolean) => void) | null = null), having both openReportFromDeepLink and handleExitToNavigation call waitForUserSignIn() while signed out causes the second caller to overwrite authPromiseResolver:
If openReportFromDeepLink overwrites authPromiseResolver (either on initial URL handling or when DeepLinkHandler's effect re-runs as conciergeReportID / introSelected hydrate during sign-in), handleExitToNavigation(exitTo)'s promise never resolves and exitTo (r/<reportID>) is never opened.
Instead, openReportFromDeepLink's waiter runs handleDeeplinkNavigation() with deeplinkRoute = "v/<accountID>/<validateCode>?exitTo=r/<reportID>" (a public route rather than the target report).
What changes do you think we should make in order to solve the problem?
Strip query parameters and hash fragments in src/libs/isPublicScreenRoute.ts:
Normalize the route path before matching against PUBLIC_SCREENS_ROUTES so v/:accountID/:validateCode?exitTo=... is properly recognized as a public screen route and openReportFromDeepLink exits early as intended:
Support multiple concurrent callers in waitForUserSignIn() (src/libs/actions/Session/index.ts):
Replace the single authPromiseResolver variable with a Set of resolvers so concurrent callers (such as handleExitToNavigation and any deep-link listeners) all resolve when deprecatedSession.authToken becomes truthy:
What alternative solutions did you explore? (Optional)
Extracting exitTo inside openReportFromDeepLink: rejected because ValidateLoginPage already owns magic-link sign-in and exitTo navigation via signInWithValidateCodeAndNavigate / handleExitToNavigation; openReportFromDeepLink explicitly intends to skip public routes at line 498.
What automated tests can you add to prevent this from re-occurring?
Add unit tests for isPublicScreenRoute verifying that v/12345/67890?exitTo=r/98765 returns true.
Add a unit test in tests/unit/SessionTest.ts verifying that multiple concurrent calls to waitForUserSignIn() all resolve when ONYXKEYS.SESSION receives an authToken.
Please re-state the problem that we are trying to solve in this issue.
On iOS, when a signed-out user taps an email action link with an exitTo parameter (such as viewing an expense report: v/<accountID>/<validateCode>?exitTo=r/<reportID>), the app signs in with the magic link but fails to open the linked report.
What is the root cause of that problem?
There are two interacting defects in isPublicScreenRoute.ts and Session/index.ts:
isPublicScreenRoute fails to match URLs with Query Parameters (src/libs/isPublicScreenRoute.ts:3-8):
In src/libs/actions/Link.ts (openReportFromDeepLink):
openReportFromDeepLink intends to let ValidateLoginPage handle the magic link and subsequent exitTo navigation when the user is not authenticated.
However, isPublicScreenRoute constructs an end-anchored regex without stripping query strings:
For v/<accountID>/<validateCode>?exitTo=r/<reportID>, the regex ^v/\w+/\w+$ fails because of ?exitTo=.... As a result, openReportFromDeepLink fails to exit early and proceeds to invoke waitForUserSignIn().
Single-Resolver Race in waitForUserSignIn() (src/libs/actions/Session/index.ts:100, 1657-1665):
Simultaneously, ValidateLoginPage processes the magic link and calls handleExitToNavigation(exitTo) (Session/index.ts:1667), which also calls waitForUserSignIn().
In Session/index.ts:
Because only a single callback variable is maintained, concurrent calls to waitForUserSignIn() from openReportFromDeepLink and handleExitToNavigation overwrite authPromiseResolver.
When deprecatedSession.authToken becomes truthy, only one promise resolves: handleExitToNavigation's promise is dropped, so the user is never navigated to the linked report (r/<reportID>). Instead, openReportFromDeepLink attempts to navigate to the magic-link route itself.
What changes do you think we should make in order to solve the problem?
Normalize Route in src/libs/isPublicScreenRoute.ts:
Strip query parameters and hash fragments before testing against PUBLIC_SCREENS_ROUTES:
Support Concurrent Listeners in src/libs/actions/Session/index.ts:
Replace the singleton authPromiseResolver with a Set of callbacks:
constauthPromiseResolvers=newSet<(value: boolean)=>void>();// In ONYXKEYS.SESSION listener:if(deprecatedSession.authToken&&authPromiseResolvers.size>0){authPromiseResolvers.forEach((resolve)=>resolve(true));authPromiseResolvers.clear();}// In waitForUserSignIn:functionwaitForUserSignIn(): Promise<boolean>{returnnewPromise<boolean>((resolve)=>{if(deprecatedSession.authToken){resolve(true);return;}authPromiseResolvers.add(resolve);});}
What alternative solutions did you explore? (Optional)
Parsing exitTo inside openReportFromDeepLink: Handling exitTo in openReportFromDeepLink would duplicate the authentication handoff logic that ValidateLoginPage already manages via signInWithValidateCodeAndNavigate. Allowing isPublicScreenRoute to recognize routes with query parameters cleanly delegates magic-link authentication to ValidateLoginPage as originally architected.
Automated Tests
Unit tests for isPublicScreenRoute:
Verify v/12345/code?exitTo=r/999 returns true.
Verify routes with query parameters and hash fragments (v/123/code#test) return true.
Unit tests in tests/unit/SessionTest.ts:
Verify that multiple concurrent calls to waitForUserSignIn() while signed out all resolve successfully when authToken is populated in Onyx.
Manual Tests
On iOS, sign in to New Expensify and then explicitly sign out.
Open an Expensify email containing a report action link (e.g. "View report").
Tap the link to open the Expensify app.
Verify that the app signs in and immediately opens the linked expense report.
Please re-state the problem that we are trying to solve in this issue.
On iOS, when the user is signed out and taps a report link from an email notification (or a /v/<accountID>/<validateCode>?exitTo=r/<reportID> magic link), the app opens the sign-in screen or fails to land on the linked expense report.
What is the root cause of that problem?
Inspecting both attached screen recordings alongside the deep-link and HybridApp sign-in code reveals two distinct layers to this issue:
What happens in both attached videos (direct /r/<reportID> links vs /v/<accountID>/<validateCode> magic links):
In the Applause recording (0:17 to 0:26), the tester copies the "open it in Expensify" link from the "Please approve ARS 3,756.00 expense report" email and pastes it into Apple Notes. That email link is https://new.expensify.com/r/7454297444119905 (a direct /r/<reportID> deep link with no /v/<accountID>/<validateCode> credentials). Because the user is signed out and the expense report is not a public room, openReportFromDeepLink (src/libs/actions/Link.ts:453-478) calls openReport, unblocks the splash screen onto SignInPage, and waits at waitForUserSignIn() (line 519) for the user to sign in before opening the report.
Similarly, in @mkhutornyi's Apple Notes recording (85fc0e8f...), the top URL (https://new.expensify.com/v/23472625/SNWFEYJTO?exitTo=/r/84514393167816) contains a line break before SNWFEYJTO, so iOS Notes only linkified the second URL (https://new.expensify.com//r/84514393167816/8385869340041982305), which was tapped at 0:01 and has no validateCode.
Why a true /v/<accountID>/<validateCode>?exitTo=/r/<reportID> magic link also fails to open the report when signed out on iOS:
Unstripped query string in isPublicScreenRoute (src/libs/isPublicScreenRoute.ts:3-8):isPublicScreenRoute(route) tests new RegExp('^v/\\w+/\\w+$') against the full route including ?exitTo=/r/<reportID>. Because ?exitTo=... fails the $ anchor, isPublicScreenRoute returns false, so openReportFromDeepLink (src/libs/actions/Link.ts:498) does not return early.
Singleton authPromiseResolver overwrite + stale onboardingSubscription in Link.ts (src/libs/actions/Session/index.ts:100, 1657-1677 and src/libs/actions/Link.ts:519-649): Because openReportFromDeepLink does not return early at line 498, both openReportFromDeepLink (Link.ts:519) and ValidateLoginPage -> handleExitToNavigation (Session/index.ts:1668) call waitForUserSignIn(). Session/index.ts:100 stores only a single authPromiseResolver, so the second call overwrites the first resolver. Even if openReportFromDeepLink wins the resolver, its onboardingSubscription (Link.ts:570-648) subsequently executes Navigation.navigate(deeplinkRoute) where deeplinkRoute is the /v/<accountID>/<validateCode>?exitTo=... route itself rather than the exitTo report route.
HybridApp readyToShowAuthScreens timing (src/libs/Navigation/AppNavigator/AppNavigator.native.tsx:16-19 and src/libs/HybridApp.ts:118-125): On iOS HybridApp, signing out calls resetSignInFlow(), which sets hybridApp.useNewDotSignInPage = true and hybridApp.readyToShowAuthScreens = false. When SigninUserWithLink succeeds, session.authToken updates in Onyx before HybridApp.ts:119-124 (setUseNewDotSignInPage(false).then(() => setReadyToShowAuthScreens(true))) completes its chained Onyx.merge calls, delaying <AuthScreens /> mount if tryNewDot or account fields arrive in a later Onyx update.
What changes do you think we should make in order to solve the problem?
In src/libs/isPublicScreenRoute.ts, strip query parameters (?) and hash fragments (#) (and normalize leading/trailing slashes) before matching against PUBLIC_SCREENS_ROUTES so v/<accountID>/<validateCode>?exitTo=... returns true and openReportFromDeepLink (src/libs/actions/Link.ts:498) exits early, leaving ValidateLoginPage as the sole handler of magic-link authentication and exitTo navigation.
In src/libs/actions/Session/index.ts, replace the singleton let authPromiseResolver with a Set<(value: boolean) => void>() (matching the note in Session/index.ts:942) so concurrent callers of waitForUserSignIn() never drop each other's resolution.
In src/libs/HybridApp.ts (signInToOldDotAndChooseExperience), merge {useNewDotSignInPage: false, readyToShowAuthScreens: true} in a single atomic Onyx.merge(ONYXKEYS.HYBRID_APP, ...) call when staying in NewDot instead of chaining setUseNewDotSignInPage(false).then(() => setReadyToShowAuthScreens(true)).
What alternative solutions did you explore? (Optional)
Having openReportFromDeepLink parse exitTo directly was considered, but ValidateLoginPage already owns signInWithValidateCodeAndNavigate(accountID, validateCode, preferredLocale, exitTo). Making isPublicScreenRoute query-aware prevents openReportFromDeepLink from competing with ValidateLoginPage.
Activity
applause-bot commentedon Nov 3, 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/5ae127757e27b487e823afd7b03f1bfab578cd4023748d7d35eda5525a560882
mkhutornyi commentedon Nov 3, 2026
Investigating...
mkhutornyi commentedon Nov 3, 2026
I reverted #102261 locally and the bug is still reproducible. So it's not the offending PR.
MelvinBot commentedon Nov 3, 2026
This bug is not caused by Show the offline message instead of a bare spinner on the magic link page. That PR is on staging but not production, and this issue also reproduces on production. The PR only changes what renders while offline. Online,
FullPageOfflineBlockingView.tsx:37returns the same spinner as before. The sign-in effects inValidateLoginPageare unchanged.I think the HybridApp sign-in gate is the likely cause. On HybridApp,
AuthScreensonly mount whenhybridApp.readyToShowAuthScreensis true (AppNavigator.native.tsx:17). That flag is only set aftercredentials.autoGeneratedLogin,autoGeneratedPassword,account.requiresTwoFactorAuth,needsTwoFactorAuthSetup, andnvp_tryNewDotall arrive in Onyx (HybridApp.ts:98-125). A magic-link sign-in skips the normal email and code steps. If theSigninUserWithLinkresponse leaves out any of those values, the app stays on the public screens, and theexitTonavigation never runs. Separately, ifnvp_tryNewDotsends the user to Classic, NewDot closes without opening the report (HybridApp.ts:120-121).That gate is shared with Android, so the iOS-only part may be in the native iOS HybridApp layer, outside the App repo. For example, the universal link may not reach NewDot while it is signed out.
I wasn't able to verify this because log search was unavailable in this run and I couldn't review the video. This bug only happens in the iOS app, so I didn't test it on web.
To confirm, check the logs for the affected tester around the repro time:
SigninUserWithLinksent, and did it succeed? If it wasn't sent, iOS never handed the link to NewDot.[HybridApp] Performing sign-in on OldDot sidefollow? If not, the gate above is blocking.Smaller issues found along the way
isPublicScreenRoute.tsbuilds an anchored regex such as^v/\w+/\w+$. Av/...?exitTo=...route fails that match, soopenReportFromDeepLink(Link.ts:489-510) also callswaitForUserSignIn().waitForUserSignIn()keeps only one resolver (Session/index.ts:100). With two callers, one promise never resolves. That may drop theexitTonavigation fromhandleExitToNavigation. It is fragile, but probably not enough to cause this bug alone.Next Steps: Reply with
@MelvinBot <your feedback>to refine this analysis.view run
melvin-bot commentedon Nov 3, 2026
Current assignee @mkhutornyi is eligible for the External assigner, not assigning anyone new.
[-]iOS - Log in - Online magic link does not open the linked report when signed out[/-][+][$175] iOS - Log in - Online magic link does not open the linked report when signed out[/+]melvin-bot commentedon Nov 3, 2026
Job added to Upwork: https://www.upwork.com/jobs/~022117640669203853917
mkhutornyi commentedon Nov 3, 2026
Confirmed this is reproducible in iOS production app downloaded from appstore
bug.repro.mov
camesenin commentedon Nov 3, 2026
Proposal
Please re-state the problem that we are trying to solve in this issue.
After signing out on iOS, opening a valid report action link from an Expensify email reportedly fails to complete sign-in and display the linked report. The required behavior is successful authentication followed by navigation to that specific report while online.
What is the root cause of that problem?
Unverified. This case does not yet meet the required evidence standard for a submission-ready fix.
The supplied material contains no implementation, authentication trace, sanitized link structure, or failing automated test. No Expensify checkout exists in the workspace, and attempts to retrieve the relevant source and PR diff failed.
The official release notes identify PR #102261 as changing the magic-link page’s offline presentation. That establishes an investigation lead, not proof that it caused the authentication or navigation failure. Expensify release notes
The following remain unresolved:
Exact file paths, line numbers, and a causal explanation cannot responsibly be asserted without inspecting the affected revision.
What changes do you think we should make in order to solve the problem?
No defensible TypeScript diff can be specified from the available evidence. Inventing function names or changing an assumed connectivity guard would violate the audit requirements.
Before selecting a patch:
9.5.28-5with a fresh, valid link and record whether the app was running or terminated.A fix must demonstrate both successful authentication and navigation to the linked report. Removing an offline message alone does not establish either outcome.
What alternative solutions did you explore? (Optional)
No MelvinBot proposal was supplied, so there is no proposal to dismantle. Its authorship of the referenced PR does not establish a defect.
Reject these unsupported approaches:
Duplicate and existing-fix status also remain unverified because the issue URL and current discussion were not supplied or retrieved.
What automated tests can you add to prevent this from re-occurring?
Proposed regression contract, not implemented or executed:
it('authenticates a signed-out user from a valid online report link and opens that report')Using the affected revision’s existing Jest navigation/authentication harness:
Add cases for offline-to-online recovery, expired links, duplicate URL delivery, and authenticated users. The principal regression test must fail before the patch and pass afterward.
An iOS device reproduction is additionally required to verify universal-link delivery and app lifecycle behavior that mocked Jest tests cannot establish.
Contributor details
Your Expensify account email: camesenin@gmail.com
Upwork Profile Link: https://www.upwork.com/freelancers/~01b60d704ba377be83
melvin-bot commentedon Nov 3, 2026
✅ Contributor details stored successfully. Thank you for contributing to Expensify!
Enes2553 commentedon Nov 3, 2026
Proposal
Please re-state the problem that we are trying to solve in this issue.
On the iOS app, after logging in and signing out of New Expensify, tapping View on a report action magic link from an email (
.../v/<accountID>/<validateCode>?exitTo=r/<reportID>) does not complete navigation to the linked report after signing in.What is the root cause of that problem?
When a signed-out user opens a magic link containing an
exitToquery parameter (e.g.v/<accountID>/<validateCode>?exitTo=r/<reportID>), two bugs in the deep-link and session sign-in flow collide:isPublicScreenRoutefails on public routes with query parameters (src/libs/isPublicScreenRoute.ts:3-8):When the deep link arrives via
DeepLinkHandler.tsx,openReportFromDeepLink(src/libs/actions/Link.ts:484-500) extractsroute = getRouteFromLink(url)("v/<accountID>/<validateCode>?exitTo=r/<reportID>") and checks:However,
isPublicScreenRoutebuilds an end-anchored regex without stripping query strings or hashes:Because
PUBLIC_SCREENS_ROUTES.VALIDATE_LOGINis'v/:accountID/:validateCode',^v/\w+/\w+$fails to match'v/<accountID>/<validateCode>?exitTo=r/<reportID>'. As a result,openReportFromDeepLinkdoes not return early at line 499 and instead proceeds to line 519 (waitForUserSignIn().then(...)).waitForUserSignIn()stores only a single resolver (src/libs/actions/Session/index.ts:100, 1657-1665):At the same time,
ValidateLoginPage(src/pages/ValidateLoginPage/index.tsx:37) callssignInWithValidateCodeAndNavigate(Number(accountID), validateCode, preferredLocale, exitTo), which invokeshandleExitToNavigation(exitTo)(src/libs/actions/Session/index.ts:1667-1677) and callswaitForUserSignIn().Because
Session/index.ts:100stores only a single callback (let authPromiseResolver: ((value: boolean) => void) | null = null), having bothopenReportFromDeepLinkandhandleExitToNavigationcallwaitForUserSignIn()while signed out causes the second caller to overwriteauthPromiseResolver:openReportFromDeepLinkoverwritesauthPromiseResolver(either on initial URL handling or whenDeepLinkHandler's effect re-runs asconciergeReportID/introSelectedhydrate during sign-in),handleExitToNavigation(exitTo)'s promise never resolves andexitTo(r/<reportID>) is never opened.openReportFromDeepLink's waiter runshandleDeeplinkNavigation()withdeeplinkRoute = "v/<accountID>/<validateCode>?exitTo=r/<reportID>"(a public route rather than the target report).What changes do you think we should make in order to solve the problem?
Strip query parameters and hash fragments in
src/libs/isPublicScreenRoute.ts:Normalize the route path before matching against
PUBLIC_SCREENS_ROUTESsov/:accountID/:validateCode?exitTo=...is properly recognized as a public screen route andopenReportFromDeepLinkexits early as intended:Support multiple concurrent callers in
waitForUserSignIn()(src/libs/actions/Session/index.ts):Replace the single
authPromiseResolvervariable with aSetof resolvers so concurrent callers (such ashandleExitToNavigationand any deep-link listeners) all resolve whendeprecatedSession.authTokenbecomes truthy:In the
ONYXKEYS.SESSIONcallback (src/libs/actions/Session/index.ts:113-116):And in
waitForUserSignIn()(src/libs/actions/Session/index.ts:1657-1665):What alternative solutions did you explore? (Optional)
exitToinsideopenReportFromDeepLink: rejected becauseValidateLoginPagealready owns magic-link sign-in andexitTonavigation viasignInWithValidateCodeAndNavigate/handleExitToNavigation;openReportFromDeepLinkexplicitly intends to skip public routes at line 498.What automated tests can you add to prevent this from re-occurring?
isPublicScreenRouteverifying thatv/12345/67890?exitTo=r/98765returnstrue.tests/unit/SessionTest.tsverifying that multiple concurrent calls towaitForUserSignIn()all resolve whenONYXKEYS.SESSIONreceives anauthToken.ayolyn commentedon Nov 3, 2026
Proposal
Please re-state the problem that we are trying to solve in this issue.
On iOS, when a signed-out user taps an email action link with an
exitToparameter (such as viewing an expense report:v/<accountID>/<validateCode>?exitTo=r/<reportID>), the app signs in with the magic link but fails to open the linked report.What is the root cause of that problem?
There are two interacting defects in
isPublicScreenRoute.tsandSession/index.ts:isPublicScreenRoutefails to match URLs with Query Parameters (src/libs/isPublicScreenRoute.ts:3-8):In
src/libs/actions/Link.ts(openReportFromDeepLink):openReportFromDeepLinkintends to letValidateLoginPagehandle the magic link and subsequentexitTonavigation when the user is not authenticated.However,
isPublicScreenRouteconstructs an end-anchored regex without stripping query strings:For
v/<accountID>/<validateCode>?exitTo=r/<reportID>, the regex^v/\w+/\w+$fails because of?exitTo=.... As a result,openReportFromDeepLinkfails to exit early and proceeds to invokewaitForUserSignIn().Single-Resolver Race in
waitForUserSignIn()(src/libs/actions/Session/index.ts:100, 1657-1665):Simultaneously,
ValidateLoginPageprocesses the magic link and callshandleExitToNavigation(exitTo)(Session/index.ts:1667), which also callswaitForUserSignIn().In
Session/index.ts:Because only a single callback variable is maintained, concurrent calls to
waitForUserSignIn()fromopenReportFromDeepLinkandhandleExitToNavigationoverwriteauthPromiseResolver.When
deprecatedSession.authTokenbecomes truthy, only one promise resolves:handleExitToNavigation's promise is dropped, so the user is never navigated to the linked report (r/<reportID>). Instead,openReportFromDeepLinkattempts to navigate to the magic-link route itself.What changes do you think we should make in order to solve the problem?
Normalize Route in
src/libs/isPublicScreenRoute.ts:Strip query parameters and hash fragments before testing against
PUBLIC_SCREENS_ROUTES:Support Concurrent Listeners in
src/libs/actions/Session/index.ts:Replace the singleton
authPromiseResolverwith aSetof callbacks:What alternative solutions did you explore? (Optional)
exitToinsideopenReportFromDeepLink: HandlingexitToinopenReportFromDeepLinkwould duplicate the authentication handoff logic thatValidateLoginPagealready manages viasignInWithValidateCodeAndNavigate. AllowingisPublicScreenRouteto recognize routes with query parameters cleanly delegates magic-link authentication toValidateLoginPageas originally architected.Automated Tests
isPublicScreenRoute:v/12345/code?exitTo=r/999returnstrue.v/123/code#test) returntrue.tests/unit/SessionTest.ts:waitForUserSignIn()while signed out all resolve successfully whenauthTokenis populated in Onyx.Manual Tests
Contributor details
Your Expensify account email: juliusayolyn@gmail.com
Upwork Profile Link: https://www.upwork.com/freelancers/~0105126232377c4c5c
melvin-bot commentedon Nov 3, 2026
✅ Contributor details stored successfully. Thank you for contributing to Expensify!
jaharoni commentedon Nov 4, 2026
Proposal
Please re-state the problem that we are trying to solve in this issue.
On iOS, when the user is signed out and taps a report link from an email notification (or a
/v/<accountID>/<validateCode>?exitTo=r/<reportID>magic link), the app opens the sign-in screen or fails to land on the linked expense report.What is the root cause of that problem?
Inspecting both attached screen recordings alongside the deep-link and HybridApp sign-in code reveals two distinct layers to this issue:
What happens in both attached videos (direct
/r/<reportID>links vs/v/<accountID>/<validateCode>magic links):0:17to0:26), the tester copies the"open it in Expensify"link from the"Please approve ARS 3,756.00 expense report"email and pastes it into Apple Notes. That email link ishttps://new.expensify.com/r/7454297444119905(a direct/r/<reportID>deep link with no/v/<accountID>/<validateCode>credentials). Because the user is signed out and the expense report is not a public room,openReportFromDeepLink(src/libs/actions/Link.ts:453-478) callsopenReport, unblocks the splash screen ontoSignInPage, and waits atwaitForUserSignIn()(line 519) for the user to sign in before opening the report.@mkhutornyi's Apple Notes recording (85fc0e8f...), the top URL (https://new.expensify.com/v/23472625/SNWFEYJTO?exitTo=/r/84514393167816) contains a line break beforeSNWFEYJTO, so iOS Notes only linkified the second URL (https://new.expensify.com//r/84514393167816/8385869340041982305), which was tapped at0:01and has novalidateCode.Why a true
/v/<accountID>/<validateCode>?exitTo=/r/<reportID>magic link also fails to open the report when signed out on iOS:isPublicScreenRoute(src/libs/isPublicScreenRoute.ts:3-8):isPublicScreenRoute(route)testsnew RegExp('^v/\\w+/\\w+$')against the fullrouteincluding?exitTo=/r/<reportID>. Because?exitTo=...fails the$anchor,isPublicScreenRoutereturnsfalse, soopenReportFromDeepLink(src/libs/actions/Link.ts:498) does not return early.authPromiseResolveroverwrite + staleonboardingSubscriptioninLink.ts(src/libs/actions/Session/index.ts:100, 1657-1677andsrc/libs/actions/Link.ts:519-649): BecauseopenReportFromDeepLinkdoes not return early at line 498, bothopenReportFromDeepLink(Link.ts:519) andValidateLoginPage->handleExitToNavigation(Session/index.ts:1668) callwaitForUserSignIn().Session/index.ts:100stores only a singleauthPromiseResolver, so the second call overwrites the first resolver. Even ifopenReportFromDeepLinkwins the resolver, itsonboardingSubscription(Link.ts:570-648) subsequently executesNavigation.navigate(deeplinkRoute)wheredeeplinkRouteis the/v/<accountID>/<validateCode>?exitTo=...route itself rather than theexitToreport route.readyToShowAuthScreenstiming (src/libs/Navigation/AppNavigator/AppNavigator.native.tsx:16-19andsrc/libs/HybridApp.ts:118-125): On iOS HybridApp, signing out callsresetSignInFlow(), which setshybridApp.useNewDotSignInPage = trueandhybridApp.readyToShowAuthScreens = false. WhenSigninUserWithLinksucceeds,session.authTokenupdates in Onyx beforeHybridApp.ts:119-124(setUseNewDotSignInPage(false).then(() => setReadyToShowAuthScreens(true))) completes its chainedOnyx.mergecalls, delaying<AuthScreens />mount iftryNewDotoraccountfields arrive in a later Onyx update.What changes do you think we should make in order to solve the problem?
src/libs/isPublicScreenRoute.ts, strip query parameters (?) and hash fragments (#) (and normalize leading/trailing slashes) before matching againstPUBLIC_SCREENS_ROUTESsov/<accountID>/<validateCode>?exitTo=...returnstrueandopenReportFromDeepLink(src/libs/actions/Link.ts:498) exits early, leavingValidateLoginPageas the sole handler of magic-link authentication andexitTonavigation.src/libs/actions/Session/index.ts, replace the singletonlet authPromiseResolverwith aSet<(value: boolean) => void>()(matching the note inSession/index.ts:942) so concurrent callers ofwaitForUserSignIn()never drop each other's resolution.src/libs/HybridApp.ts(signInToOldDotAndChooseExperience), merge{useNewDotSignInPage: false, readyToShowAuthScreens: true}in a single atomicOnyx.merge(ONYXKEYS.HYBRID_APP, ...)call when staying in NewDot instead of chainingsetUseNewDotSignInPage(false).then(() => setReadyToShowAuthScreens(true)).What alternative solutions did you explore? (Optional)
openReportFromDeepLinkparseexitTodirectly was considered, butValidateLoginPagealready ownssignInWithValidateCodeAndNavigate(accountID, validateCode, preferredLocale, exitTo). MakingisPublicScreenRoutequery-aware preventsopenReportFromDeepLinkfrom competing withValidateLoginPage.