Repository navigation
Add beta override switches to the Test Tool menu - #99444
Conversation
|
Hey, I noticed you changed If you want to automatically generate translations for other locales, an Expensify employee will have to:
Alternatively, if you are an external contributor, you can run the translation script locally with your own OpenAI API key. To learn more, try running: npx bun ./scripts/generateTranslations.ts --helpTypically, you'd want to translate only what you changed by running |
Codecov Report✅ Changes either increased or maintained existing code coverage, great job!
|
|
@DylanDylann Please copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button] |
|
This PR adds a new |
| text={translate('initialSettingsPage.troubleshoot.overridden')} | ||
| isCondensed | ||
| // The condensed badge keeps the pill compact next to the beta name, but its 9px text is too small to read at a glance. | ||
| textStyles={{fontSize: variables.fontSizeSmall}} |
There was a problem hiding this comment.
❌ UI-3 (docs)
This passes an inline style object literal (textStyles={{fontSize: variables.fontSizeSmall}}) to a style-typed prop. Per STYLING.md, inline style objects are not allowed: they allocate a new object every render (breaking memoization), bypass theming, and duplicate a value that belongs in the shared style system. The value here is static (variables.fontSizeSmall) with no render-time computation, so it does not qualify for the dynamic-style exception.
Move it into src/styles (e.g. add a styles.badgeTextCondensed or similar helper) and pass the token instead. Badge's textStyles prop is typed StyleProp<TextStyle>, so it accepts a shared style reference or array:
<Badge
text={translate('initialSettingsPage.troubleshoot.overridden')}
isCondensed
textStyles={styles.badgeTextCondensed}
success
/>Reviewed at: 88918cd | Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.
|
@Expensify/design could you please take a look on UI here? 🙏 This adds a "Beta overrides" row to the Test Tool menu that opens a list of every beta with a switch each, so betas can be flipped locally without editing Onyx in the console. I went with a modal for now because it seemed like the best fit: the row lives in Does this look good to you overall? You can see it on web, Android and iOS in the Screenshots/Videos section above. A couple of details I am unsure about:
Happy to change any of it. |
|
One thing I would like a second opinion on: the override semantics. Toggling a beta always pins an explicit value. Turning it on stores Is pinning what you would expect, or would you rather toggling back removed the override? |
|
Gotta say, based on the video, I feel like an RHP would be way more appropriate for this. Curious for the other designers' thoughts, but that super tall off-centered modal feels pretty weird to me. I don't mind the badge you're using, and I'm not sure what my opinion is about the raw key names—is this mostly for internal testing? Do we think users will go in here looking for a beta to enable? |
|
@dannymcclain yes, this is for internal testing. It shows up in two places, the Test Tools menu (Cmd+D) and Settings > Troubleshoot, and it is hidden in production builds, so only devs and QA ever see it |
|
|
||
| import getEnvironment from './Environment/getEnvironment'; | ||
|
|
||
| // Module-level so the non-render callers of isBetaEnabled (actions, utils, guards) respect overrides without subscribing to this key. |
There was a problem hiding this comment.
so the non-render callers of isBetaEnabled (actions, utils, guards)
This is only permissible if ALL these callers are triggered from non-render code (which I think is unlikely). It's not proper to use this as a shortcut for getting data. I think the proper way to do this is to update all callers to pass the data as a parameter to isBetaEnabled. If there are non-UI callers, then they can use connectWithoutView() to get the data and pass it along. All the UI-callers (or anything triggered from a UI caller) should use either usePermissions or useOnyx.
@mountiny @luacmartins do you have any different thoughts about this one?
There was a problem hiding this comment.
Yea, I agree that we should follow our pattern and pass in the params if the UI is the call site.
There was a problem hiding this comment.
@arekm213 Let's remove Onyx.connectWithoutView here. In the component, we can use the usePermission hook instead, and for the util functions, we can pass betasOverride as a parameter, as the same way we handle the betas param.
That said, to keep this PR in scope and avoid a huge PR, I'd suggest splitting that work into a separate PR rather than growing this one.
There was a problem hiding this comment.
Removed the Onyx.connectWithoutView from Permissions.ts - isBetaEnabled takes betasOverride as a parameter now. The remaining util functions still need it passed through, so I'll do that in a separate PR as @DylanDylann suggested
…-beta-overrides # Conflicts: # src/components/TestToolMenu.tsx
|
@DylanDylann just reminding - conflicts are resolved again, waiting for your review |
Reviewer Checklist
Screenshots/VideosAndroid: HybridAppScreen.Recording.2026-09-08.at.11.21.55.movAndroid: mWeb ChromeScreen.Recording.2026-09-08.at.11.11.41.moviOS: HybridAppScreen.Recording.2026-09-08.at.11.20.45.moviOS: mWeb SafariScreen.Recording.2026-09-08.at.11.07.58.movMacOS: Chrome / SafariScreen.Recording.2026-09-08.at.11.05.56.mov |
|
@mountiny all yours |
| ); | ||
| } | ||
|
|
||
| BetaOverridesPage.displayName = 'BetaOverridesPage'; |
There was a problem hiding this comment.
Not needed
| BetaOverridesPage.displayName = 'BetaOverridesPage'; |
|
🚧 mountiny has triggered a test Expensify/App build. You can view the workflow run here. |
|
✋ This PR was not deployed to staging yet because QA is ongoing. It will be automatically deployed to staging after the next production release. |
|
🧪🧪 Use the links below to test this adhoc build on Android, iOS, and Web. Happy testing! 🧪🧪
|
|
🚀 Deployed to staging by https://github.com/mountiny in version: 9.4.75-0 🚀
|
|
🤖 No help site changes are required for this PR. I did not create a docs PR. Why: "Beta overrides" is a developer/QA tool that customers can never reach in production. Both entry points are gated behind
The help site documents production behavior for customers, so a dev-only switch panel has no place in it. Docs I checked
The other user-visible strings this PR adds ( I skipped the closing "please review the linked help site PR" line, since there is no help site PR to link. If you want an article written anyway — for example an internal-facing note on using beta overrides during testing — reply with |
|
Deploy Blocker #100769 was identified to be related to this PR. |
|
🚀 Deployed to production by https://github.com/mountiny in version: 9.4.75-3 🚀
Bundle Size Analysis (Sentry): |
|
🚀 Deployed to production by https://github.com/mountiny in version: 9.4.75-3 🚀
Bundle Size Analysis (Sentry): |
| let isProductionEnvironment = CONFIG.ENVIRONMENT === CONST.ENVIRONMENT.PRODUCTION; | ||
| isProduction() | ||
| .then((value) => { | ||
| isProductionEnvironment = value; |
There was a problem hiding this comment.
This line caused [Due for payment 2026-10-15] Home-Changing font size to maximum not showing insights in footer.
isProductionEnvironment starts as true on Android staging, because staging and production ship the same binary and CONFIG.ENVIRONMENT is production. This line corrects it to false after isProduction() resolves. But it only mutates a plain module variable, so React never learns about the change.
usePermissions memoizes on [betas, betaConfiguration, betaOverrides]. None of those change when the environment settles, so isBetaEnabled keeps the stale result. On a cold start, the tab bar renders before the environment resolves, ignores the override, and never re-renders. That's why the Insights tab was missing while the Beta overrides page still showed it as enabled. Toggling the override changed betaOverrides, which is why that fixed it.
Lesson: Don't gate render-time logic on async state stored in a module variable. Put it in React state (for example, useEnvironment()) and pass it in, so memoized consumers recompute when it settles. Re-resolve beta overrides once the environment settles fixed it this way.

Explanation of Change
Betas can only be flipped locally today by editing Onyx by hand in the console (
Onyx.merge('betas', [...betas, 'submit2026'])), which is easy to get wrong and gets wiped on the next reconnect.This adds a Beta overrides row to the Test Tool menu that opens a page listing every beta in
CONST.BETASwith a switch each, so a beta can be turned on or off on the current device without touching the console. The row renders fromTestToolMenu, so it is available both in the hidden Test Tools modal and inline on Settings > Troubleshoot.The page lives in the RHP on its own route (
/beta-overrides), outside Settings, following the design discussion in the thread below.Behavior:
Overriddenbadge.clearOnyxAndResetApp, so a testing setup is not lost by logging out.Implementation notes:
ONYXKEYS.BETA_OVERRIDESkey (Partial<Record<Beta, boolean>>) rather than writing intoBETAS, because the server re-pushesBETASon every OpenApp/ReconnectApp and would wipe anything stored there.Permissions.isBetaEnabled, which takes the overrides as a parameter, so nothing inPermissions.tsreads Onyx. Components get the value throughusePermissions, which reads it from a newBetaOverridesContext, and the remaining non-React caller (TransactionInlineEdit) keeps its ownOnyx.connectWithoutViewwith a comment explaining why, as Onyx Data Management requires.allbeta is excluded from the list, since it is a blanket switch rather than an individual feature.Interim gap, handled in a follow up PR to keep this one small: seven util call sites still resolve their beta from a
betasarray without receiving the overrides, so an override does not reach them yet. They areasapSubmit(ReportUtils.getExpenseReportStateAndStatus,ReportUtils.shouldCreateNewMoneyRequestReport,IOUAmountSubmission.buildSubmitAmountContext,Search.getApproveActionCallback,Policy.setWorkspaceApprovalMode),defaultRooms(ReportUtils.canSeeDefaultRoom) andvendorMatching(ViolationsUtils.getViolationsOnyxData). The follow up threads the parameter through, grouped by beta so no beta is left half working. Everything reached throughusePermissionsalready honours overrides.Proposed by @JakubKorytko.
Fixed Issues
$ #98409
PROPOSAL:
Tests
/test-toolsor press Cmd/Ctrl+D). Alternatively go to Settings > Troubleshoot.customAgenton. Verify the switch flips and anOverriddenbadge appears next to the name.customAgentthroughusePermissions, so the override reaches it.customAgentback off. Verify the badge disappears, since the override is dropped once the value matches your account again, and verify the Agents row disappears too./beta-overridesalso works as a direct deeplink, and that the back button returns to whatever was open before it.Offline tests
N/A. Overrides are stored locally in Onyx and only affect frontend checks, so behavior is identical offline. Note that force enabling a beta the account does not have on the backend can still fail at the request level once online, which the page calls out in its description.
QA Steps
Same as Tests.
PR Author Checklist
### Fixed Issuessection aboveTestssectionOffline stepssectionQA stepssectionAvatar, I verified the components usingAvatarare working as expected)StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))npm run compress-svg)Avataris modified, I verified thatAvataris working as expected in all cases)Designlabel and/or tagged@Expensify/designso the design team can review the changes.mainbranch was merged into this PR after a review, I tested again and verified the outcome was still expected according to theTeststeps.Screenshots/Videos
Android: Native
Android: mWeb Chrome
iOS: Native
iOS: mWeb Safari
MacOS: Chrome / Safari