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
- Open the NewDot app on HybridApp (iOS or Android Native)
- Do not start a GPS trip
- Navigate to Account -> Troubleshoot
- 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
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 Owner
Current Issue Owner: @abzokhattab
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
Expected Result
App hands off to Expensify Classic.
NVP_TRY_NEW_DOT.classicRedirect.dismissedflips totrueserver-side.Actual Result
Nothing happens. User stays in NewDot.
NVP_TRY_NEW_DOTis 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
closeReactNativeAppfor this flow).Root Cause
In
src/libs/actions/HybridApp/index.ts:54-67, the SESSION callback unconditionally resets module-levelcurrentTryNewDot = undefinedandisLoadingTryNewDot = trueon the first transition fromundefinedto the user'saccountID:If the NVP_TRY_NEW_DOT callback fires before the SESSION callback on app start (common on mobile),
currentTryNewDotis 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 withisLoadingTryNewDot = true.Any subsequent call to
closeReactNativeApp({shouldSetNVP: true})hits the loading-state branch ofshouldBlockOldAppExitatindex.ts:87and silently returns. The Troubleshoot button is still rendered because the component-leveluseOnyxsees 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 -> accountIDtransition. Only blankcurrentTryNewDotwhen the user actually changes (logout/login or cross-user switch):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
Related
View all open jobs on GitHub
Upwork Automation - Do Not Edit
Issue Owner
Current Issue Owner: @abzokhattab