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-11-10] [$175] [Exploratory] Navigation - App keeps on loading when opening expense from email #100682
Open a magic link carrying exitTo, either by tapping an action link in an Expensify email or with: https://new.expensify.com/v/<accountID>/123456?exitTo=/r/<reportID>
Wait 30 seconds.
Expected Result:
An offline message, the sign in page, or at minimum "This is taking longer than expected..." with a Go back button after 10 seconds.
Actual Result:
A bare centered spinner on an empty screen. No header, no error, no Go back button. The app stays unusable until the network returns.
changed the title [-][Exploratory] Navigation- App keeps on loading when opening expense from email[/-][+][Exploratory] Navigation - App keeps on loading when opening expense from email[/+]on Sep 9, 2026
The expense report screen has a loading gate with no exit condition. It shows a skeleton whenever an expense report has a non-zero total but zero transactions in Onyx — with no timeout, no retry, and no error state.
The report?.total !== 0 term is not tied to any in-flight request. Once it's true, the only way out is transactions arriving, the user going offline, or the total becoming 0.
That exactly matches the repro. The WS owner already has the report row (with its total) from OpenApp, but not its transactions — those only arrive with OpenReport. Tapping the email link resumes the backgrounded app straight into /r/<reportID>. If that OpenReport never fires or its response never lands, src/pages/inbox/ReportActions.tsx:72 returns the skeleton forever. The report in this repro has two transactions, so total !== 0 is guaranteed.
What changes do you think we should make in order to solve the problem?
Make the gate request-driven, not total-driven. In shouldWaitForTransactions, only treat "no transactions yet" as loading while an OpenReport for that report is actually pending. Drop report?.total !== 0 as a standalone reason to wait, or pair it with the loading flag the way the first term already is.
Add a bounded fallback. If transactions still haven't arrived after the request settles, render the report with an empty/error state rather than an indefinite skeleton. The same gate is used by src/components/MoneyRequestReportView/MoneyRequestReportView.tsx:156-158, so both call sites are covered by fixing the util.
Regression coverage should assert that an expense report with total !== 0 and no transactions stops showing the skeleton once its OpenReport has resolved.
Investigation details, alternatives considered, and what would confirm this
Confidence: medium. The unbounded gate is confirmed in code and was byte-identical in the reported build (9.4.74-0 = 40f2e907). What is not confirmed is whether OpenReport fails to fire on the warm deep-link path or fires and returns without transactions.
What would settle it: check whether an OpenReport request for that reportID was sent from the affected account right after the tap.
No request → the warm deep link never reached the fetch effect, and the navigation path is also at fault.
Request returned 200 → the response carried no transactions (or Onyx never merged them), and the gate above is the sole latch.
There is also a [OpenReportStall] alert at src/pages/inbox/ReportFetchHandler.tsx:501-505 that fires after 30s when the loading flag never clears. Note it will not fire for this case if OpenReport succeeded — its absence is itself a signal.
Alternatives ruled out:
The /transition handoff (the cause of the already-fixed web bug). Dead on Android: android/app/src/main/AndroidManifest.xml:72-93 claims only new.expensify.com and 21 path prefixes — /transition is not among them, and www.expensify.com is not claimed at all. Android hands both to the browser. getCurrentUrl() is also hardcoded to '' on native (src/libs/Navigation/currentUrl/index.native.ts:3), so the exitTo machinery is a no-op there regardless.
isInPreloadedTab blocking every fetchReport() call. It is a real hazard on current main, but it did not exist in 9.4.74 — useIsInPreloadedTab.ts landed with perf: preload the Reports tab so the first Inbox open runs the warm path, merged 12 days after this was reported. Verified with git ls-tree 40f2e9076a -- src/hooks/useIsInPreloadedTab.ts (empty).
Separate defect found along the way (not this bug, worth its own issue):setInitialURL in src/components/InitialURLContextProvider.tsx:40-47 has no external callers, so initialURL is a mount-time-only snapshot that is never refreshed for a warm onNewIntent link. Combined with src/libs/SessionUtils.ts:11-41, where isLoggingInAsNewUser(undefined, email) returns true, a stale snapshot can sign a valid session out. The symptom there is a sign-out, not a hang, so it isn't this issue.
Next Steps for Contributor+ team: To accept:@MelvinBot implement [this](https://github.com/Expensify/App/issues/100682) to create a draft PR. To refine:@MelvinBot <your feedback> To reject: Explain why you are rejecting Melvin's proposal.
changed the title [-][Exploratory] Navigation - App keeps on loading when opening expense from email[/-][+][$175] [Exploratory] Navigation - App keeps on loading when opening expense from email[/+]on Nov 2, 2026
The offline repro no longer goes anywhere near the expense-report skeleton — it hangs earlier, on the magic-link sign-in page itself.
On native, ValidateLoginPage (src/pages/ValidateLoginPage/index.tsx) is a deep-link entry point that renders nothing but <FullScreenLoadingIndicator shouldUseGoBackButton={false} />. When the user is signed out, it calls signInWithValidateCodeAndNavigate(accountID, validateCode, preferredLocale, exitTo), which does two things:
signInWithValidateCode(...) applies optimistic Onyx data (session.autoAuthState = SIGNING_IN, account.isLoading = true) and sends API.write(SIGN_IN_USER_WITH_LINK). Offline, that write is persisted to the offline queue and is not sent — so neither its successData (autoAuthState: JUST_SIGNED_IN) nor its failureData (autoAuthState: FAILED) ever lands. The page's only escape hatch — the useEffect that navigates back when autoAuthState === FAILED — therefore never fires.
Because the link carries exitTo, handleExitToNavigation(exitTo) is called, which awaits waitForUserSignIn() — an unbounded promise that resolves only when session.authToken becomes truthy. Offline, it never does.
Result: autoAuthState is stuck at SIGNING_IN, authToken never arrives, waitForUserSignIn() never resolves, and the page keeps rendering a bare centered spinner with no header, no error and no Go back button until connectivity returns. That matches the reported actual result exactly (empty screen, no header/error/Go back, unusable until the network is back).
Notes:
The hang is specific to links carrying exitTo. Without exitTo, signInWithValidateCodeAndNavigate calls Navigation.goBack() immediately, so the user lands on the sign-in page even offline.
The loading gate from the earlier proposal on this thread (shouldWaitForTransactions in MoneyRequestReportUtils.ts) is not involved in this repro on current main: it short-circuits with if (isOffline) return false, and the original online repro no longer occurs.
What is the expected behavior?
Per the issue: an offline message, the sign-in page, or at minimum a "This is taking longer than expected..." message with a Go back button after 10 seconds — instead of an indefinite bare spinner.
What is your proposed solution?
Wrap the page's loader in the existing FullPageOfflineBlockingView, on both platforms:
src/pages/ValidateLoginPage/index.tsx (native)
return(<FullPageOfflineBlockingView>{/* No "Go Back" button: this is a deep-link entry point, so there is usually no history to pop back to. */}<FullScreenLoadingIndicatorshouldUseGoBackButton={false}/></FullPageOfflineBlockingView>);
src/pages/ValidateLoginPage/index.web.tsx (web) — same wrap around the FullScreenLoadingIndicator rendered while autoAuthState === SIGNING_IN / isCompletingDirectSignIn.
Behavior:
While offline, FullPageOfflineBlockingView swaps the bare spinner for the standard blocking view ("You appear to be offline" / "This feature requires internet"), so the user can see why the page is stuck — the first expected outcome in the issue.
When the network returns, FullPageOfflineBlockingView swaps back to the loader, the queued SigninUserWithLink request is sent, sign-in completes, and the already-registered handleExitToNavigation(exitTo) takes the user to their expense report — the deep-link intent is preserved with no extra code.
Bailing out to the sign-in page when offline while autoAuthState === SIGNING_IN (an effect calling Navigation.goBack(), which lands on the sign-in page via the existing resetToAppRoot fallback). This also fixes the hang, but it navigates the user away mid-flow: they lose the "it just continues when connectivity returns" behavior, and the queued sign-in completing later can yank them out of whatever they started doing on the sign-in page. The offline blocking view keeps the user in place and the flow self-resumes.
Test plan
Unit tests:
tests/ui/LoadingIndicatorGoBackOptOutTest.tsx (native page): with the device offline, a magic link with exitTo renders the offline message instead of a bare ActivityIndicator, and still offers no "Go back"; when the network returns, the loader is back (still with no "Go back").
tests/ui/ValidateLoginPageTest.tsx (web page): offline + exitTo + autoAuthState: SIGNING_IN shows common.youAppearToBeOffline and no validate-login-loading; restoring connectivity brings the loader back.
Manual tests:
[Native Android/iOS] Sign out → go offline → open https://new.expensify.com/v/<accountID>/<code>?exitTo=/r/<reportID> → offline message is shown instead of the bare spinner.
[Native Android/iOS] Same, then restore connectivity → sign-in completes → app lands on the report from exitTo.
[Native] Signed in, offline → open the same link → still navigates to exitTo (no offline view blocking a signed-in user's handoff).
[Web] Repeat 1–2 in a browser.
[All platforms] Online regression: magic link with exitTo still signs in and lands on the report; magic link without exitTo still returns to the sign-in page.
📣 @Aleksandr-Zykov! 📣
Hey, it seems we don’t have your contributor details yet! You'll only have to do this once, and this is how we'll hire you on Upwork.
Please follow these steps:
Get the email address used to login to your Expensify account. If you don't already have an Expensify account, create one here. If you have multiple accounts (e.g. one for testing), please use your main account email.
Get the link to your Upwork profile. It's necessary because we only pay via Upwork. You can access it by logging in, and then clicking on your name. It'll look like this. If you don't already have an account, sign up for one here.
Copy the format below and paste it in a comment on this issue. Replace the placeholder text with your actual details.
Format:
Contributor details
Your Expensify account email: <REPLACE EMAIL HERE>
Upwork Profile Link: <REPLACE LINK HERE>
changed the title [-][$175] [Exploratory] Navigation - App keeps on loading when opening expense from email[/-][+][Due for payment 2026-11-10] [$175] [Exploratory] Navigation - App keeps on loading when opening expense from email[/+]on Nov 3, 2026
The solution for this issue has been 🚀 deployed to production 🚀 in version 9.5.30-0 and is now subject to a 7-day regression period 📆. Here is the list of pull requests that resolve this issue:
If no regressions arise, payment will be issued on 2026-11-10. 🎊
The following checklist (instructions) will need to be completed before the issue can be closed. Please copy/paste the Contributor+ Checklist from here into a new comment on this GH and complete it. If you have the K2 extension, you can simply click: [this button]. If no checklist is needed for this issue, you can click: [no checklist button]
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/b238bf30ac9b96db288b490494d0973d6cd7880c0c3924630f7a83f345eada0d
[-][Exploratory] Navigation- App keeps on loading when opening expense from email[/-][+][Exploratory] Navigation - App keeps on loading when opening expense from email[/+]MelvinBot commentedon Sep 24, 2026
Proposal
What is the root cause of that problem?
The expense report screen has a loading gate with no exit condition. It shows a skeleton whenever an expense report has a non-zero total but zero transactions in Onyx — with no timeout, no retry, and no error state.
src/libs/MoneyRequestReportUtils.ts:163:The
report?.total !== 0term is not tied to any in-flight request. Once it's true, the only way out is transactions arriving, the user going offline, or the total becoming0.That exactly matches the repro. The WS owner already has the report row (with its total) from
OpenApp, but not its transactions — those only arrive withOpenReport. Tapping the email link resumes the backgrounded app straight into/r/<reportID>. If thatOpenReportnever fires or its response never lands,src/pages/inbox/ReportActions.tsx:72returns the skeleton forever. The report in this repro has two transactions, sototal !== 0is guaranteed.What changes do you think we should make in order to solve the problem?
shouldWaitForTransactions, only treat "no transactions yet" as loading while anOpenReportfor that report is actually pending. Dropreport?.total !== 0as a standalone reason to wait, or pair it with the loading flag the way the first term already is.src/components/MoneyRequestReportView/MoneyRequestReportView.tsx:156-158, so both call sites are covered by fixing the util.Regression coverage should assert that an expense report with
total !== 0and no transactions stops showing the skeleton once itsOpenReporthas resolved.Investigation details, alternatives considered, and what would confirm this
Confidence: medium. The unbounded gate is confirmed in code and was byte-identical in the reported build (
9.4.74-0=40f2e907). What is not confirmed is whetherOpenReportfails to fire on the warm deep-link path or fires and returns without transactions.What would settle it: check whether an
OpenReportrequest for that reportID was sent from the affected account right after the tap.There is also a
[OpenReportStall]alert atsrc/pages/inbox/ReportFetchHandler.tsx:501-505that fires after 30s when the loading flag never clears. Note it will not fire for this case ifOpenReportsucceeded — its absence is itself a signal.Alternatives ruled out:
/transitionhandoff (the cause of the already-fixed web bug). Dead on Android:android/app/src/main/AndroidManifest.xml:72-93claims onlynew.expensify.comand 21 path prefixes —/transitionis not among them, andwww.expensify.comis not claimed at all. Android hands both to the browser.getCurrentUrl()is also hardcoded to''on native (src/libs/Navigation/currentUrl/index.native.ts:3), so theexitTomachinery is a no-op there regardless.isInPreloadedTabblocking everyfetchReport()call. It is a real hazard on currentmain, but it did not exist in9.4.74—useIsInPreloadedTab.tslanded with perf: preload the Reports tab so the first Inbox open runs the warm path, merged 12 days after this was reported. Verified withgit ls-tree 40f2e9076a -- src/hooks/useIsInPreloadedTab.ts(empty).Separate defect found along the way (not this bug, worth its own issue):
setInitialURLinsrc/components/InitialURLContextProvider.tsx:40-47has no external callers, soinitialURLis a mount-time-only snapshot that is never refreshed for a warmonNewIntentlink. Combined withsrc/libs/SessionUtils.ts:11-41, whereisLoggingInAsNewUser(undefined, email)returnstrue, a stale snapshot can sign a valid session out. The symptom there is a sign-out, not a hang, so it isn't this issue.Possibly related issues:
/transitionrace), fixed in9.4.78-3. Not a duplicate.Next Steps for Contributor+ team:
To accept:
@MelvinBot implement [this](https://github.com/Expensify/App/issues/100682)to create a draft PR.To refine:
@MelvinBot <your feedback>To reject: Explain why you are rejecting Melvin's proposal.
view run
melvin-bot commentedon Sep 24, 2026
Triggered auto assignment to Contributor-plus team member for initial proposal review - @mkhutornyi (
External)melvin-bot commentedon Sep 24, 2026
Unable to auto-create job on Upwork. The BZ team member should create it manually for this issue.
30 remaining items
[-][Exploratory] Navigation - App keeps on loading when opening expense from email[/-][+][$175] [Exploratory] Navigation - App keeps on loading when opening expense from email[/+]melvin-bot commentedon Nov 2, 2026
Job added to Upwork: https://www.upwork.com/jobs/~022117293371170771863
melvin-bot commentedon Nov 2, 2026
Triggered auto assignment to @AndrewGable, see https://stackoverflow.com/c/expensify/questions/7972 for more details.
Aleksandr-Zykov commentedon Nov 2, 2026
Proposal
What is the root cause of the problem?
The offline repro no longer goes anywhere near the expense-report skeleton — it hangs earlier, on the magic-link sign-in page itself.
On native,
ValidateLoginPage(src/pages/ValidateLoginPage/index.tsx) is a deep-link entry point that renders nothing but<FullScreenLoadingIndicator shouldUseGoBackButton={false} />. When the user is signed out, it callssignInWithValidateCodeAndNavigate(accountID, validateCode, preferredLocale, exitTo), which does two things:signInWithValidateCode(...)applies optimistic Onyx data (session.autoAuthState = SIGNING_IN,account.isLoading = true) and sendsAPI.write(SIGN_IN_USER_WITH_LINK). Offline, that write is persisted to the offline queue and is not sent — so neither itssuccessData(autoAuthState: JUST_SIGNED_IN) nor itsfailureData(autoAuthState: FAILED) ever lands. The page's only escape hatch — theuseEffectthat navigates back whenautoAuthState === FAILED— therefore never fires.exitTo,handleExitToNavigation(exitTo)is called, which awaitswaitForUserSignIn()— an unbounded promise that resolves only whensession.authTokenbecomes truthy. Offline, it never does.Result:
autoAuthStateis stuck atSIGNING_IN,authTokennever arrives,waitForUserSignIn()never resolves, and the page keeps rendering a bare centered spinner with no header, no error and no Go back button until connectivity returns. That matches the reported actual result exactly (empty screen, no header/error/Go back, unusable until the network is back).Notes:
exitTo. WithoutexitTo,signInWithValidateCodeAndNavigatecallsNavigation.goBack()immediately, so the user lands on the sign-in page even offline.shouldWaitForTransactionsinMoneyRequestReportUtils.ts) is not involved in this repro on currentmain: it short-circuits withif (isOffline) return false, and the original online repro no longer occurs.What is the expected behavior?
Per the issue: an offline message, the sign-in page, or at minimum a "This is taking longer than expected..." message with a Go back button after 10 seconds — instead of an indefinite bare spinner.
What is your proposed solution?
Wrap the page's loader in the existing
FullPageOfflineBlockingView, on both platforms:src/pages/ValidateLoginPage/index.tsx(native)src/pages/ValidateLoginPage/index.web.tsx(web) — same wrap around theFullScreenLoadingIndicatorrendered whileautoAuthState === SIGNING_IN/isCompletingDirectSignIn.Behavior:
FullPageOfflineBlockingViewswaps the bare spinner for the standard blocking view ("You appear to be offline" / "This feature requires internet"), so the user can see why the page is stuck — the first expected outcome in the issue.FullPageOfflineBlockingViewswaps back to the loader, the queuedSigninUserWithLinkrequest is sent, sign-in completes, and the already-registeredhandleExitToNavigation(exitTo)takes the user to their expense report — the deep-link intent is preserved with no extra code.shouldUseGoBackButton={false}opt-out is deliberately kept, matching the review decision in PR Enable the Go Back button by default on FullScreenLoadingIndicator #99786 (a "Go back" from a cold deep-link entry point would be dead).Alternative considered and rejected
Bailing out to the sign-in page when offline while
autoAuthState === SIGNING_IN(an effect callingNavigation.goBack(), which lands on the sign-in page via the existingresetToAppRootfallback). This also fixes the hang, but it navigates the user away mid-flow: they lose the "it just continues when connectivity returns" behavior, and the queued sign-in completing later can yank them out of whatever they started doing on the sign-in page. The offline blocking view keeps the user in place and the flow self-resumes.Test plan
Unit tests:
tests/ui/LoadingIndicatorGoBackOptOutTest.tsx(native page): with the device offline, a magic link withexitTorenders the offline message instead of a bareActivityIndicator, and still offers no "Go back"; when the network returns, the loader is back (still with no "Go back").tests/ui/ValidateLoginPageTest.tsx(web page): offline +exitTo+autoAuthState: SIGNING_INshowscommon.youAppearToBeOfflineand novalidate-login-loading; restoring connectivity brings the loader back.Manual tests:
https://new.expensify.com/v/<accountID>/<code>?exitTo=/r/<reportID>→ offline message is shown instead of the bare spinner.exitTo.exitTo(no offline view blocking a signed-in user's handoff).exitTostill signs in and lands on the report; magic link withoutexitTostill returns to the sign-in page.melvin-bot commentedon Nov 2, 2026
📣 @Aleksandr-Zykov! 📣
Hey, it seems we don’t have your contributor details yet! You'll only have to do this once, and this is how we'll hire you on Upwork.
Please follow these steps:
Format:
Aleksandr-Zykov commentedon Nov 2, 2026
Contributor details
Your Expensify account email: zich200877@gmail.com
Upwork Profile Link: https://www.upwork.com/freelancers/~0149d8dbd138411f3d
melvin-bot commentedon Nov 2, 2026
✅ Contributor details stored successfully. Thank you for contributing to Expensify!
[-][$175] [Exploratory] Navigation - App keeps on loading when opening expense from email[/-][+][Due for payment 2026-11-10] [$175] [Exploratory] Navigation - App keeps on loading when opening expense from email[/+]melvin-bot commentedon Nov 3, 2026
@mkhutornyi
The solution for this issue has been 🚀 deployed to production 🚀 in version 9.5.30-0 and is now subject to a 7-day regression period 📆. Here is the list of pull requests that resolve this issue:
If no regressions arise, payment will be issued on 2026-11-10. 🎊
The following checklist (instructions) will need to be completed before the issue can be closed. Please copy/paste the Contributor+ Checklist from here into a new comment on this GH and complete it. If you have the K2 extension, you can simply click: [this button]. If no checklist is needed for this issue, you can click: [no checklist button]