Skip to content

Fix "Not here" page after saving split dates (#88434)#88472

Closed
neil-marcellini wants to merge 3 commits into
mainfrom
neil/fix-88434-split-expense-route-shape
Closed

Fix "Not here" page after saving split dates (#88434)#88472
neil-marcellini wants to merge 3 commits into
mainfrom
neil/fix-88434-split-expense-route-shape

Conversation

@neil-marcellini

@neil-marcellini neil-marcellini commented Apr 21, 2026

Copy link
Copy Markdown
Contributor

Explanation of Change

(Neil's AI agent)
The SPLIT_EXPENSE and SPLIT_EXPENSE_SEARCH routes declared an optional :backTo? path parameter at the end of the path:

create/split-expense/overview/:reportID/:transactionID/:splitExpenseTransactionID/:backTo?
create/split-expense/overview/:reportID/:transactionID/:splitExpenseTransactionID/search/:backTo?

That trailing slot collided with the nested split tab screens (amount, percentage, date) registered under those routes in src/libs/Navigation/linkingConfig/config.ts. Because of the collision, a URL like:

create/split-expense/overview/<reportID>/<transactionID>/0/date

could be parsed with backTo = "date" instead of nesting the date tab under the parent screen.

The regression surfaced in the split-by-date flow:

  1. User opens the Date tab on SplitExpensePage. Current URL ends in /date.
  2. handleDatePress navigates to SPLIT_EXPENSE_CREATE_DATE_RANGE and uses Navigation.getActiveRoute() as backTo, so backTo is the .../0/date URL.
  3. After saving, Navigation.goBack(backTo) runs getStateFromPath(backTo) to unwind.
  4. The route-shape ambiguity caused that state to be resolved without the required split-expense params, so SplitExpensePage rendered FullPageNotFoundView ("Not here").

(Neil's AI agent)
The fix has two complementary layers:

Layer 1 — Remove :backTo? from the route patterns. getUrlWithBackToParam already appends backTo as a query string (?backTo=...); it never fills the :backTo? path slot itself. Dropping the slot from the pattern removes the ambiguity while keeping route.params.backTo populated (React Navigation parses query strings into params automatically), so every consumer that reads route.params.backTo (SplitExpensePage, SplitExpenseCreateDateRagePage, SplitExpenseEditPage) keeps working unchanged.

The pattern was originally introduced in #78027 (fixing #78080, "Back button on split RHP show the page twice") as a workaround for stack duplication when navigating Split > Date > Split dates. That PR even added an inline TODO: "TODO: Remove backTo from route once we have find another way to fix navigation issues with tabs." This change completes that TODO.

Layer 2 — Strip trailing tab segments from the backTo URL fed into the date-range screen. SplitExpensePage.handleDatePress previously forwarded Navigation.getActiveRoute() straight into backTo. On the Date tab that URL ends in /date, which is exactly the ambiguous segment that caused "Not here." Even with the route pattern fixed, re-entering that URL into getStateFromPath is unnecessarily brittle (and still collides with the nested date tab route). The new pure helper src/libs/stripSplitTabSuffix.ts removes a trailing /amount, /percentage, or /date segment from the URL before it's passed as backTo. The selected tab is still restored across the back navigation because OnyxTabNavigator persists the active tab in Onyx.

(Neil's AI agent)
Notes on scope:

  • I deliberately did not modify RELATIONS or getSearchScreenNameForRoute for the split-expense RHP screens. getMatchingFullScreenRoute already resolves the underlying fullscreen state from backTo first (now unambiguous) and only falls back to RELATIONS / defaults when backTo is absent. Adding these screens to SEARCH_TO_RHP would incorrectly default non-search deep-links to the Search tab; the default fallback to Inbox is the documented behavior and is acceptable for deep-link entry (per contributingGuides/NAVIGATION.md).
  • I deliberately did not touch the other :backTo? patterns on unrelated search routes — they have a different shape and no tab collision.
  • Full behavioral flow + related cleanups are documented in a comment on the issue: [Due for payment 2026-05-07] Split: “Not here” page displayed when selecting split dates #88434

(Neil's AI agent) Added tests/navigation/splitExpenseRouteShapeTests.ts which locks in both layers of the fix:

  • Route-shape assertions on ROUTES.SPLIT_EXPENSE / ROUTES.SPLIT_EXPENSE_SEARCH and their getRoute helpers.
  • Behavioral coverage of stripSplitTabSuffix for every CONST.TAB.SPLIT value, including query-string preservation and mid-path false positives.

TDD round trip was verified end-to-end: with both layers of the fix reverted, the 4 route-shape assertions fail; reapplying both brings all 15 tests green.

Fixed Issues

$ #88434
PROPOSAL: N/A (deploy blocker)

Tests

(Neil's AI agent)

  1. Log in as a Control Workspace member.
  2. Open a Workspace chat and create an expense.
  3. Open the expense, click More, select Split.
  4. On the split expense page, click the Date field header (the Date tab).
  5. Tap Split dates.
  6. Pick two different dates and click Save.
  7. Verify the app returns to the Split expense overview (Date tab) with the updated splits and does not show the "Not here" page.
  8. Repeat steps 5-7 with the Amount and Percentage tabs active when entering Split dates to make sure tab-suffixed URLs still round-trip.
  • Verify that no errors appear in the JS console

Offline tests

Same as Tests — offline/online transitions are not part of this code path. Verify the navigation still works with no network after the initial page load.

QA Steps

(Neil's AI agent) Same as Tests.

// TODO: These must be filled out, or the issue title must include "[No QA]."

  • Verify that no errors appear in the JS console

PR Author Checklist

  • I linked the correct issue in the ### Fixed Issues section above
  • I wrote clear testing steps that cover the changes made in this PR
    • I added steps for local testing in the Tests section
    • I added steps for the expected offline behavior in the Offline steps section
    • I added steps for Staging and/or Production testing in the QA steps section
    • I added steps to cover failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
    • I tested this PR with a High Traffic account against the staging or production API to ensure there are no regressions (e.g. long loading states that impact usability).
  • I included screenshots or videos for tests on all platforms
  • I ran the tests on all platforms & verified they passed on:
    • Android: Native
    • Android: mWeb Chrome
    • iOS: Native
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • I verified there are no console errors (if there's a console error not related to the PR, report it or open an issue for it to be fixed)
  • I followed proper code patterns (see Reviewing the code)
    • I verified that any callback methods that were added or modified are named for what the method does and never what callback they handle (i.e. toggleReport and not onIconClick)
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text shown in the product is localized by adding it to src/languages/* files and using the translation method
      • If any non-english text was added/modified, I used JaimeGPT to get English > Spanish translation. I then posted it in #expensify-open-source and it was approved by an internal Expensify engineer. Link to Slack message: N/A — no copy changes.
    • I verified all numbers, amounts, dates and phone numbers shown in the product are using the localization methods
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
    • I verified proper file naming conventions were followed for any new files or renamed files. All non-platform specific files are named after what they export and are not named "index.js". All platform-specific files are named for the platform the code supports as outlined in the README.
    • I verified the JSDocs style guidelines (in STYLE.md) were followed
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I followed the guidelines as stated in the Review Guidelines
  • I tested other components that can be impacted by my changes (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar are working as expected)
  • I verified all code is DRY (the PR doesn't include any logic written more than once, with the exception of tests)
  • I verified any variables that can be defined as constants (ie. in CONST.ts or at the top of the file that uses the constant) are defined as such
  • I verified that if a function's arguments changed that all usages have also been updated correctly
  • If any new file was added I verified that:
    • The file has a description of what it does and/or why is needed at the top of the file if the code is not self explanatory
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))
  • If new assets were added or existing ones were modified, I verified that:
    • The assets are optimized and compressed (for SVG files, run npm run compress-svg)
    • The assets load correctly across all supported platforms.
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • If a new page is added, I verified it's using the ScrollView component to make it scrollable when more elements are added to the page.
  • I added unit tests for any new feature or bug fix in this PR to help automatically prevent regressions in this user flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.

Screenshots/Videos

Android: Native
Android: mWeb Chrome
iOS: Native
iOS: mWeb Safari
MacOS: Chrome / Safari

Made with Cursor

Remove the trailing `:backTo?` optional path parameter from the
SPLIT_EXPENSE and SPLIT_EXPENSE_SEARCH route patterns. The optional
path slot collided with the split tab names (amount, percentage,
date) nested under those routes, so URLs like
`create/split-expense/overview/<r>/<t>/<s>/date` were parsed with
`backTo=date` instead of nesting the date tab. When
`Navigation.goBack(backTo)` later resolved that URL, the resulting
state was missing required params and SplitExpensePage rendered the
"Not here" (FullPageNotFoundView) screen after saving split dates.

`backTo` continues to travel as a query-string parameter via
`getUrlWithBackToParam`, and React Navigation still populates
`route.params.backTo` from the query string, so all consumer code
keeps working unchanged.

Adds tests/navigation/splitExpenseRouteShapeTests.ts to lock the
fixed route shape in place.

Made-with: Cursor
@neil-marcellini

Copy link
Copy Markdown
Contributor Author

(Neil's AI agent)

Some extra reviewer context on why this fix is narrow and how backTo / RELATIONS / Dynamic Routes relate, in case you're wondering why I didn't also touch RELATIONS or reach for a Dynamic Route migration here.

What RELATIONS is, and how it relates to backTo

src/libs/Navigation/linkingConfig/RELATIONS/ is a bundle of small static maps that answer one question: when the app boots directly onto an RHP screen (via deep link, refresh, etc.), which fullscreen screen should sit underneath it?

The consumer lives in getMatchingFullScreenRoute (src/libs/Navigation/helpers/getAdaptedStateFromPath.ts):

function getMatchingFullScreenRoute(route: NavigationPartialRoute) {
    const isDynamicScreen = isDynamicRouteScreen(route.name as Screen);

    // Check for backTo param. One screen with different backTo value may need different screens visible under the overlay.
    if (isRouteWithBackToParam(route) && !isDynamicScreen) {
        const stateForBackTo = getStateFromPath(route.params.backTo as RoutePath);
        ...
    }

    const routeNameForLookup = getSearchScreenNameForRoute(route);
    if (RHP_TO_SEARCH[routeNameForLookup]) { ... }
    if (RHP_TO_SIDEBAR[route.name])        { ... }
    if (RHP_TO_HOME[route.name])           { ... }
    if (RHP_TO_WORKSPACES_LIST[route.name]) { ... }
    if (RHP_TO_WORKSPACE[route.name])      { ... }
    if (RHP_TO_SETTINGS[route.name])       { ... }
    if (RHP_TO_DOMAIN[route.name])         { ... }

    // falls through to Inbox default
}

Priority order:

  1. If the URL carries ?backTo=…, use it to compute the underlay (by recursively resolving that URL to a state and taking its focused route).
  2. Otherwise fall through the static RHP_TO_* tables.
  3. Otherwise, Inbox is the default. NAVIGATION.md:

    If the RHP screen is not added to any relation, the Inbox tab will be opened underneath by default.

So RELATIONS and backTo solve the same problem — what goes under the RHP — but in opposite directions:

RELATIONS backTo
Kind Static, declarative Dynamic, per-navigation URL param
Lives in linkingConfig/RELATIONS/*.ts The URL itself (?backTo=...)
When it wins Only when backTo is absent Whenever present (unless the target is a dynamic-route screen)
Good at Cold loads, deep links, refresh with no prior stack Preserving "came from X" context when the same RHP is opened from multiple places
Weakness One underlay per screen; can't vary by entry point URL pollution, duplicated routes across the stack, round-trip parsing bugs (this issue)

For Melvin's secondary suggestion — adding SPLIT_EXPENSE_CREATE_DATE_RANGE to the right RELATIONS map — the value is purely cold-load / refresh behavior: if you paste the split-dates URL directly into the address bar today, nothing in the maps points at Search or a split navigator, so you'd land on Inbox as the default underlay. Worth doing, but it's a separate bug from the "Not here" one, and it doesn't affect the goBack(backTo) flow that this PR is fixing.

There are also a few non-RHP maps in the same folder: SIDEBAR_TO_SPLIT / SPLIT_TO_SIDEBAR (which sidebar owns which Split navigator), TAB_TO_FULLSCREEN / FULLSCREEN_TO_TAB (which bottom tab each fullscreen navigator belongs to). Same pattern — a small declarative table replacing ad-hoc runtime logic.

Where the "remove backTo" initiative sits

The initiative is alive, well-designed, and partially complete — not finished.

Ratchet in place:

  • Design doc: [Design Doc] Replacing backTo / forwardTo with Dynamic URL Navigation #73825 "Replacing backTo / forwardTo with Dynamic URL Navigation" — executed against.
  • Dynamic Routes as the replacement mechanism: 54 entries in DYNAMIC_ROUTES in src/ROUTES.ts today.
  • ESLint blocks new getUrlWithBackToParam calls:
    selector: 'CallExpression[callee.name="getUrlWithBackToParam"]',
    message: 'Usage of getUrlWithBackToParam function is prohibited. This is legacy code and no new occurrences should be added...'
    
  • ESLint blocks new backTo properties on screen param lists:
    selector: 'TSPropertySignature[key.name="backTo"]',
    message: 'The `backTo` route param is deprecated. Do not add new `backTo` properties to screen param lists...'
    
  • contributingGuides/NAVIGATION.md — every backTo section now carries a [!WARNING] Deprecated banner pointing first at Dynamic Routes, with a full "Migrating from backTo to dynamic routes" guide.
  • Tracking issues: [Tracking] Replacing backTo / forwardTo with Dynamic URL Navigation - Release 1 #80515 "Release 1" (OPEN), [Tracking] Replacing backTo / forwardTo with Dynamic URL Navigation - Release 2 #83358 "Release 2" (OPEN). Bodies are placeholders; the actual unit of work is tracked as BT-XXX-labeled sub-issues / PRs.

What hasn't happened yet:

  • ~1,962 backTo references remain across src/ (including 462 inside types.ts, 374 in ROUTES.ts — i.e. ~1,100 real call sites across pages).
  • 221 screen param lists still declare backTo?: tagged with the "legacy" eslint-disable marker.
  • 227 other eslint-disable-next-line comments grandfathering legacy getUrlWithBackToParam calls.
  • 226 getRoute(…, backTo?) signatures still live in src/ROUTES.ts.

Migration PRs are landing continuously under the BT-… label — just the last couple of weeks have seen QBO Export, NetSuite Parts 1–2, QBO AutoSync, Exit Survey, Categories / Tags, Keyboard Shortcuts, etc.

Why this PR is the narrow fix

Doing the "full" fix for this flow — migrating SPLIT_EXPENSE_CREATE_DATE_RANGE off backTo to a Dynamic Route, adding the appropriate RELATIONS entry, and reworking handleDatePress to stop relying on Navigation.getActiveRoute() — would bring new surface area (and new BT--style migration risk) into a deploy-blocker patch.

The route-shape fix is aligned with the end state (Dynamic Routes don't carry :backTo? in the path) and with the current state (?backTo= query strings continue to work for every grandfathered callsite), so it's a net step toward the initiative without jumping ahead of it. Longer-term cleanup (Dynamic Route migration for this flow, filling in the RELATIONS entry, and replacing the getActiveRoute()backTo call in handleDatePress) is better done as a follow-up BT- PR rather than piled onto the deploy fix.

Addresses the "Remove backTo from route" TODO that accompanied the
:backTo? path-param removal from SPLIT_EXPENSE / SPLIT_EXPENSE_SEARCH.

The second half of the #88434 regression: SplitExpensePage.handleDatePress
forwarded Navigation.getActiveRoute() straight into backTo. On the Date
tab that URL ended in /date, which collides with the nested tab segment
once goBack resolves it. We now sanitize that URL through a pure helper,
stripSplitTabSuffix, before handing it to the date-range screen as
backTo. The selected tab is still restored across the back navigation
because OnyxTabNavigator persists it in Onyx.

Also locks in both layers of the fix with unit tests.

Made-with: Cursor
Renames SplitExpenseCreateDateRagePage to SplitExpenseCreateDateRangePage
and updates the matching type, component, test ID, and the require() path
in ModalStackNavigators.

Made-with: Cursor
@neil-marcellini

Copy link
Copy Markdown
Contributor Author

I'm closing this because we ended up finding the problematic PR and reverting, but this might be a good template for someone to look at for a long-term fix.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant