Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion contributingGuides/NAVIGATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -710,7 +710,7 @@ Do not use dynamic routes when:
`DYNAMIC_ROUTES` in `src/ROUTES.ts`: each entry has:

- `path`: The URL suffix (e.g. `'verify-account'`).
- `entryScreens`: List of screen names that are allowed to have this suffix appended (access control; see [Entry Screens (Access Control)](#entry-screens-access-control)).
- `entryScreens`: List of screen names that are allowed to have this suffix appended (access control; see [Entry Screens (Access Control)](#entry-screens-access-control)). Use `['*']` to allow all screens.

`createDynamicRoute(suffix)` — [`createDynamicRoute.ts`](src/libs/Navigation/helpers/createDynamicRoute.ts). Accepts a `DynamicRouteSuffix` (from `DYNAMIC_ROUTES`), appends it to the current active route and returns the full route. Use the following when navigating to a dynamic route:

Expand All @@ -731,6 +731,24 @@ When parsing a URL, `src/libs/Navigation/helpers/getStateFromPath.ts` resolves t

When adding or extending a dynamic route, list every screen that should be able to open it (e.g. `SCREENS.SETTINGS.WALLET.ROOT` for Verify Account from Wallet).

#### Wildcard access (`'*'`)

Setting `entryScreens` to `['*']` grants access to the dynamic route from any screen. This bypasses per-screen authorization entirely for that route.

```ts
KEYBOARD_SHORTCUTS: {
path: 'keyboard-shortcuts',
entryScreens: ['*'],
},
```

> [!CAUTION]
> **Use `'*'` only when the dynamic route genuinely needs to be reachable from every screen.**
> If only a subset of screens should access the route, list them explicitly.
> Overusing `'*'` weakens the access control that `entryScreens` provides
> and makes it harder to reason about which screens can trigger a given flow.
> When in doubt, prefer an explicit list.

### Current limitations (work in progress)

- **Path parameters:** Suffixes must not include path params (e.g. `a/:reportID`). Query parameters are supported - see [Dynamic routes with query parameters](#dynamic-routes-with-query-parameters).
Expand Down
2 changes: 1 addition & 1 deletion src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ const VERIFY_ACCOUNT = 'verify-account';

type DynamicRouteConfig = {
path: string;
entryScreens: Screen[];
entryScreens: ReadonlyArray<Screen | '*'>;
getRoute?: (...args: never[]) => string;
queryParams?: readonly string[];
};
Expand Down
4 changes: 2 additions & 2 deletions src/libs/Navigation/helpers/getStateFromPath.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,11 @@ function getStateFromPath(path: Route): PartialState<NavigationState> {

// Get the currently focused route from the base path to check permissions
const focusedRoute = findFocusedRouteWithOnyxTabGuard(getStateFromPath(pathWithoutDynamicSuffix) ?? {});
const entryScreens: Screen[] = DYNAMIC_ROUTES[dynamicRoute as DynamicRouteKey]?.entryScreens ?? [];
const entryScreens: ReadonlyArray<Screen | '*'> = DYNAMIC_ROUTES[dynamicRoute as DynamicRouteKey]?.entryScreens ?? [];

// Check if the focused route is allowed to access this dynamic route
if (focusedRoute?.name) {
if (entryScreens.includes(focusedRoute.name as Screen)) {
if (entryScreens.some((s) => s === '*' || s === focusedRoute.name)) {
// Generate navigation state for the dynamic route
const dynamicRouteState = getStateForDynamicRoute(normalizedPath, dynamicRoute as DynamicRouteKey, focusedRoute?.params as Record<string, unknown> | undefined);
return dynamicRouteState;
Expand Down
31 changes: 31 additions & 0 deletions tests/navigation/getStateFromPathTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ jest.mock('@src/ROUTES', () => ({
path: 'suffix-b-from-multi',
entryScreens: ['DynamicMultiSegScreen'],
},
WILDCARD_SUFFIX: {
path: 'wildcard-suffix',
entryScreens: ['*'],
},
},
}));

Expand All @@ -62,6 +66,7 @@ describe('getStateFromPath', () => {
const dynamicSuffixBState = {routes: [{name: 'DynamicSuffixBScreen'}]};
const dynamicMultiSegState = {routes: [{name: 'DynamicMultiSegScreen', params: focusedRouteParams}]};
const dynamicMultiSegLayerState = {routes: [{name: 'DynamicMultiSegLayerScreen'}]};
const dynamicWildcardState = {routes: [{name: 'DynamicWildcardScreen'}]};

beforeEach(() => {
jest.clearAllMocks();
Expand All @@ -79,6 +84,9 @@ describe('getStateFromPath', () => {
if (dynamicRouteKey === 'MULTI_SEG_LAYER') {
return dynamicMultiSegLayerState;
}
if (dynamicRouteKey === 'WILDCARD_SUFFIX') {
return dynamicWildcardState;
}
return {routes: [{name: 'UnknownDynamic'}]};
});
});
Expand Down Expand Up @@ -159,4 +167,27 @@ describe('getStateFromPath', () => {
expect(mockGetStateForDynamicRoute).toHaveBeenCalledWith(fullPath, 'MULTI_SEG_LAYER', focusedRouteParams);
});
});

describe('wildcard entryScreens', () => {
it('should authorize any focused screen when entryScreens contains wildcard', () => {
const fullPath = '/base/wildcard-suffix';

const result = getStateFromPath(fullPath as unknown as Route);

expect(result).toBe(dynamicWildcardState);
expect(mockGetStateForDynamicRoute).toHaveBeenCalledWith(fullPath, 'WILDCARD_SUFFIX', focusedRouteParams);
expect(mockLogWarn).not.toHaveBeenCalled();
});

it('should authorize wildcard in a layered scenario where the inner screen is not explicitly listed', () => {
const fullPath = '/base/suffix-a/wildcard-suffix';

const result = getStateFromPath(fullPath as unknown as Route);

expect(result).toBe(dynamicWildcardState);
expect(mockGetStateForDynamicRoute).toHaveBeenCalledWith('/base/suffix-a', 'SUFFIX_A', focusedRouteParams);
expect(mockGetStateForDynamicRoute).toHaveBeenCalledWith(fullPath, 'WILDCARD_SUFFIX', focusedRouteParams);
expect(mockLogWarn).not.toHaveBeenCalled();
});
});
});
Loading