From c6cfc28f51eba6878da3290f4c05e79d9d8cfbbd Mon Sep 17 00:00:00 2001 From: staszekscp Date: Fri, 27 Jun 2025 11:22:04 +0200 Subject: [PATCH 1/2] Remove getStateFromPath patch --- ...ion+core+7.10.0+002+getStateFromPath.patch | 361 ------------------ src/ROUTES.ts | 6 +- src/libs/Navigation/linkingConfig/config.ts | 3 - 3 files changed, 3 insertions(+), 367 deletions(-) delete mode 100644 patches/react-navigation/@react-navigation+core+7.10.0+002+getStateFromPath.patch diff --git a/patches/react-navigation/@react-navigation+core+7.10.0+002+getStateFromPath.patch b/patches/react-navigation/@react-navigation+core+7.10.0+002+getStateFromPath.patch deleted file mode 100644 index 75fefac95da5..000000000000 --- a/patches/react-navigation/@react-navigation+core+7.10.0+002+getStateFromPath.patch +++ /dev/null @@ -1,361 +0,0 @@ -diff --git a/node_modules/@react-navigation/core/lib/module/getStateFromPath.js b/node_modules/@react-navigation/core/lib/module/getStateFromPath.js -index 7132844..8af0a15 100644 ---- a/node_modules/@react-navigation/core/lib/module/getStateFromPath.js -+++ b/node_modules/@react-navigation/core/lib/module/getStateFromPath.js -@@ -29,31 +29,23 @@ import { validatePathConfig } from "./validatePathConfig.js"; - * @param options Extra options to fine-tune how to parse the path. - */ - export function getStateFromPath(path, options) { -- const { -- initialRoutes, -- configs -- } = getConfigResources(options); -- const screens = options?.screens; -+ if (options) { -+ validatePathConfig(options); -+ } -+ let initialRoutes = []; -+ if (options !== null && options !== void 0 && options.initialRouteName) { -+ initialRoutes.push({ -+ initialRouteName: options.initialRouteName, -+ parentScreens: [] -+ }); -+ } -+ const screens = options === null || options === void 0 ? void 0 : options.screens; - let remaining = path.replace(/\/+/g, '/') // Replace multiple slash (//) with single ones -- .replace(/^\//, '') // Remove extra leading slash -- .replace(/\?.*$/, ''); // Remove query params which we will handle later -+ .replace(/^\//, '') // Remove extra leading slash -+ .replace(/\?.*$/, ''); // Remove query params which we will handle later - - // Make sure there is a trailing slash - remaining = remaining.endsWith('/') ? remaining : `${remaining}/`; -- const prefix = options?.path?.replace(/^\//, ''); // Remove extra leading slash -- -- if (prefix) { -- // Make sure there is a trailing slash -- const normalizedPrefix = prefix.endsWith('/') ? prefix : `${prefix}/`; -- -- // If the path doesn't start with the prefix, it's not a match -- if (!remaining.startsWith(normalizedPrefix)) { -- return undefined; -- } -- -- // Remove the prefix from the path -- remaining = remaining.replace(normalizedPrefix, ''); -- } - if (screens === undefined) { - // When no config is specified, use the path segments as route names - const routes = remaining.split('/').filter(Boolean).map(segment => { -@@ -67,10 +59,82 @@ export function getStateFromPath(path, options) { - } - return undefined; - } -+ -+ // Create a normalized configs array which will be easier to use -+ const configs = [].concat(...Object.keys(screens).map(key => createNormalizedConfigs(key, screens, [], initialRoutes, []))).sort((a, b) => { -+ // Sort config so that: -+ // - the most exhaustive ones are always at the beginning -+ // - patterns with wildcard are always at the end -+ -+ // If 2 patterns are same, move the one with less route names up -+ // This is an error state, so it's only useful for consistent error messages -+ if (a.pattern === b.pattern) { -+ return b.routeNames.join('>').localeCompare(a.routeNames.join('>')); -+ } -+ -+ // If one of the patterns starts with the other, it's more exhaustive -+ // So move it up -+ if (a.pattern.startsWith(b.pattern)) { -+ return -1; -+ } -+ if (b.pattern.startsWith(a.pattern)) { -+ return 1; -+ } -+ const aParts = a.pattern.split('/'); -+ const bParts = b.pattern.split('/'); -+ for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) { -+ // if b is longer, b get higher priority -+ if (aParts[i] == null) { -+ return 1; -+ } -+ // if a is longer, a get higher priority -+ if (bParts[i] == null) { -+ return -1; -+ } -+ const aWildCard = aParts[i] === '*' || aParts[i].startsWith(':'); -+ const bWildCard = bParts[i] === '*' || bParts[i].startsWith(':'); -+ // if both are wildcard we compare next component -+ if (aWildCard && bWildCard) { -+ continue; -+ } -+ // if only a is wild card, b get higher priority -+ if (aWildCard) { -+ return 1; -+ } -+ // if only b is wild card, a get higher priority -+ if (bWildCard) { -+ return -1; -+ } -+ } -+ return bParts.length - aParts.length; -+ }); -+ -+ // Check for duplicate patterns in the config -+ configs.reduce((acc, config) => { -+ if (acc[config.pattern]) { -+ const a = acc[config.pattern].routeNames; -+ const b = config.routeNames; -+ -+ // It's not a problem if the path string omitted from a inner most screen -+ // For example, it's ok if a path resolves to `A > B > C` or `A > B` -+ const intersects = a.length > b.length ? b.every((it, i) => a[i] === it) : a.every((it, i) => b[i] === it); -+ if (!intersects) { -+ throw new Error(`Found conflicting screens with the same pattern. The pattern '${config.pattern}' resolves to both '${a.join(' > ')}' and '${b.join(' > ')}'. Patterns must be unique and cannot resolve to more than one screen.`); -+ } -+ } -+ return Object.assign(acc, { -+ [config.pattern]: config -+ }); -+ }, {}); - if (remaining === '/') { - // We need to add special handling of empty path so navigation to empty path also works - // When handling empty path, we should only look at the root level config -- const match = configs.find(config => config.segments.join('/') === ''); -+ const match = configs.find(config => config.path === '' && config.routeNames.every( -+ // Make sure that none of the parent configs have a non-empty path defined -+ name => { -+ var _configs$find; -+ return !((_configs$find = configs.find(c => c.screen === name)) !== null && _configs$find !== void 0 && _configs$find.path); -+ })); - if (match) { - return createNestedStateObject(path, match.routeNames.map(name => ({ - name -@@ -86,7 +150,11 @@ export function getStateFromPath(path, options) { - const { - routes, - remainingPath -- } = matchAgainstConfigs(remaining, configs); -+ } = matchAgainstConfigs(remaining, configs.map(c => ({ -+ ...c, -+ // Add `$` to the regex to make sure it matches till end of the path and not just beginning -+ regex: c.regex ? new RegExp(c.regex.source + '$') : undefined -+ }))); - if (routes !== undefined) { - // This will always be empty if full path matched - current = createNestedStateObject(path, routes, initialRoutes, configs); -@@ -241,6 +309,14 @@ function getConfigsWithRegexes(configs) { - regex: c.regex ? new RegExp(c.regex.source + '$') : undefined - })); - } -+ -+const joinPaths = function () { -+ for (var _len = arguments.length, paths = new Array(_len), _key = 0; _key < _len; _key++) { -+ paths[_key] = arguments[_key]; -+ } -+ return [].concat(...paths.map(p => p.split('/'))).filter(Boolean).join('/'); -+}; -+ - const matchAgainstConfigs = (remaining, configs) => { - let routes; - let remainingPath = remaining; -@@ -254,37 +330,34 @@ const matchAgainstConfigs = (remaining, configs) => { - - // If our regex matches, we need to extract params from the path - if (match) { -- routes = config.routeNames.map(routeName => { -- const routeConfig = configs.find(c => { -- // Check matching name AND pattern in case same screen is used at different levels in config -- return c.screen === routeName && arrayStartsWith(config.segments, c.segments); -- }); -- const params = routeConfig && match.groups ? Object.fromEntries(Object.entries(match.groups).map(([key, value]) => { -- const index = Number(key.replace('param_', '')); -- const param = routeConfig.params.find(it => it.index === index); -- if (param?.screen === routeName && param?.name) { -- return [param.name, value]; -- } -- return null; -- }).filter(it => it != null).map(([key, value]) => { -- if (value == null) { -- return [key, undefined]; -+ var _config$pattern; -+ const matchedParams = (_config$pattern = config.pattern) === null || _config$pattern === void 0 ? void 0 : _config$pattern.split('/').filter(p => p.startsWith(':')).reduce((acc, p, i) => Object.assign(acc, { -+ // The param segments appear every second item starting from 2 in the regex match result -+ [p]: match[(i + 1) * 2].replace(/\//, '') -+ }), {}); -+ routes = config.routeNames.map(name => { -+ var _config$path; -+ const config = configs.find(c => c.screen === name); -+ const params = config === null || config === void 0 ? void 0 : (_config$path = config.path) === null || _config$path === void 0 ? void 0 : _config$path.split('/').filter(p => p.startsWith(':')).reduce((acc, p) => { -+ const value = matchedParams[p]; -+ if (value) { -+ var _config$parse; -+ const key = p.replace(/^:/, '').replace(/\?$/, ''); -+ acc[key] = (_config$parse = config.parse) !== null && _config$parse !== void 0 && _config$parse[key] ? config.parse[key](value) : value; - } -- const decoded = decodeURIComponent(value); -- const parsed = routeConfig.parse?.[key] ? routeConfig.parse[key](decoded) : decoded; -- return [key, parsed]; -- })) : undefined; -+ return acc; -+ }, {}); - if (params && Object.keys(params).length) { - return { -- name: routeName, -+ name, - params - }; - } - return { -- name: routeName -+ name - }; - }); -- remainingPath = remainingPath.replace(match[0], ''); -+ remainingPath = remainingPath.replace(match[1], ''); - break; - } - } -@@ -293,61 +366,34 @@ const matchAgainstConfigs = (remaining, configs) => { - remainingPath - }; - }; --const createNormalizedConfigs = (screen, routeConfig, initials, paths, parentScreens, routeNames) => { -+ -+const createNormalizedConfigs = function (screen, routeConfig) { -+ let routeNames = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : []; -+ let initials = arguments.length > 3 ? arguments[3] : undefined; -+ let parentScreens = arguments.length > 4 ? arguments[4] : undefined; -+ let parentPattern = arguments.length > 5 ? arguments[5] : undefined; - const configs = []; - routeNames.push(screen); - parentScreens.push(screen); -+ -+ // @ts-expect-error: we can't strongly typecheck this for now - const config = routeConfig[screen]; - if (typeof config === 'string') { -- paths.push({ -- screen, -- path: config -- }); -- configs.push(createConfigItem(screen, [...routeNames], [...paths])); -+ // If a string is specified as the value of the key(e.g. Foo: '/path'), use it as the pattern -+ const pattern = parentPattern ? joinPaths(parentPattern, config) : config; -+ configs.push(createConfigItem(screen, routeNames, pattern, config)); - } else if (typeof config === 'object') { -+ let pattern; -+ - // if an object is specified as the value (e.g. Foo: { ... }), - // it can have `path` property and - // it could have `screens` prop which has nested configs - if (typeof config.path === 'string') { -- if (config.exact && config.path == null) { -- throw new Error(`Screen '${screen}' doesn't specify a 'path'. A 'path' needs to be specified when specifying 'exact: true'. If you don't want this screen in the URL, specify it as empty string, e.g. \`path: ''\`.`); -+ if (config.exact && config.path === undefined) { -+ throw new Error("A 'path' needs to be specified when specifying 'exact: true'. If you don't want this screen in the URL, specify it as empty string, e.g. `path: ''`."); - } -- -- // We should add alias configs after the main config -- // So unless they are more specific, main config will be matched first -- const aliasConfigs = []; -- if (config.alias) { -- for (const alias of config.alias) { -- if (typeof alias === 'string') { -- aliasConfigs.push(createConfigItem(screen, [...routeNames], [...paths, { -- screen, -- path: alias -- }], config.parse)); -- } else if (typeof alias === 'object') { -- aliasConfigs.push(createConfigItem(screen, [...routeNames], alias.exact ? [{ -- screen, -- path: alias.path -- }] : [...paths, { -- screen, -- path: alias.path -- }], alias.parse)); -- } -- } -- } -- if (config.exact) { -- // If it's an exact path, we don't need to keep track of the parent screens -- // So we can clear it -- paths.length = 0; -- } -- paths.push({ -- screen, -- path: config.path -- }); -- configs.push(createConfigItem(screen, [...routeNames], [...paths], config.parse)); -- configs.push(...aliasConfigs); -- } -- if (typeof config !== 'string' && typeof config.path !== 'string' && config.alias?.length) { -- throw new Error(`Screen '${screen}' doesn't specify a 'path'. A 'path' needs to be specified in order to use 'alias'.`); -+ pattern = config.exact !== true ? joinPaths(parentPattern || '', config.path || '') : config.path || ''; -+ configs.push(createConfigItem(screen, routeNames, pattern, config.path, config.parse)); - } - if (config.screens) { - // property `initialRouteName` without `screens` has no purpose -@@ -358,7 +404,7 @@ const createNormalizedConfigs = (screen, routeConfig, initials, paths, parentScr - }); - } - Object.keys(config.screens).forEach(nestedConfig => { -- const result = createNormalizedConfigs(nestedConfig, config.screens, initials, [...paths], [...parentScreens], routeNames); -+ const result = createNormalizedConfigs(nestedConfig, config.screens, routeNames, initials, [...parentScreens], pattern ?? parentPattern); - configs.push(...result); - }); - } -@@ -366,41 +412,27 @@ const createNormalizedConfigs = (screen, routeConfig, initials, paths, parentScr - routeNames.pop(); - return configs; - }; --const createConfigItem = (screen, routeNames, paths, parse) => { -- const parts = []; - -- // Parse the path string into parts for easier matching -- for (const { -- screen, -- path -- } of paths) { -- parts.push(...getPatternParts(path).map(part => ({ -- ...part, -- screen -- }))); -- } -- const regex = parts.length ? new RegExp(`^(${parts.map((it, i) => { -- if (it.param) { -- const reg = it.regex || '[^/]+'; -- return `(((?${reg})\\/)${it.optional ? '?' : ''})`; -+const createConfigItem = (screen, routeNames, pattern, path, parse) => { -+ // Normalize pattern to remove any leading, trailing slashes, duplicate slashes etc. -+ pattern = pattern.split('/').filter(Boolean).join('/'); -+ const regex = pattern ? new RegExp(`^(${pattern.split('/').map(it => { -+ if (it.startsWith(':')) { -+ return `(([^/]+\\/)${it.endsWith('?') ? '?' : ''})`; - } -- return `${it.segment === '*' ? '.*' : escape(it.segment)}\\/`; -- }).join('')})$`) : undefined; -- const segments = parts.map(it => it.segment); -- const params = parts.map((it, i) => it.param ? { -- index: i, -- screen: it.screen, -- name: it.param -- } : null).filter(it => it != null); -+ return `${it === '*' ? '.*' : escape(it)}\\/`; -+ }).join('')})`) : undefined; - return { - screen, - regex, -- segments, -- params, -- routeNames, -+ pattern, -+ path, -+ // The routeNames array is mutated, so copy it to keep the current state -+ routeNames: [...routeNames], - parse - }; - }; -+ - const findParseConfigForRoute = (routeName, flatConfig) => { - for (const config of flatConfig) { - if (routeName === config.routeNames[config.routeNames.length - 1]) { diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 15d7874dc603..d86def1c1122 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -890,17 +890,17 @@ const ROUTES = { getRoute: (iouType: IOUType, iouRequestType: IOURequestType) => `start/${iouType as string}/${iouRequestType as string}` as const, }, MONEY_REQUEST_CREATE_TAB_DISTANCE: { - route: ':action/:iouType/start/:transactionID/:reportID/distance/:backToReport?', + route: 'distance/:backToReport?', getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string, backToReport?: string) => `create/${iouType as string}/start/${transactionID}/${reportID}/distance/${backToReport ?? ''}` as const, }, MONEY_REQUEST_CREATE_TAB_MANUAL: { - route: ':action/:iouType/start/:transactionID/:reportID/manual/:backToReport?', + route: 'manual/:backToReport?', getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string, backToReport?: string) => `${action as string}/${iouType as string}/start/${transactionID}/${reportID}/manual/${backToReport ?? ''}` as const, }, MONEY_REQUEST_CREATE_TAB_SCAN: { - route: ':action/:iouType/start/:transactionID/:reportID/scan/:backToReport?', + route: 'scan/:backToReport?', getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string, backToReport?: string) => `create/${iouType as string}/start/${transactionID}/${reportID}/scan/${backToReport ?? ''}` as const, }, diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 9c5cbc519edf..561b2578ba0e 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -1325,15 +1325,12 @@ const config: LinkingOptions['config'] = { screens: { distance: { path: ROUTES.MONEY_REQUEST_CREATE_TAB_DISTANCE.route, - exact: true, }, manual: { path: ROUTES.MONEY_REQUEST_CREATE_TAB_MANUAL.route, - exact: true, }, scan: { path: ROUTES.MONEY_REQUEST_CREATE_TAB_SCAN.route, - exact: true, }, // eslint-disable-next-line @typescript-eslint/naming-convention 'per-diem': { From 53600d2f4dc12f1c5694637b4660993611df077a Mon Sep 17 00:00:00 2001 From: staszekscp Date: Fri, 27 Jun 2025 11:33:13 +0200 Subject: [PATCH 2/2] Update details.md for react-navigation patches --- patches/react-navigation/details.md | 7 ------- 1 file changed, 7 deletions(-) diff --git a/patches/react-navigation/details.md b/patches/react-navigation/details.md index 0a05e9ff5c24..42bebae7a2e8 100644 --- a/patches/react-navigation/details.md +++ b/patches/react-navigation/details.md @@ -47,13 +47,6 @@ - PR Introducing Patch: [#22437](https://github.com/Expensify/App/pull/22437) - PR Updating Patch: [#33280](https://github.com/Expensify/App/pull/33280) [#37421](https://github.com/Expensify/App/pull/37421) [#49539](https://github.com/Expensify/App/pull/49539) [#64155](https://github.com/Expensify/App/pull/64155) -### [@react-navigation+core+7.10.0+002+getStateFromPath.patch](@react-navigation+core+7.10.0+002+getStateFromPath.patch) -- Reason: Make sure navigation state props retrieved from the path are available at all nesting levels to avoid undefined state. -- Upstream PR/issue: N/A -- E/App issue: [#48150](https://github.com/Expensify/App/issues/48150) -- PR Introducing Patch: [#48151](https://github.com/Expensify/App/pull/48151) -- PR Updating Patch: [#64155](https://github.com/Expensify/App/pull/64155) - ### [@react-navigation+stack+7.3.3+003+fix-invisible-sidebar-safari.patch](@react-navigation+stack+7.3.3+003+fix-invisible-sidebar-safari.patch) - Reason: Sidebar is invisible on the safari - Upstream PR/issue: N/A