If you haven't already, check out our contributing guidelines for onboarding and email contributors@expensify.com to request to join our Slack channel!
Version Number: 9.4.25-1 (native shell) / stale NewDot bundle 9.3.91-0
Reproducible in staging?: Needs Reproduction (Unable to reproduce — single isolated server-side event)
Reproducible in production?: Needs Reproduction
If this was caught during regression testing, add the test name, ID and link from BrowserStack:
Email or phone of affected tester (no customers): cole@expensify.com
Logs: VictoriaLogs investigation — request_id a143bbd4a9ef3335-SJC on 2026-07-01T07:35:23Z
Expensify/Expensify Issue URL:
Issue reported by: @vit (internal investigation)
Slack conversation: #Expensify Bugs — cole reported 2026-07-02 ~4:39 PM
Action Performed:
- Open the iOS native app on the latest TestFlight staging build
- Use the app normally (background/resume)
- Observe the full-screen Update required blocker
Expected Result:
Users on the latest staging build should never see the forced-update screen.
Actual Result:
cole@expensify.com saw the Update required / Please update to the latest version of New Expensify screen on iOS native TestFlight while already on the latest staging build. Hard-quitting the app cleared the screen and the app worked normally afterward.
Workaround:
Force-quit and reopen the app.
Platforms:
Select the officially supported platforms where the issue was reproduced:
Issue observed in the following devices
- iOS (TestFlight staging)
- Native shell:
Expensify/9.4.25.1 CFNetwork/3860.600.12 Darwin/25.5.0
- Screenshot attached in Slack thread (
IMG_3948.PNG)
Screenshots/Videos
See Slack thread screenshot — full-screen Update Required blocker with green Update button.
Investigation summary (for implementers)
What triggered the screen
The forced-update UI is shown when any authenticated API returns jsonCode: 426:
- Backend (Web-Expensify/lib/Request.php) compares
appversion param against MINIMUM_APP_VERSION (9.4.9 at time of incident)
- Frontend (App/src/libs/HttpUtils.ts) calls
alertUser() → sets ONYXKEYS.RAM_ONLY_UPDATE_REQUIRED
- App/src/Expensify.tsx throws
CONST.ERROR.UPDATE_REQUIRED → error boundary renders App/src/pages/ErrorPage/UpdateRequiredView.tsx
The flag is RAM-only (App/src/setup/index.ts ramOnlyKeys) and is never cleared in-session — only a process restart resets it. That explains the self-recovery after hard-quit.
Exactly one request triggered the 426 across the entire Jun 15 – Jul 3 window:
| Field |
Value |
| Timestamp |
2026-07-01T07:35:23Z |
| request_id |
a143bbd4a9ef3335-SJC |
| command |
ReconnectApp |
| platform |
ios |
| referer |
ecash |
| appversion in request body |
9.3.91-0 |
| nginx User-Agent (native shell) |
Expensify/9.4.25.1 |
| minimum at time |
9.4.9 |
| needsUpgrade |
true |
| jsonCode |
426 |
Log sequence:
[ForceAppUpgrade] Comparing app version to minimum ~~ appVersion: '9.3.91-0' minimum: '9.4.9' needsUpgrade: '1'
Deprecated app version detected - forcing the user to upgrade
API threw ExpError ... exceptionJsonCode: '426'
All other cole requests that day used current versions (ecash9.4.24-0, ecash9.4.25-1, iphone9.4.25-1). The stale ecash9.3.91-0 source appeared once total.
Root cause hypothesis
The iOS HybridApp ran a stale NewDot JS bundle (9.3.91-0) inside a current native shell (9.4.25.1). The appversion sent on API calls comes from bundled package.json (App/src/libs/Network/enhanceParameters.ts → appversion: pkg.version), so it reflected the old JS bundle, not the native binary version.
On app resume, ReconnectApp fired with the stale version → backend correctly returned 426 → client stuck on Update Required until restart loaded a fresh bundle.
Open question for implementer: How did HybridApp retain/serve bundle 9.3.91-0 after a TestFlight update to 9.4.25? Investigate bundle loading, OTA update, and cache invalidation in Mobile-Expensify / @expensify/react-native-hybrid-app.
What this is NOT
- Not a backend false positive —
version_compare('9.3.91-0', '9.4.9', '<') is correct
- Not cole running an actually outdated build — native User-Agent confirms
9.4.25.1
- Not related to the recent minimum-version bump logic being wrong — the stale bundle predates the bump
Proposed fix direction
- Investigate HybridApp bundle lifecycle — ensure NewDot JS bundle version always matches or is invalidated when native app updates (TestFlight/OTA)
- Add defensive logging — when native shell version ≠ bundled
pkg.version, log/Sentry so future occurrences are diagnosable
- (Optional follow-up, separate issue) — auto-recover from transient 426 if subsequent requests succeed with current version
Key files
Upwork Automation - Do Not Edit
Issue Owner
Current Issue Owner: @mkhutornyi
If you haven't already, check out our contributing guidelines for onboarding and email contributors@expensify.com to request to join our Slack channel!
Version Number: 9.4.25-1 (native shell) / stale NewDot bundle 9.3.91-0
Reproducible in staging?: Needs Reproduction (Unable to reproduce — single isolated server-side event)
Reproducible in production?: Needs Reproduction
If this was caught during regression testing, add the test name, ID and link from BrowserStack:
Email or phone of affected tester (no customers): cole@expensify.com
Logs: VictoriaLogs investigation — request_id
a143bbd4a9ef3335-SJCon 2026-07-01T07:35:23ZExpensify/Expensify Issue URL:
Issue reported by: @vit (internal investigation)
Slack conversation: #Expensify Bugs — cole reported 2026-07-02 ~4:39 PM
Action Performed:
Expected Result:
Users on the latest staging build should never see the forced-update screen.
Actual Result:
cole@expensify.com saw the Update required / Please update to the latest version of New Expensify screen on iOS native TestFlight while already on the latest staging build. Hard-quitting the app cleared the screen and the app worked normally afterward.
Workaround:
Force-quit and reopen the app.
Platforms:
Select the officially supported platforms where the issue was reproduced:
Issue observed in the following devices
Expensify/9.4.25.1 CFNetwork/3860.600.12 Darwin/25.5.0IMG_3948.PNG)Screenshots/Videos
See Slack thread screenshot — full-screen Update Required blocker with green Update button.
Investigation summary (for implementers)
What triggered the screen
The forced-update UI is shown when any authenticated API returns
jsonCode: 426:appversionparam againstMINIMUM_APP_VERSION(9.4.9at time of incident)alertUser()→ setsONYXKEYS.RAM_ONLY_UPDATE_REQUIREDCONST.ERROR.UPDATE_REQUIRED→ error boundary renders App/src/pages/ErrorPage/UpdateRequiredView.tsxThe flag is RAM-only (App/src/setup/index.ts
ramOnlyKeys) and is never cleared in-session — only a process restart resets it. That explains the self-recovery after hard-quit.Server logs for cole@expensify.com (2026-07-01)
Exactly one request triggered the 426 across the entire Jun 15 – Jul 3 window:
2026-07-01T07:35:23Za143bbd4a9ef3335-SJCReconnectAppiosecash9.3.91-0Expensify/9.4.25.19.4.9true426Log sequence:
All other cole requests that day used current versions (
ecash9.4.24-0,ecash9.4.25-1,iphone9.4.25-1). The staleecash9.3.91-0source appeared once total.Root cause hypothesis
The iOS HybridApp ran a stale NewDot JS bundle (
9.3.91-0) inside a current native shell (9.4.25.1). Theappversionsent on API calls comes from bundledpackage.json(App/src/libs/Network/enhanceParameters.ts →appversion: pkg.version), so it reflected the old JS bundle, not the native binary version.On app resume,
ReconnectAppfired with the stale version → backend correctly returned 426 → client stuck on Update Required until restart loaded a fresh bundle.Open question for implementer: How did HybridApp retain/serve bundle
9.3.91-0after a TestFlight update to9.4.25? Investigate bundle loading, OTA update, and cache invalidation in Mobile-Expensify /@expensify/react-native-hybrid-app.What this is NOT
version_compare('9.3.91-0', '9.4.9', '<')is correct9.4.25.1Proposed fix direction
pkg.version, log/Sentry so future occurrences are diagnosableKey files
appversionUpwork Automation - Do Not Edit
Issue Owner
Current Issue Owner: @mkhutornyi