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-05-07] [Due for payment 2026-05-05] [$250] "Switch to Expensify Classic" button on Settings page leads to 404 due to missing entryScreen #88986
EXIT_SURVEY_REASON.entryScreens in ROUTES.ts:346-349 only permits Settings_Troubleshoot. However, the "Switch to Expensify Classic" button also exists on InitialSettingsPage.tsx:340-341, which calls createDynamicRoute(EXIT_SURVEY_REASON.path) without a basePath.
On wide screens, the default central pane is Settings_Profile (SettingsSplitNavigator.tsx:40), so createDynamicRoute builds settings/profile/exit-survey/reason. getStateFromPath then resolves Settings_Profile as the focused route, which is not in entryScreens, so it logs a warning and falls through to RNGetStateFromPath which can't match the path → 404.
The same button on TroubleshootPage.tsx:145 works fine because the focused route IS Settings_Troubleshoot.
Console warning: [getStateFromPath.ts][DynamicRoute] Focused route Settings_Profile is not allowed to access dynamic route with suffix exit-survey/reason - ""
Fix
Add SCREENS.SETTINGS.PROFILE.ROOT to EXIT_SURVEY_REASON.entryScreens in ROUTES.ts.
changed the title [-]"Switch to Expensify Classic" button on Settings page leads to 404 due to missing entryScreen[/-][+][$250] "Switch to Expensify Classic" button on Settings page leads to 404 due to missing entryScreen[/+]on Apr 27, 2026
Please re-state the problem that we are trying to solve in this issue.
When a user clicks "Switch to Expensify Classic" from the main Settings page (InitialSettingsPage), it causes a 404 instead of opening the exit survey, which prevents the user from completing the expected switch flow from that settings entry point.
What is the root cause of that problem?
The issue is caused by how dynamic settings routes are validated in src/ROUTES.ts and resolved from the current settings context. createDynamicRoute() builds the exit survey URL relative to the currently focused central pane screen, and getStateFromPath only allows that route when the focused screen is listed in the route's entryScreens configuration.
For DYNAMIC_ROUTES.EXIT_SURVEY_REASON in src/ROUTES.ts, entryScreens includes SCREENS.SETTINGS.TROUBLESHOOT but does not include SCREENS.SETTINGS.PROFILE.ROOT. On wide screens, SettingsSplitNavigator in src/libs/Navigation/AppNavigator/Navigators/SettingsSplitNavigator.tsx uses SCREENS.SETTINGS.PROFILE.ROOT as the defaultCentralScreen, so opening the survey from InitialSettingsPage resolves from Settings_Profile. Because that screen is not allowed in entryScreens, getStateFromPath rejects the route and the app falls through to an unmatched path, which results in the 404.
The same configuration gap exists for DYNAMIC_ROUTES.EXIT_SURVEY_CONFIRM in src/ROUTES.ts, whose entryScreens also omits SCREENS.SETTINGS.PROFILE.ROOT. As a result, the direct confirm-step path fails in the same way when InitialSettingsPage navigates there without first opening the reason step.
What changes do you think we should make in order to solve the problem?
src/ROUTES.ts — add SCREENS.SETTINGS.PROFILE.ROOT to EXIT_SURVEY_REASON.entryScreens.
src/ROUTES.ts — add SCREENS.SETTINGS.PROFILE.ROOT to EXIT_SURVEY_CONFIRM.entryScreens.
What alternative solutions did you explore? (Optional)
Passing an explicit basePath of settings/troubleshoot to createDynamicRoute from InitialSettingsPage would force the route to resolve through the Troubleshoot screen. This was rejected because it hard-codes the main Settings page to a sibling screen's path and makes the flow depend on that URL structure instead of the route's own allowed entry screens.
When the user clicks "Switch to Expensify Classic" from the sidebar (InitialSettingsPage.tsx:340), createDynamicRoute builds the path relative to the current central pane (e.g. /settings/profile/exit-survey/reason). getStateFromPath then extracts Settings_Profile as the focused route, finds it's not in entryScreens, and the route falls through to a 404.
What changes do you think we should make in order to solve the problem?
Add all settings central pane screens to EXIT_SURVEY_REASON.entryScreens in src/ROUTES.ts:346-349. The sidebar button can be clicked while any central pane is focused (not just Profile), so we need to allow all of them. The same fix should also be applied to EXIT_SURVEY_CONFIRM at line 350-353, since InitialSettingsPage can navigate directly to it as well (line 344).
Alternatively, if every settings screen should be able to reach the exit survey, using the wildcard '*' in entryScreens would be the simplest and most future-proof approach.
What alternative solutions did you explore? (Optional)
Adding only SCREENS.SETTINGS.PROFILE.ROOT (as suggested in the issue body) fixes the default case but leaves the bug reproducible when the user has navigated to any other central pane (Preferences, Wallet, Security, etc.) before clicking the sidebar button.
Next Steps for Contributor+ team: Reply with @MelvinBot implement this to create a draft PR, @MelvinBot <your feedback> to refine this analysis, or explain why you are rejecting Melvin's proposal.
Please re-state the problem that we are trying to solve in this issue.
On wide layouts, clicking the Switch to Expensify Classic button on the Settings page (the entry shown in the general menu of InitialSettingsPage) navigates to a 404 page instead of opening the exit-survey reason step.
The same button on the Troubleshoot page works correctly, so the bug is specific to the entry point on the main Settings screen.
What is the root cause of that problem?
The handler that runs when the button is clicked builds the destination URL with createDynamicRoute:
createDynamicRoute is called without an explicit basePath, so it defaults to Navigation.getActiveRoute() and concatenates the dynamic suffix to whatever URL is currently active:
On wide layouts, the central pane of SettingsSplitNavigator defaults to Settings_Profile, so the active URL when the user is on Settings is /settings/profile:
The combined target therefore becomes /settings/profile/exit-survey/reason. When getStateFromPath resolves that URL, the focused route is Settings_Profile, but the EXIT_SURVEY_REASON dynamic route only allows Settings_Troubleshoot as an entry screen:
Because Settings_Profile is not in entryScreens, the dynamic resolver logs a warning (Focused route Settings_Profile is not allowed to access dynamic route with suffix exit-survey/reason) and falls back to RNGetStateFromPath, which has no static match for settings/profile/exit-survey/reason and renders the 404 NotFoundPage.
The Troubleshoot version works because the user is already focused on Settings_Troubleshoot at the moment of the click, which is in the allow-list.
The same handler in InitialSettingsPage also has a second branch that navigates straight to EXIT_SURVEY_CONFIRM when the classic-redirect prompt was previously dismissed (shouldOpenSurveyReasonPage === false), and EXIT_SURVEY_CONFIRM.entryScreens has the same gap — it allows Dynamic_ExitSurvey_Reason and Settings_Troubleshoot but not Settings_Profile, so that branch 404s too once a user reaches it.
What changes do you think we should make in order to solve the problem?
Add SCREENS.SETTINGS.PROFILE.ROOT to the entryScreens allow-list of both EXIT_SURVEY_REASON and EXIT_SURVEY_CONFIRM in src/ROUTES.ts. Both branches of the handler in InitialSettingsPage need to work, so both dynamic routes need the same entry screen registered.
After this change, when the button on the main Settings page builds /settings/profile/exit-survey/reason (or .../exit-survey/confirm), the dynamic resolver will accept Settings_Profile as a valid focused route, and getStateFromPath will return the correct state to render the exit-survey screen.
The Troubleshoot button is unaffected — Settings_Troubleshoot remains in the allow-list, so its behavior does not change.
What alternative solutions did you explore? (Optional)
Pass an explicit basePath such as ROUTES.SETTINGS into createDynamicRoute from the InitialSettingsPage handler so the resolved URL is always /settings/exit-survey/reason regardless of which central pane is active. This works but it requires changing call-site logic, and createDynamicRoute's allow-list mechanism is the intended place to declare which screens may launch a dynamic route — so configuring entryScreens is more in line with existing conventions.
Make EXIT_SURVEY_REASON.entryScreens accept the wildcard ['*'] as KEYBOARD_SHORTCUTS does. Rejected because it would silently widen the allow-list to every screen in the app and lose the safety net the explicit list is intended to provide.
Root cause:Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.EXIT_SURVEY_REASON.path)) in InitialSettingsPage.tsx (lines 340–341) doesn't specify a base path, so createDynamicRoute resolves to the currently focused route. On wide screens the default central pane is Settings_Profile, which is not listed in EXIT_SURVEY_REASON.entryScreens in ROUTES.ts (lines 346–349) — causing the 404.
Fix: Add SCREENS.SETTINGS.PROFILE.ROOT to entryScreens for both EXIT_SURVEY_REASON and EXIT_SURVEY_CONFIRM in ROUTES.ts:
changed the title [-][$250] "Switch to Expensify Classic" button on Settings page leads to 404 due to missing entryScreen[/-][+][Due for payment 2026-05-05] [$250] "Switch to Expensify Classic" button on Settings page leads to 404 due to missing entryScreen[/+]on Apr 28, 2026
The solution for this issue has been 🚀 deployed to production 🚀 in version 9.3.63-0 and is now subject to a 7-day regression period 📆. Here is the list of pull requests that resolve this issue:
@mjasikowski / @youssef-lr The PR fixing this issue has been merged! The following checklist (instructions) will need to be completed before the issue can be closed. Please copy/paste the BugZero 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]
The solution for this issue has been 🚀 deployed to production 🚀 in version 9.3.64-31 and is now subject to a 7-day regression period 📆. Here is the list of pull requests that resolve this issue:
changed the title [-][Due for payment 2026-05-05] [$250] "Switch to Expensify Classic" button on Settings page leads to 404 due to missing entryScreen[/-][+][Due for payment 2026-05-07] [Due for payment 2026-05-05] [$250] "Switch to Expensify Classic" button on Settings page leads to 404 due to missing entryScreen[/+]on Apr 30, 2026
@mjasikowski / @youssef-lr The PR fixing this issue has been merged! The following checklist (instructions) will need to be completed before the issue can be closed. Please copy/paste the BugZero 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]
Activity
melvin-bot commentedon Apr 27, 2026
Triggered auto assignment to Contributor-plus team member for initial proposal review - @parasharrajat (
External)melvin-bot commentedon Apr 27, 2026
Job added to Upwork: https://www.upwork.com/jobs/~022048836662852713050
[-]"Switch to Expensify Classic" button on Settings page leads to 404 due to missing entryScreen[/-][+][$250] "Switch to Expensify Classic" button on Settings page leads to 404 due to missing entryScreen[/+]aswin-s commentedon Apr 27, 2026
Proposal
Please re-state the problem that we are trying to solve in this issue.
When a user clicks "Switch to Expensify Classic" from the main Settings page (
InitialSettingsPage), it causes a 404 instead of opening the exit survey, which prevents the user from completing the expected switch flow from that settings entry point.What is the root cause of that problem?
The issue is caused by how dynamic settings routes are validated in
src/ROUTES.tsand resolved from the current settings context.createDynamicRoute()builds the exit survey URL relative to the currently focused central pane screen, andgetStateFromPathonly allows that route when the focused screen is listed in the route'sentryScreensconfiguration.For
DYNAMIC_ROUTES.EXIT_SURVEY_REASONinsrc/ROUTES.ts,entryScreensincludesSCREENS.SETTINGS.TROUBLESHOOTbut does not includeSCREENS.SETTINGS.PROFILE.ROOT. On wide screens,SettingsSplitNavigatorinsrc/libs/Navigation/AppNavigator/Navigators/SettingsSplitNavigator.tsxusesSCREENS.SETTINGS.PROFILE.ROOTas thedefaultCentralScreen, so opening the survey fromInitialSettingsPageresolves fromSettings_Profile. Because that screen is not allowed inentryScreens,getStateFromPathrejects the route and the app falls through to an unmatched path, which results in the 404.The same configuration gap exists for
DYNAMIC_ROUTES.EXIT_SURVEY_CONFIRMinsrc/ROUTES.ts, whoseentryScreensalso omitsSCREENS.SETTINGS.PROFILE.ROOT. As a result, the direct confirm-step path fails in the same way whenInitialSettingsPagenavigates there without first opening the reason step.What changes do you think we should make in order to solve the problem?
src/ROUTES.ts— addSCREENS.SETTINGS.PROFILE.ROOTtoEXIT_SURVEY_REASON.entryScreens.src/ROUTES.ts— addSCREENS.SETTINGS.PROFILE.ROOTtoEXIT_SURVEY_CONFIRM.entryScreens.What alternative solutions did you explore? (Optional)
Passing an explicit
basePathofsettings/troubleshoottocreateDynamicRoutefromInitialSettingsPagewould force the route to resolve through the Troubleshoot screen. This was rejected because it hard-codes the main Settings page to a sibling screen's path and makes the flow depend on that URL structure instead of the route's own allowed entry screens.MelvinBot commentedon Apr 27, 2026
Proposal
What is the root cause of that problem?
EXIT_SURVEY_REASON.entryScreensonly permitsSettings_Troubleshoot. On wide screens, the sidebar (InitialSettingsPage) and a central pane are rendered together. The default central pane isSettings_Profile(SettingsSplitNavigator.tsx:40).When the user clicks "Switch to Expensify Classic" from the sidebar (
InitialSettingsPage.tsx:340),createDynamicRoutebuilds the path relative to the current central pane (e.g./settings/profile/exit-survey/reason).getStateFromPaththen extractsSettings_Profileas the focused route, finds it's not inentryScreens, and the route falls through to a 404.What changes do you think we should make in order to solve the problem?
Add all settings central pane screens to
EXIT_SURVEY_REASON.entryScreensinsrc/ROUTES.ts:346-349. The sidebar button can be clicked while any central pane is focused (not just Profile), so we need to allow all of them. The same fix should also be applied toEXIT_SURVEY_CONFIRMat line 350-353, sinceInitialSettingsPagecan navigate directly to it as well (line 344).Alternatively, if every settings screen should be able to reach the exit survey, using the wildcard
'*'inentryScreenswould be the simplest and most future-proof approach.What alternative solutions did you explore? (Optional)
Adding only
SCREENS.SETTINGS.PROFILE.ROOT(as suggested in the issue body) fixes the default case but leaves the bug reproducible when the user has navigated to any other central pane (Preferences, Wallet, Security, etc.) before clicking the sidebar button.Next Steps for Contributor+ team: Reply with
@MelvinBot implement thisto create a draft PR,@MelvinBot <your feedback>to refine this analysis, or explain why you are rejecting Melvin's proposal.wildan-m commentedon Apr 27, 2026
Proposal
Please re-state the problem that we are trying to solve in this issue.
On wide layouts, clicking the Switch to Expensify Classic button on the Settings page (the entry shown in the general menu of
InitialSettingsPage) navigates to a 404 page instead of opening the exit-survey reason step.The same button on the Troubleshoot page works correctly, so the bug is specific to the entry point on the main Settings screen.
What is the root cause of that problem?
The handler that runs when the button is clicked builds the destination URL with
createDynamicRoute:App/src/pages/settings/InitialSettingsPage.tsx
Lines 339 to 345 in 626a3df
createDynamicRouteis called without an explicitbasePath, so it defaults toNavigation.getActiveRoute()and concatenates the dynamic suffix to whatever URL is currently active:App/src/libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute.ts
Lines 64 to 73 in 626a3df
On wide layouts, the central pane of
SettingsSplitNavigatordefaults toSettings_Profile, so the active URL when the user is on Settings is/settings/profile:App/src/libs/Navigation/AppNavigator/Navigators/SettingsSplitNavigator.tsx
Line 40 in 626a3df
The combined target therefore becomes
/settings/profile/exit-survey/reason. WhengetStateFromPathresolves that URL, the focused route isSettings_Profile, but theEXIT_SURVEY_REASONdynamic route only allowsSettings_Troubleshootas an entry screen:App/src/ROUTES.ts
Lines 346 to 353 in 626a3df
Because
Settings_Profileis not inentryScreens, the dynamic resolver logs a warning (Focused route Settings_Profile is not allowed to access dynamic route with suffix exit-survey/reason) and falls back toRNGetStateFromPath, which has no static match forsettings/profile/exit-survey/reasonand renders the 404 NotFoundPage.The Troubleshoot version works because the user is already focused on
Settings_Troubleshootat the moment of the click, which is in the allow-list.The same handler in
InitialSettingsPagealso has a second branch that navigates straight toEXIT_SURVEY_CONFIRMwhen the classic-redirect prompt was previously dismissed (shouldOpenSurveyReasonPage === false), andEXIT_SURVEY_CONFIRM.entryScreenshas the same gap — it allowsDynamic_ExitSurvey_ReasonandSettings_Troubleshootbut notSettings_Profile, so that branch 404s too once a user reaches it.What changes do you think we should make in order to solve the problem?
Add
SCREENS.SETTINGS.PROFILE.ROOTto theentryScreensallow-list of bothEXIT_SURVEY_REASONandEXIT_SURVEY_CONFIRMinsrc/ROUTES.ts. Both branches of the handler inInitialSettingsPageneed to work, so both dynamic routes need the same entry screen registered.After this change, when the button on the main Settings page builds
/settings/profile/exit-survey/reason(or.../exit-survey/confirm), the dynamic resolver will acceptSettings_Profileas a valid focused route, andgetStateFromPathwill return the correct state to render the exit-survey screen.The Troubleshoot button is unaffected —
Settings_Troubleshootremains in the allow-list, so its behavior does not change.Compare branch:
https://github.com/Expensify/App/compare/main...wildan-m:App:wildan/88986-exit-survey-profile-entry-screen?expand=1
What alternative solutions did you explore? (Optional)
basePathsuch asROUTES.SETTINGSintocreateDynamicRoutefrom theInitialSettingsPagehandler so the resolved URL is always/settings/exit-survey/reasonregardless of which central pane is active. This works but it requires changing call-site logic, andcreateDynamicRoute's allow-list mechanism is the intended place to declare which screens may launch a dynamic route — so configuringentryScreensis more in line with existing conventions.EXIT_SURVEY_REASON.entryScreensaccept the wildcard['*']asKEYBOARD_SHORTCUTSdoes. Rejected because it would silently widen the allow-list to every screen in the app and lose the safety net the explicit list is intended to provide.mjasikowski commentedon Apr 27, 2026
@MelvinBot implement your proposal in a draft PR
Guidondor commentedon Apr 27, 2026
Proposal
Root cause:
Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.EXIT_SURVEY_REASON.path))inInitialSettingsPage.tsx(lines 340–341) doesn't specify a base path, socreateDynamicRouteresolves to the currently focused route. On wide screens the default central pane isSettings_Profile, which is not listed inEXIT_SURVEY_REASON.entryScreensinROUTES.ts(lines 346–349) — causing the 404.Fix: Add
SCREENS.SETTINGS.PROFILE.ROOTtoentryScreensfor bothEXIT_SURVEY_REASONandEXIT_SURVEY_CONFIRMinROUTES.ts:MelvinBot commentedon Apr 27, 2026
🤖 Draft PR created: #88990
Please mark it as "Ready for review" when it is ready for review.
4 remaining items
youssef-lr commentedon Apr 28, 2026
Tested well for me, when I manually change the url to anything +

exit-survey/reason[-][$250] "Switch to Expensify Classic" button on Settings page leads to 404 due to missing entryScreen[/-][+][Due for payment 2026-05-05] [$250] "Switch to Expensify Classic" button on Settings page leads to 404 due to missing entryScreen[/+]melvin-bot commentedon Apr 28, 2026
Reviewinglabel has been removed, please complete the "BugZero Checklist".melvin-bot commentedon Apr 28, 2026
The solution for this issue has been 🚀 deployed to production 🚀 in version 9.3.63-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-05-05. 🎊
melvin-bot commentedon Apr 28, 2026
@mjasikowski / @youssef-lr The PR fixing this issue has been merged! The following checklist (instructions) will need to be completed before the issue can be closed. Please copy/paste the BugZero 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]
melvin-bot commentedon Apr 30, 2026
The solution for this issue has been 🚀 deployed to production 🚀 in version 9.3.64-31 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-05-07. 🎊
[-][Due for payment 2026-05-05] [$250] "Switch to Expensify Classic" button on Settings page leads to 404 due to missing entryScreen[/-][+][Due for payment 2026-05-07] [Due for payment 2026-05-05] [$250] "Switch to Expensify Classic" button on Settings page leads to 404 due to missing entryScreen[/+]melvin-bot commentedon Apr 30, 2026
@mjasikowski / @youssef-lr The PR fixing this issue has been merged! The following checklist (instructions) will need to be completed before the issue can be closed. Please copy/paste the BugZero 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]