Skip to content
4 changes: 2 additions & 2 deletions contributingGuides/NAVIGATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -503,7 +503,7 @@ In Expensify, we use an extended implementation of this function because:

Here are examples how the state is generated based on route:

- `settings/workspaces/1/overview`
- `workspaces/1/overview`

```json
{
Expand Down Expand Up @@ -536,7 +536,7 @@ Here are examples how the state is generated based on route:
"params": {
"policyID": "1"
},
"path": "/settings/workspaces/1/overview",
"path": "workspaces/1/overview",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should also add this new path to apple-app-site-association
file, so our smart app banner can display on mSafari.

More details:
#66743 (comment)

"key": "Workspace_Overview-key"
}
]
Expand Down
8 changes: 4 additions & 4 deletions contributingGuides/NAVIGATION_TESTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,11 @@

1. Open any workspace settings (Settings → Workspaces → Select any workspace)
2. Click the Settings button on the bottom tab.
3. Verify that the Workspace list is displayed (`/settings/workspaces`)
3. Verify that the Workspace list is displayed (`/workspaces`)
4. Select any workspace again.
5. Reload the page.
6. Click the Settings button on the bottom tab.
7. Verify that the Workspace list is displayed (`/settings/workspaces`)
7. Verify that the Workspace list is displayed (`/workspaces/`)


#### The last visited screen in the settings tab is saved when switching between tabs
Expand All @@ -52,7 +52,7 @@

#### Going up to the workspace list page after refreshing on the workspace settings and pressing the up button

1. Open the workspace settings from the deep link (use a link in format: `/settings/workspaces/:policyID:/profile`)
1. Open the workspace settings from the deep link (use a link in format: `/workspaces/:policyID:/profile`)
2. Click the app’s back button.
3. Verify if the workspace list is displayed.

Expand Down Expand Up @@ -241,4 +241,4 @@ Linked issue: https://github.com/Expensify/App/issues/50177
11. Go back.
12. Verify you are navigated back to the employee size step.
13. Go back.
14. Verify you are navigated back to the Purpose step.
14. Verify you are navigated back to the Purpose step.
6 changes: 3 additions & 3 deletions contributingGuides/STYLE.md
Original file line number Diff line number Diff line change
Expand Up @@ -535,13 +535,13 @@ We need to change the `getRoute()` `policyID` argument type to allow `undefined`

```diff
WORKSPACE_PROFILE_ADDRESS: {
route: 'settings/workspaces/:policyID/profile/address',
- getRoute: (policyID: string, backTo?: string) => getUrlWithBackToParam(`settings/workspaces/${policyID}/profile/address` as const, backTo),
route: 'workspaces/:policyID/profile/address',
- getRoute: (policyID: string, backTo?: string) => getUrlWithBackToParam(`workspaces/${policyID}/profile/address` as const, backTo),
+ getRoute: (policyID: string | undefined, backTo?: string) => {
+ if (!policyID) {
+ Log.warn("Invalid policyID is used to build the WORKSPACE_PROFILE_ADDRESS route")
+ }
+ return getUrlWithBackToParam(`settings/workspaces/${policyID}/profile/address` as const, backTo);
+ return getUrlWithBackToParam(`workspaces/${policyID}/profile/address` as const, backTo);
+ },
},
```
Expand Down
33 changes: 0 additions & 33 deletions help/ref/settings/workspaces/:policyId/index.md

This file was deleted.

File renamed without changes.
1,040 changes: 515 additions & 525 deletions src/ROUTES.ts

Large diffs are not rendered by default.

1,091 changes: 649 additions & 442 deletions src/components/SidePanel/HelpContent/helpContentMap.tsx

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sumo-slonik These changes feel to a big part unrelated, why is it here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file is automatically generated. I regenerated it to include the changes I made, using the following command:
cd help && bundle exec jekyll build && cd .. && cp help/_src/helpContentMap.tsx src/components/SidePanel/HelpContent/helpContentMap.tsx
It's possible that some changes appeared on main that weren’t rebuilt by others.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeahhh, we need this help content, for example, the members page content is not present on staging/ prod, but with our PR, we add the content and now we can see the same when we visit the members page, Now i also feel like @blazejkustra should review this PR once, since they were the implementor of the help content PR.

It's possible that some changes appeared on main that weren’t rebuilt by others.

Makes sense!

Staging:

Screenshot 2025-07-01 at 2 50 34 PM

Our PR:

Screenshot 2025-07-01 at 2 51 29 PM

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's possible that some changes appeared on main that weren’t rebuilt by others.

That's exactly what happened, Ted added some content here but didn't regenerate helpContentMap. I’ve opened a PR with the regenerated content, currently pending final review from an internal engineer.

Now i also feel like @blazejkustra should review this PR once, since they were the implementor of the help content PR.

I was overseeing this PR in terms of the help panel, and everything was implemented according to the current standards @allgandalf @mountiny

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that I think about it I believe it would be safer to wait for this to be merged first, then regenerate content one more time here and then merge this PR :shipit:

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions src/libs/Navigation/helpers/getAdaptedStateFromPath.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@
import ONYXKEYS from '@src/ONYXKEYS';
import SCREENS from '@src/SCREENS';
import type {Report} from '@src/types/onyx';
import getMatchingNewRoute from './getMatchingNewRoute';
import getParamsFromRoute from './getParamsFromRoute';
import {isFullScreenName} from './isNavigatorName';
import replacePathInNestedState from './replacePathInNestedState';

let allReports: OnyxCollection<Report>;
Onyx.connect({

Check warning on line 21 in src/libs/Navigation/helpers/getAdaptedStateFromPath.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.REPORT,
waitForCollectionCallback: true,
callback: (value) => {
Expand Down Expand Up @@ -226,6 +227,8 @@
const getAdaptedStateFromPath: GetAdaptedStateFromPath = (path, options, shouldReplacePathInNestedState = true) => {
let normalizedPath = !path.startsWith('/') ? `/${path}` : path;

normalizedPath = getMatchingNewRoute(normalizedPath) ?? normalizedPath;

// Bing search results still link to /signin when searching for “Expensify”, but the /signin route no longer exists in our repo, so we redirect it to the home page to avoid showing a Not Found page.
if (normalizedPath === CONST.SIGNIN_ROUTE) {
normalizedPath = '/';
Expand Down
33 changes: 33 additions & 0 deletions src/libs/Navigation/helpers/getMatchingNewRoute.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import oldRoutes from '@navigation/linkingConfig/OldRoutes';

/**
* Maps an old route path to its corresponding new route based on the `oldRoutes` map.
* It finds the best matching pattern (with wildcard `*` support) and replaces the matched
* part of the path with the new route value.
*
* @param path - The input URL path to match and transform.
* @returns The new route path if a match is found, otherwise `undefined`.
*
* Related issue: https://github.com/Expensify/App/issues/64968
*/
function getMatchingNewRoute(path: string) {
let bestMatch;
let maxLength = -1;

for (const pattern of Object.keys(oldRoutes)) {
const regexStr = `^${pattern.replace('*', '.*')}`;
const regex = new RegExp(regexStr);

if (regex.test(path) && pattern.length > maxLength) {
bestMatch = pattern;
maxLength = pattern.length;
}
}
if (!bestMatch) {
return bestMatch;
}

const finalRegexp = bestMatch?.replace('*', '') ?? '';
return path.replace(finalRegexp, oldRoutes[bestMatch]);
}
export default getMatchingNewRoute;
4 changes: 3 additions & 1 deletion src/libs/Navigation/helpers/getStateFromPath.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,18 @@ import type {NavigationState, PartialState} from '@react-navigation/native';
import {getStateFromPath as RNGetStateFromPath} from '@react-navigation/native';
import {linkingConfig} from '@libs/Navigation/linkingConfig';
import type {Route} from '@src/ROUTES';
import getMatchingNewRoute from './getMatchingNewRoute';

/**
* @param path - The path to parse
* @returns - It's possible that there is no navigation action for the given path
*/
function getStateFromPath(path: Route): PartialState<NavigationState> {
const normalizedPath = !path.startsWith('/') ? `/${path}` : path;
const normalizedPathAfterRedirection = getMatchingNewRoute(normalizedPath) ?? normalizedPath;

// This function is used in the linkTo function where we want to use default getStateFromPath function.
const state = RNGetStateFromPath(normalizedPath, linkingConfig.config);
const state = RNGetStateFromPath(normalizedPathAfterRedirection, linkingConfig.config);

if (!state) {
throw new Error('Failed to parse the path to a navigation state.');
Expand Down
4 changes: 3 additions & 1 deletion src/libs/Navigation/helpers/linkTo/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import getStateFromPath from '@libs/Navigation/helpers/getStateFromPath';
import normalizePath from '@libs/Navigation/helpers/normalizePath';
import {linkingConfig} from '@libs/Navigation/linkingConfig';
import {shallowCompare} from '@libs/ObjectUtils';
import getMatchingNewRoute from '@navigation/helpers/getMatchingNewRoute';
import type {NavigationPartialRoute, ReportsSplitNavigatorParamList, RootNavigatorParamList, StackNavigationAction} from '@navigation/types';
import CONST from '@src/CONST';
import NAVIGATORS from '@src/NAVIGATORS';
Expand Down Expand Up @@ -56,11 +57,12 @@ export default function linkTo(navigation: NavigationContainerRef<RootNavigatorP
const {forceReplace} = {...defaultLinkToOptions, ...options} as Required<LinkToOptions>;

const normalizedPath = normalizePath(path) as Route;
const normalizedPathAfterRedirection = (getMatchingNewRoute(normalizedPath) ?? normalizedPath) as Route;

// This is the state generated with the default getStateFromPath function.
// It won't include the whole state that will be generated for this path but the focused route will be correct.
// It is necessary because getActionFromState will generate RESET action for whole state generated with our custom getStateFromPath function.
const stateFromPath = getStateFromPath(normalizedPath) as PartialState<NavigationState<RootNavigatorParamList>>;
const stateFromPath = getStateFromPath(normalizedPathAfterRedirection) as PartialState<NavigationState<RootNavigatorParamList>>;
const currentState = navigation.getRootState() as NavigationState<RootNavigatorParamList>;

const focusedRouteFromPath = findFocusedRoute(stateFromPath);
Expand Down
8 changes: 8 additions & 0 deletions src/libs/Navigation/linkingConfig/OldRoutes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const oldRoutes: Record<string, string> = {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like we could add this to cloudfront to redirect, but also do we need to add /workspaces to the AndroidManifest or apple-site json thing for deeplinks?

<!-- Production URLs -->
<data android:scheme="https" android:host="new.expensify.com" android:pathPrefix="/r"/>
<data android:scheme="https" android:host="new.expensify.com" android:pathPrefix="/a"/>
<data android:scheme="https" android:host="new.expensify.com" android:pathPrefix="/settings"/>
<data android:scheme="https" android:host="new.expensify.com" android:pathPrefix="/details"/>
<data android:scheme="https" android:host="new.expensify.com" android:pathPrefix="/v"/>
<data android:scheme="https" android:host="new.expensify.com" android:pathPrefix="/bank-account"/>
<data android:scheme="https" android:host="new.expensify.com" android:pathPrefix="/iou"/>
<data android:scheme="https" android:host="new.expensify.com" android:pathPrefix="/request"/>
<data android:scheme="https" android:host="new.expensify.com" android:pathPrefix="/submit"/>
<data android:scheme="https" android:host="new.expensify.com" android:pathPrefix="/enable-payments"/>
<data android:scheme="https" android:host="new.expensify.com" android:pathPrefix="/statements"/>
<data android:scheme="https" android:host="new.expensify.com" android:pathPrefix="/concierge"/>
<data android:scheme="https" android:host="new.expensify.com" android:pathPrefix="/split"/>
<data android:scheme="https" android:host="new.expensify.com" android:pathPrefix="/request"/>
<data android:scheme="https" android:host="new.expensify.com" android:pathPrefix="/new"/>
<data android:scheme="https" android:host="new.expensify.com" android:pathPrefix="/search"/>
<data android:scheme="https" android:host="new.expensify.com" android:pathPrefix="/send"/>
<data android:scheme="https" android:host="new.expensify.com" android:pathPrefix="/pay"/>
<data android:scheme="https" android:host="new.expensify.com" android:pathPrefix="/money2020"/>
<data android:scheme="https" android:host="new.expensify.com" android:pathPrefix="/track-expense"/>
<data android:scheme="https" android:host="new.expensify.com" android:pathPrefix="/submit-expense"/>
<!-- Staging URLs -->
<data android:scheme="https" android:host="staging.new.expensify.com" android:pathPrefix="/r"/>
<data android:scheme="https" android:host="staging.new.expensify.com" android:pathPrefix="/a"/>
<data android:scheme="https" android:host="staging.new.expensify.com" android:pathPrefix="/settings"/>
<data android:scheme="https" android:host="staging.new.expensify.com" android:pathPrefix="/details"/>
<data android:scheme="https" android:host="staging.new.expensify.com" android:pathPrefix="/v"/>
<data android:scheme="https" android:host="staging.new.expensify.com" android:pathPrefix="/bank-account"/>
<data android:scheme="https" android:host="staging.new.expensify.com" android:pathPrefix="/iou"/>
<data android:scheme="https" android:host="staging.new.expensify.com" android:pathPrefix="/request"/>
<data android:scheme="https" android:host="staging.new.expensify.com" android:pathPrefix="/submit"/>
<data android:scheme="https" android:host="staging.new.expensify.com" android:pathPrefix="/enable-payments"/>
<data android:scheme="https" android:host="staging.new.expensify.com" android:pathPrefix="/statements"/>
<data android:scheme="https" android:host="staging.new.expensify.com" android:pathPrefix="/concierge"/>
<data android:scheme="https" android:host="staging.new.expensify.com" android:pathPrefix="/split"/>
<data android:scheme="https" android:host="staging.new.expensify.com" android:pathPrefix="/request"/>
<data android:scheme="https" android:host="staging.new.expensify.com" android:pathPrefix="/new"/>
<data android:scheme="https" android:host="staging.new.expensify.com" android:pathPrefix="/search"/>
<data android:scheme="https" android:host="staging.new.expensify.com" android:pathPrefix="/send"/>
<data android:scheme="https" android:host="staging.new.expensify.com" android:pathPrefix="/pay"/>
<data android:scheme="https" android:host="staging.new.expensify.com" android:pathPrefix="/money2020"/>
<data android:scheme="https" android:host="staging.new.expensify.com" android:pathPrefix="/track-expense"/>
<data android:scheme="https" android:host="staging.new.expensify.com" android:pathPrefix="/submit-expense"/>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll finish the changes in the PR related to the empty state in the report, and then I'll get started on this.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sumo-slonik ok there are some conflicts now, can you please check the deeplink configs for ios and android?

// eslint-disable-next-line @typescript-eslint/naming-convention
'/settings/workspaces/*': '/workspaces/',
// eslint-disable-next-line @typescript-eslint/naming-convention
'/settings/workspaces': '/workspaces',
};

export default oldRoutes;
25 changes: 25 additions & 0 deletions tests/navigation/getMatchingNewRouteTest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import getMatchingNewRoute from '@navigation/helpers/getMatchingNewRoute';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice job with the tests!


describe('getBestMatchingPath', () => {
it('returns mapped base path when input matches the exact pattern', () => {
expect(getMatchingNewRoute('/settings/workspaces/')).toBe('/workspaces/');
});

it('returns mapped base path when input matches the exact pattern', () => {
expect(getMatchingNewRoute('/settings/workspaces')).toBe('/workspaces');
});

it('returns mapped path when input matches the pattern and have more content', () => {
expect(getMatchingNewRoute('/settings/workspaces/anything/more')).toBe('/workspaces/anything/more');
});

it('returns undefined when input does not match any pattern - similar prefix but different ending', () => {
expect(getMatchingNewRoute('/settings/anything/')).toBe(undefined);
});

it('returns undefined when input is unrelated to any pattern', () => {
expect(getMatchingNewRoute('/anything/workspaces/')).toBe(undefined);
expect(getMatchingNewRoute('/anything/anything/')).toBe(undefined);
expect(getMatchingNewRoute('/anything/anything/anything')).toBe(undefined);
});
});
Loading