Skip to content

[Due for payment 2026-05-13] [$250] HybridApp - Switch to Expensify Classic silently does nothing on mobile when NVP_TRY_NEW_DOT is populated before the SESSION callback fires #88243

@Julesssss

Description

@Julesssss

Version Number: v9.3.60-17
Reproducible in staging?: Yes
Reproducible in production?: Likely (introduced in #87101, deployed 2026-04-08)
Email or phone of affected tester (no customers):
Issue reported by: @Julesssss (noticed while verifying #88182 fix)

Action Performed

  1. Open the NewDot app on HybridApp (iOS or Android Native)
  2. Do not start a GPS trip
  3. Navigate to Account -> Troubleshoot
  4. Tap "Switch to Expensify Classic"

Expected Result

App hands off to Expensify Classic. NVP_TRY_NEW_DOT.classicRedirect.dismissed flips to true server-side.

Actual Result

Nothing happens. User stays in NewDot. NVP_TRY_NEW_DOT is unchanged server-side:

{
  "classicRedirect": { "dismissed": false, "timestamp": "2026-04-07T15:20:32.352Z" },
  "distance": { "enabled": "true" },
  "smartScan": { "enabled": "true" },
  "tappedTryNewExpensifyButton": { "timestamp": "2026-04-07T15:20:32.352Z" }
}

Web is unaffected (web does not call closeReactNativeApp for this flow).

Root Cause

In src/libs/actions/HybridApp/index.ts:54-67, the SESSION callback unconditionally resets module-level currentTryNewDot = undefined and isLoadingTryNewDot = true on the first transition from undefined to the user's accountID:

Onyx.connectWithoutView({
    key: ONYXKEYS.SESSION,
    callback: (session) => {
        const nextSessionAccountID = getSessionAccountID(session);
        if (nextSessionAccountID === currentSessionAccountID) return;
        currentSessionAccountID = nextSessionAccountID;
        currentTryNewDot = undefined;
        hasReceivedTryNewDotUpdate = false;
        isLoadingTryNewDot = nextSessionAccountID !== undefined || isLoadingApp !== false;
    },
});

If the NVP_TRY_NEW_DOT callback fires before the SESSION callback on app start (common on mobile), currentTryNewDot is set, then immediately blanked. Because the Onyx NVP value itself does not change, the NVP callback never re-fires to repopulate it. The module is permanently stuck with isLoadingTryNewDot = true.

Any subsequent call to closeReactNativeApp({shouldSetNVP: true}) hits the loading-state branch of shouldBlockOldAppExit at index.ts:87 and silently returns. The Troubleshoot button is still rendered because the component-level useOnyx sees the NVP value fine — this is the desync.

Introduced by #87101 (merged 2026-04-08).

Proposed Fix

Skip the module-level reset on the initial undefined -> accountID transition. Only blank currentTryNewDot when the user actually changes (logout/login or cross-user switch):

if (nextSessionAccountID === currentSessionAccountID) return;
const isInitialSessionLoad = currentSessionAccountID === undefined;
currentSessionAccountID = nextSessionAccountID;
if (isInitialSessionLoad) return;
currentTryNewDot = undefined;
hasReceivedTryNewDotUpdate = false;
isLoadingTryNewDot = nextSessionAccountID !== undefined || isLoadingApp !== false;

Existing tests that codify the re-block-on-session-switch behavior (tests/unit/HybridAppActionsTest.ts:118-156) continue to pass because they test a real accountID change, not the initial load.

Workaround

None. Users on mobile who land in this state cannot switch to Classic until they fully sign out and sign back in (which may also fail on first session). Web is unaffected.

Platforms

  • Android: App
  • Android: mWeb Chrome
  • iOS: App
  • iOS: mWeb Safari
  • iOS: mWeb Chrome
  • Windows: Chrome
  • MacOS: Chrome / Safari

Related

View all open jobs on GitHub

Upwork Automation - Do Not Edit
  • Upwork Job URL: https://www.upwork.com/jobs/~022045263129318285513
  • Upwork Job ID: 2045263129318285513
  • Last Price Increase: 2026-04-17
Issue OwnerCurrent Issue Owner: @abzokhattab

Metadata

Metadata

Labels

Awaiting PaymentAuto-added when associated PR is deployed to productionBugSomething is broken. Auto assigns a BugZero manager.DailyKSv2EngineeringExternalAdded to denote the issue can be worked on by a contributor

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions