react-native-navigation 8.8.6 → 8.8.7-snapshot.2601

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/android/src/main/java/com/reactnativenavigation/NavigationApplication.java +3 -0
  2. package/android/src/main/java/com/reactnativenavigation/NavigationPackage.kt +27 -8
  3. package/android/src/main/java/com/reactnativenavigation/customrow/BottomTabsCustomRow.kt +262 -0
  4. package/android/src/main/java/com/reactnativenavigation/customrow/BottomTabsCustomRowAttacher.kt +205 -0
  5. package/android/src/main/java/com/reactnativenavigation/customrow/BottomTabsCustomRowConfigStore.kt +32 -0
  6. package/android/src/main/java/com/reactnativenavigation/customrow/BottomTabsCustomRowLayout.kt +139 -0
  7. package/android/src/main/java/com/reactnativenavigation/customrow/BottomTabsCustomRowModule.kt +37 -0
  8. package/android/src/main/java/com/reactnativenavigation/customrow/BottomTabsCustomRowOptions.kt +68 -0
  9. package/android/src/main/java/com/reactnativenavigation/options/BottomTabOptions.java +4 -1
  10. package/android/src/main/java/com/reactnativenavigation/react/ReactView.java +13 -0
  11. package/android/src/main/java/com/reactnativenavigation/react/events/ComponentType.java +2 -1
  12. package/android/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabPresenter.java +28 -0
  13. package/android/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabsController.java +59 -0
  14. package/android/src/main/java/com/reactnativenavigation/views/bottomtabs/BottomTabs.java +76 -0
  15. package/android/src/main/java/com/reactnativenavigation/views/bottomtabs/CustomBottomTabItemView.kt +73 -0
  16. package/android/src/main/java/com/reactnativenavigation/views/stack/topbar/titlebar/TitleBarReactButtonView.java +54 -12
  17. package/android/src/test/java/com/reactnativenavigation/views/TitleAndButtonsContainerTest.kt +15 -1
  18. package/android/src/test/java/com/reactnativenavigation/views/TitleBarReactButtonViewTest.java +199 -0
  19. package/ios/ARCHITECTURE.md +5 -0
  20. package/ios/BottomTabPresenter.h +7 -0
  21. package/ios/BottomTabPresenter.mm +27 -0
  22. package/ios/RNNAppDelegate.h +16 -0
  23. package/ios/RNNAppDelegate.mm +73 -0
  24. package/ios/RNNBottomTabOptions.h +2 -0
  25. package/ios/RNNBottomTabOptions.mm +5 -1
  26. package/ios/RNNBottomTabsController.h +2 -0
  27. package/ios/RNNBottomTabsController.mm +209 -1
  28. package/ios/RNNBottomTabsCustomRow.h +57 -0
  29. package/ios/RNNBottomTabsCustomRow.mm +252 -0
  30. package/ios/RNNBottomTabsCustomRowOptions.h +42 -0
  31. package/ios/RNNBottomTabsCustomRowOptions.mm +37 -0
  32. package/ios/RNNBottomTabsOptions.h +2 -0
  33. package/ios/RNNBottomTabsOptions.mm +2 -0
  34. package/ios/RNNComponentViewCreator.h +2 -1
  35. package/ios/RNNCustomTabBarItemView.h +26 -0
  36. package/ios/RNNCustomTabBarItemView.mm +83 -0
  37. package/ios/RNNReactRootViewCreator.mm +1 -0
  38. package/ios/RNNViewControllerFactory.mm +1 -0
  39. package/ios/ReactNativeNavigation.xcodeproj/project.pbxproj +24 -0
  40. package/lib/module/ARCHITECTURE.md +30 -0
  41. package/lib/module/Navigation.js +34 -1
  42. package/lib/module/Navigation.js.map +1 -1
  43. package/lib/module/NavigationDelegate.js +21 -0
  44. package/lib/module/NavigationDelegate.js.map +1 -1
  45. package/lib/module/adapters/AndroidCustomRowForwarder.js +75 -0
  46. package/lib/module/adapters/AndroidCustomRowForwarder.js.map +1 -0
  47. package/lib/module/commands/Commands.js +8 -0
  48. package/lib/module/commands/Commands.js.map +1 -1
  49. package/lib/module/index.js +1 -0
  50. package/lib/module/index.js.map +1 -1
  51. package/lib/module/interfaces/Options.js.map +1 -1
  52. package/lib/module/linking/DeferredLinkQueue.js +52 -0
  53. package/lib/module/linking/DeferredLinkQueue.js.map +1 -0
  54. package/lib/module/linking/DeferredLinkQueue.test.js +54 -0
  55. package/lib/module/linking/DeferredLinkQueue.test.js.map +1 -0
  56. package/lib/module/linking/LinkingHandler.js +139 -0
  57. package/lib/module/linking/LinkingHandler.js.map +1 -0
  58. package/lib/module/linking/LinkingHandler.test.js +384 -0
  59. package/lib/module/linking/LinkingHandler.test.js.map +1 -0
  60. package/lib/module/linking/ModalLayoutBuilder.js +56 -0
  61. package/lib/module/linking/ModalLayoutBuilder.js.map +1 -0
  62. package/lib/module/linking/ModalLayoutBuilder.test.js +154 -0
  63. package/lib/module/linking/ModalLayoutBuilder.test.js.map +1 -0
  64. package/lib/module/linking/RouteMatcher.js +104 -0
  65. package/lib/module/linking/RouteMatcher.js.map +1 -0
  66. package/lib/module/linking/RouteMatcher.test.js +164 -0
  67. package/lib/module/linking/RouteMatcher.test.js.map +1 -0
  68. package/lib/module/linking/URLParser.js +56 -0
  69. package/lib/module/linking/URLParser.js.map +1 -0
  70. package/lib/module/linking/URLParser.test.js +100 -0
  71. package/lib/module/linking/URLParser.test.js.map +1 -0
  72. package/lib/module/linking/types.js +4 -0
  73. package/lib/module/linking/types.js.map +1 -0
  74. package/lib/typescript/Navigation.d.ts +22 -0
  75. package/lib/typescript/Navigation.d.ts.map +1 -1
  76. package/lib/typescript/NavigationDelegate.d.ts +13 -0
  77. package/lib/typescript/NavigationDelegate.d.ts.map +1 -1
  78. package/lib/typescript/adapters/AndroidCustomRowForwarder.d.ts +23 -0
  79. package/lib/typescript/adapters/AndroidCustomRowForwarder.d.ts.map +1 -0
  80. package/lib/typescript/commands/Commands.d.ts +1 -0
  81. package/lib/typescript/commands/Commands.d.ts.map +1 -1
  82. package/lib/typescript/index.d.ts +1 -0
  83. package/lib/typescript/index.d.ts.map +1 -1
  84. package/lib/typescript/interfaces/Options.d.ts +85 -0
  85. package/lib/typescript/interfaces/Options.d.ts.map +1 -1
  86. package/lib/typescript/linking/DeferredLinkQueue.d.ts +26 -0
  87. package/lib/typescript/linking/DeferredLinkQueue.d.ts.map +1 -0
  88. package/lib/typescript/linking/DeferredLinkQueue.test.d.ts +2 -0
  89. package/lib/typescript/linking/DeferredLinkQueue.test.d.ts.map +1 -0
  90. package/lib/typescript/linking/LinkingHandler.d.ts +71 -0
  91. package/lib/typescript/linking/LinkingHandler.d.ts.map +1 -0
  92. package/lib/typescript/linking/LinkingHandler.test.d.ts +2 -0
  93. package/lib/typescript/linking/LinkingHandler.test.d.ts.map +1 -0
  94. package/lib/typescript/linking/ModalLayoutBuilder.d.ts +21 -0
  95. package/lib/typescript/linking/ModalLayoutBuilder.d.ts.map +1 -0
  96. package/lib/typescript/linking/ModalLayoutBuilder.test.d.ts +2 -0
  97. package/lib/typescript/linking/ModalLayoutBuilder.test.d.ts.map +1 -0
  98. package/lib/typescript/linking/RouteMatcher.d.ts +23 -0
  99. package/lib/typescript/linking/RouteMatcher.d.ts.map +1 -0
  100. package/lib/typescript/linking/RouteMatcher.test.d.ts +2 -0
  101. package/lib/typescript/linking/RouteMatcher.test.d.ts.map +1 -0
  102. package/lib/typescript/linking/URLParser.d.ts +16 -0
  103. package/lib/typescript/linking/URLParser.d.ts.map +1 -0
  104. package/lib/typescript/linking/URLParser.test.d.ts +2 -0
  105. package/lib/typescript/linking/URLParser.test.d.ts.map +1 -0
  106. package/lib/typescript/linking/types.d.ts +107 -0
  107. package/lib/typescript/linking/types.d.ts.map +1 -0
  108. package/package.json +1 -1
  109. package/src/ARCHITECTURE.md +30 -0
  110. package/src/Navigation.ts +36 -1
  111. package/src/NavigationDelegate.ts +22 -0
  112. package/src/adapters/AndroidCustomRowForwarder.ts +83 -0
  113. package/src/commands/Commands.ts +15 -0
  114. package/src/index.ts +1 -0
  115. package/src/interfaces/Options.ts +87 -0
  116. package/src/linking/DeferredLinkQueue.test.ts +60 -0
  117. package/src/linking/DeferredLinkQueue.ts +55 -0
  118. package/src/linking/LinkingHandler.test.ts +332 -0
  119. package/src/linking/LinkingHandler.ts +169 -0
  120. package/src/linking/ModalLayoutBuilder.test.ts +105 -0
  121. package/src/linking/ModalLayoutBuilder.ts +60 -0
  122. package/src/linking/RouteMatcher.test.ts +128 -0
  123. package/src/linking/RouteMatcher.ts +126 -0
  124. package/src/linking/URLParser.test.ts +105 -0
  125. package/src/linking/URLParser.ts +62 -0
  126. package/src/linking/types.ts +115 -0
@@ -0,0 +1,128 @@
1
+ import { RouteMatcher } from './RouteMatcher';
2
+ import { ScreensConfig } from './types';
3
+
4
+ describe('RouteMatcher', () => {
5
+ let uut: RouteMatcher;
6
+
7
+ beforeEach(() => {
8
+ uut = new RouteMatcher();
9
+ });
10
+
11
+ function configure(screens: ScreensConfig) {
12
+ uut.setRouteTree(uut.buildRouteTree(screens));
13
+ }
14
+
15
+ it('matches a flat route', () => {
16
+ configure({ Home: 'home' });
17
+ expect(uut.match('home', 'myapp://home', {})).toEqual({
18
+ url: 'myapp://home',
19
+ path: [{ screen: 'Home', params: {} }],
20
+ queryParams: {},
21
+ });
22
+ });
23
+
24
+ it('extracts a single path parameter', () => {
25
+ configure({ Profile: 'user/:id' });
26
+ expect(uut.match('user/42', 'myapp://user/42', {})).toEqual({
27
+ url: 'myapp://user/42',
28
+ path: [{ screen: 'Profile', params: { id: '42' } }],
29
+ queryParams: {},
30
+ });
31
+ });
32
+
33
+ it('extracts multiple path parameters', () => {
34
+ configure({ Post: 'user/:userId/post/:postId' });
35
+ const result = uut.match('user/5/post/99', 'myapp://user/5/post/99', {});
36
+ expect(result?.path[0].params).toEqual({ userId: '5', postId: '99' });
37
+ });
38
+
39
+ it('returns null when no route matches', () => {
40
+ configure({ Home: 'home' });
41
+ expect(uut.match('unknown', 'myapp://unknown', {})).toBeNull();
42
+ });
43
+
44
+ it('returns null when path is longer than any pattern', () => {
45
+ configure({ Home: 'home' });
46
+ expect(uut.match('home/extra', 'myapp://home/extra', {})).toBeNull();
47
+ });
48
+
49
+ it('matches the first registered route on ambiguity', () => {
50
+ configure({
51
+ Profile: 'user/:id',
52
+ User: 'user/:userId',
53
+ });
54
+ const result = uut.match('user/42', 'myapp://user/42', {});
55
+ expect(result?.path[0].screen).toBe('Profile');
56
+ });
57
+
58
+ it('matches a nested route as a chain', () => {
59
+ configure({
60
+ Settings: {
61
+ path: 'settings',
62
+ screens: { Notifications: 'notifications' },
63
+ },
64
+ });
65
+ expect(uut.match('settings/notifications', 'myapp://settings/notifications', {})).toEqual({
66
+ url: 'myapp://settings/notifications',
67
+ path: [
68
+ { screen: 'Settings', params: {} },
69
+ { screen: 'Notifications', params: {} },
70
+ ],
71
+ queryParams: {},
72
+ });
73
+ });
74
+
75
+ it('matches a parent-only nested route', () => {
76
+ configure({
77
+ Settings: {
78
+ path: 'settings',
79
+ screens: { Notifications: 'notifications' },
80
+ },
81
+ });
82
+ expect(uut.match('settings', 'myapp://settings', {})).toEqual({
83
+ url: 'myapp://settings',
84
+ path: [{ screen: 'Settings', params: {} }],
85
+ queryParams: {},
86
+ });
87
+ });
88
+
89
+ it('supports grouping nodes that consume no segments', () => {
90
+ configure({
91
+ Main: {
92
+ screens: {
93
+ Feed: 'feed',
94
+ Search: 'search',
95
+ },
96
+ },
97
+ });
98
+ const result = uut.match('feed', 'myapp://feed', {});
99
+ expect(result?.path).toEqual([
100
+ { screen: 'Main', params: {} },
101
+ { screen: 'Feed', params: {} },
102
+ ]);
103
+ });
104
+
105
+ it('passes query params through to the match result', () => {
106
+ configure({ Home: 'home' });
107
+ const result = uut.match('home', 'myapp://home?ref=push', { ref: 'push' });
108
+ expect(result?.queryParams).toEqual({ ref: 'push' });
109
+ });
110
+
111
+ it('returns null when no route tree has been configured', () => {
112
+ expect(uut.match('home', 'myapp://home', {})).toBeNull();
113
+ });
114
+
115
+ it('matches nested params at multiple levels', () => {
116
+ configure({
117
+ Org: {
118
+ path: 'org/:orgId',
119
+ screens: { Project: 'project/:projectId' },
120
+ },
121
+ });
122
+ const result = uut.match('org/1/project/2', 'myapp://org/1/project/2', {});
123
+ expect(result?.path).toEqual([
124
+ { screen: 'Org', params: { orgId: '1' } },
125
+ { screen: 'Project', params: { projectId: '2' } },
126
+ ]);
127
+ });
128
+ });
@@ -0,0 +1,126 @@
1
+ import {
2
+ ScreensConfig,
3
+ ScreenConfig,
4
+ RouteNode,
5
+ RouteMatch,
6
+ RouteMatchSegment,
7
+ } from './types';
8
+
9
+ /**
10
+ * Compiles a `screens` config into a tree of `RouteNode`s and resolves
11
+ * parsed paths to a matched `RouteMatch`.
12
+ *
13
+ * Matching is depth-first, in insertion order. The first node whose pattern
14
+ * (and nested children) consume the entire path wins.
15
+ */
16
+ export class RouteMatcher {
17
+ private routeTree: RouteNode[] | null = null;
18
+
19
+ public buildRouteTree(screens: ScreensConfig): RouteNode[] {
20
+ const nodes: RouteNode[] = [];
21
+ for (const [screenName, config] of Object.entries(screens)) {
22
+ nodes.push(this.buildNode(screenName, config));
23
+ }
24
+ return nodes;
25
+ }
26
+
27
+ public setRouteTree(tree: RouteNode[]): void {
28
+ this.routeTree = tree;
29
+ }
30
+
31
+ /**
32
+ * Match a parsed path (e.g. `"user/42"`) against the configured tree.
33
+ * Returns the matched chain or `null` when no route matches.
34
+ */
35
+ public match(
36
+ path: string,
37
+ url: string,
38
+ queryParams: Record<string, string>
39
+ ): RouteMatch | null {
40
+ if (!this.routeTree) return null;
41
+
42
+ const pathSegments = path.split('/').filter((s) => s.length > 0);
43
+ const result = this.matchRecursive(pathSegments, 0, this.routeTree);
44
+ if (!result) return null;
45
+
46
+ return { url, path: result, queryParams };
47
+ }
48
+
49
+ private buildNode(screenName: string, config: ScreenConfig): RouteNode {
50
+ if (typeof config === 'string') {
51
+ return {
52
+ screen: screenName,
53
+ pattern: config,
54
+ segments: this.splitPattern(config),
55
+ children: [],
56
+ };
57
+ }
58
+
59
+ const children = config.screens ? this.buildRouteTree(config.screens) : [];
60
+ return {
61
+ screen: screenName,
62
+ pattern: config.path ?? null,
63
+ segments: config.path ? this.splitPattern(config.path) : [],
64
+ children,
65
+ };
66
+ }
67
+
68
+ private splitPattern(pattern: string): string[] {
69
+ return pattern.split('/').filter((s) => s.length > 0);
70
+ }
71
+
72
+ private matchRecursive(
73
+ pathSegments: string[],
74
+ offset: number,
75
+ nodes: RouteNode[]
76
+ ): RouteMatchSegment[] | null {
77
+ for (const node of nodes) {
78
+ const result = this.tryMatchNode(pathSegments, offset, node);
79
+ if (result) return result;
80
+ }
81
+ return null;
82
+ }
83
+
84
+ private tryMatchNode(
85
+ pathSegments: string[],
86
+ offset: number,
87
+ node: RouteNode
88
+ ): RouteMatchSegment[] | null {
89
+ if (node.pattern === null && node.segments.length === 0) {
90
+ if (node.children.length === 0) return null;
91
+ const childMatch = this.matchRecursive(pathSegments, offset, node.children);
92
+ if (!childMatch) return null;
93
+ return [{ screen: node.screen, params: {} }, ...childMatch];
94
+ }
95
+
96
+ const patternSegments = node.segments;
97
+ if (offset + patternSegments.length > pathSegments.length) {
98
+ return null;
99
+ }
100
+
101
+ const params: Record<string, string> = {};
102
+ for (let i = 0; i < patternSegments.length; i++) {
103
+ const pattern = patternSegments[i];
104
+ const actual = pathSegments[offset + i];
105
+ if (pattern.startsWith(':')) {
106
+ params[pattern.slice(1)] = actual;
107
+ } else if (pattern !== actual) {
108
+ return null;
109
+ }
110
+ }
111
+
112
+ const newOffset = offset + patternSegments.length;
113
+ const segment: RouteMatchSegment = { screen: node.screen, params };
114
+
115
+ if (newOffset === pathSegments.length) {
116
+ return [segment];
117
+ }
118
+
119
+ if (node.children.length > 0) {
120
+ const childMatch = this.matchRecursive(pathSegments, newOffset, node.children);
121
+ if (childMatch) return [segment, ...childMatch];
122
+ }
123
+
124
+ return null;
125
+ }
126
+ }
@@ -0,0 +1,105 @@
1
+ import { URLParser } from './URLParser';
2
+
3
+ describe('URLParser', () => {
4
+ let uut: URLParser;
5
+
6
+ beforeEach(() => {
7
+ uut = new URLParser();
8
+ });
9
+
10
+ describe('stripPrefix', () => {
11
+ it('returns the remainder after the matching prefix', () => {
12
+ expect(uut.stripPrefix('myapp://home', ['myapp://'])).toBe('home');
13
+ });
14
+
15
+ it('returns null when no prefix matches', () => {
16
+ expect(uut.stripPrefix('other://home', ['myapp://'])).toBeNull();
17
+ });
18
+
19
+ it('chooses the longest matching prefix', () => {
20
+ const result = uut.stripPrefix('https://myapp.com/v2/home', [
21
+ 'https://myapp.com',
22
+ 'https://myapp.com/v2',
23
+ ]);
24
+ expect(result).toBe('/home');
25
+ });
26
+ });
27
+
28
+ describe('parse', () => {
29
+ const prefixes = ['myapp://', 'https://myapp.com'];
30
+
31
+ it('returns null when no prefix matches', () => {
32
+ expect(uut.parse('other://home', prefixes)).toBeNull();
33
+ });
34
+
35
+ it('parses a simple path', () => {
36
+ expect(uut.parse('myapp://home', prefixes)).toEqual({
37
+ path: 'home',
38
+ queryParams: {},
39
+ });
40
+ });
41
+
42
+ it('trims leading and trailing slashes', () => {
43
+ expect(uut.parse('myapp:///home/', prefixes)).toEqual({
44
+ path: 'home',
45
+ queryParams: {},
46
+ });
47
+ });
48
+
49
+ it('parses query parameters', () => {
50
+ expect(uut.parse('myapp://search?q=hello&page=2', prefixes)).toEqual({
51
+ path: 'search',
52
+ queryParams: { q: 'hello', page: '2' },
53
+ });
54
+ });
55
+
56
+ it('decodes URL-encoded path segments', () => {
57
+ expect(uut.parse('myapp://hello%20world', prefixes)).toEqual({
58
+ path: 'hello world',
59
+ queryParams: {},
60
+ });
61
+ });
62
+
63
+ it('decodes URL-encoded query parameter keys and values', () => {
64
+ expect(uut.parse('myapp://search?q=hello%20world&a%20b=c', prefixes)).toEqual({
65
+ path: 'search',
66
+ queryParams: { q: 'hello world', 'a b': 'c' },
67
+ });
68
+ });
69
+
70
+ it('strips fragments before parsing the query', () => {
71
+ expect(uut.parse('myapp://home?x=1#section', prefixes)).toEqual({
72
+ path: 'home',
73
+ queryParams: { x: '1' },
74
+ });
75
+ });
76
+
77
+ it('strips fragments when there is no query string', () => {
78
+ expect(uut.parse('myapp://home#section', prefixes)).toEqual({
79
+ path: 'home',
80
+ queryParams: {},
81
+ });
82
+ });
83
+
84
+ it('treats query keys without values as empty strings', () => {
85
+ expect(uut.parse('myapp://home?flag', prefixes)).toEqual({
86
+ path: 'home',
87
+ queryParams: { flag: '' },
88
+ });
89
+ });
90
+
91
+ it('handles malformed encoding without throwing', () => {
92
+ expect(uut.parse('myapp://%E0%A4%A', prefixes)).toEqual({
93
+ path: '%E0%A4%A',
94
+ queryParams: {},
95
+ });
96
+ });
97
+
98
+ it('keeps "=" inside values intact', () => {
99
+ expect(uut.parse('myapp://search?token=a=b=c', prefixes)).toEqual({
100
+ path: 'search',
101
+ queryParams: { token: 'a=b=c' },
102
+ });
103
+ });
104
+ });
105
+ });
@@ -0,0 +1,62 @@
1
+ import { ParsedURL } from './types';
2
+
3
+ /**
4
+ * Parses a URL string against a list of configured prefixes.
5
+ *
6
+ * Responsibilities:
7
+ * - Strip the longest matching prefix (or report no match).
8
+ * - Drop any `#fragment` portion.
9
+ * - Decode each path segment so literal patterns compare against decoded
10
+ * text (e.g. `hello%20world` matches a `hello world` pattern segment).
11
+ * - Parse the query string into a plain key/value map.
12
+ */
13
+ export class URLParser {
14
+ public stripPrefix(url: string, prefixes: string[]): string | null {
15
+ const sorted = [...prefixes].sort((a, b) => b.length - a.length);
16
+ for (const prefix of sorted) {
17
+ if (url.startsWith(prefix)) {
18
+ return url.slice(prefix.length);
19
+ }
20
+ }
21
+ return null;
22
+ }
23
+
24
+ public parse(url: string, prefixes: string[]): ParsedURL | null {
25
+ const stripped = this.stripPrefix(url, prefixes);
26
+ if (stripped === null) {
27
+ return null;
28
+ }
29
+
30
+ const withoutFragment = stripped.split('#', 1)[0];
31
+ const [pathPart, queryPart] = withoutFragment.split('?', 2);
32
+
33
+ const path = pathPart
34
+ .split('/')
35
+ .filter((s) => s.length > 0)
36
+ .map((segment) => safeDecode(segment))
37
+ .join('/');
38
+
39
+ const queryParams: Record<string, string> = {};
40
+ if (queryPart) {
41
+ for (const pair of queryPart.split('&')) {
42
+ if (!pair) continue;
43
+ const eqIndex = pair.indexOf('=');
44
+ const rawKey = eqIndex === -1 ? pair : pair.slice(0, eqIndex);
45
+ const rawValue = eqIndex === -1 ? '' : pair.slice(eqIndex + 1);
46
+ const key = safeDecode(rawKey);
47
+ if (!key) continue;
48
+ queryParams[key] = safeDecode(rawValue);
49
+ }
50
+ }
51
+
52
+ return { path, queryParams };
53
+ }
54
+ }
55
+
56
+ function safeDecode(value: string): string {
57
+ try {
58
+ return decodeURIComponent(value);
59
+ } catch {
60
+ return value;
61
+ }
62
+ }
@@ -0,0 +1,115 @@
1
+ import { Layout } from '../interfaces/Layout';
2
+
3
+ /**
4
+ * A single matched segment of a deep link route.
5
+ */
6
+ export interface RouteMatchSegment {
7
+ /** Name of the registered RNN component this segment maps to. */
8
+ screen: string;
9
+ /** Path parameters extracted from this segment's pattern (e.g. `:id`). */
10
+ params: Record<string, string>;
11
+ }
12
+
13
+ /**
14
+ * Result of matching a URL against the configured `screens` tree.
15
+ */
16
+ export interface RouteMatch {
17
+ /** Original URL that was matched. */
18
+ url: string;
19
+ /**
20
+ * Ordered chain of matched screens, from outermost to innermost.
21
+ * For a nested route like `settings/notifications`, this contains
22
+ * `[{ screen: 'Settings' }, { screen: 'Notifications' }]`.
23
+ */
24
+ path: RouteMatchSegment[];
25
+ /** Query string parameters from the URL. */
26
+ queryParams: Record<string, string>;
27
+ }
28
+
29
+ /**
30
+ * Internal representation of a parsed URL after prefix stripping.
31
+ */
32
+ export interface ParsedURL {
33
+ /** Decoded path with leading/trailing slashes removed. */
34
+ path: string;
35
+ /** Query string parameters. */
36
+ queryParams: Record<string, string>;
37
+ }
38
+
39
+ /**
40
+ * Internal compiled representation of a single screen entry in the route tree.
41
+ */
42
+ export interface RouteNode {
43
+ /** Screen name (the key from `screens`). */
44
+ screen: string;
45
+ /** Path pattern as written by the user, or `null` for grouping nodes. */
46
+ pattern: string | null;
47
+ /** Pattern split into segments, ready for matching. */
48
+ segments: string[];
49
+ /** Nested children. */
50
+ children: RouteNode[];
51
+ }
52
+
53
+ /**
54
+ * Per-screen configuration. Either a path pattern string, or an object with
55
+ * an optional path and nested screens.
56
+ *
57
+ * Examples:
58
+ * `'home'` - leaf route
59
+ * `'user/:id'` - leaf with path parameter
60
+ * `{ path: 'settings', screens: {...} }` - nested route
61
+ * `{ screens: {...} }` - grouping node (no path)
62
+ */
63
+ export type ScreenConfig =
64
+ | string
65
+ | {
66
+ path?: string;
67
+ screens?: ScreensConfig;
68
+ };
69
+
70
+ export interface ScreensConfig {
71
+ [screenName: string]: ScreenConfig;
72
+ }
73
+
74
+ /**
75
+ * Configuration passed to `Navigation.setLinking`.
76
+ *
77
+ * When a deep link is received, RNN parses it, matches it against the
78
+ * `screens` tree, and by default presents the matched chain as a modal
79
+ * (wrapped in a stack so a topBar is available for a close button).
80
+ *
81
+ * To customize the modal layout, provide `getModal`. To bypass the modal
82
+ * behavior entirely (e.g. push onto an existing stack), provide `onLink`.
83
+ */
84
+ export interface LinkingConfig {
85
+ /** URL prefixes your app handles (custom schemes and universal-link hosts). */
86
+ prefixes: string[];
87
+ /** Screen-to-path mapping. */
88
+ config: {
89
+ screens: ScreensConfig;
90
+ };
91
+ /**
92
+ * Customize the modal layout for a matched route. Return `undefined` to
93
+ * skip presenting a modal for this match (useful for conditional skipping).
94
+ * When omitted, the default builder wraps the matched chain in a stack.
95
+ */
96
+ getModal?: (match: RouteMatch) => Layout | undefined;
97
+ /**
98
+ * Full escape hatch. When provided, RNN does not present a modal; the
99
+ * handler is responsible for executing whatever navigation commands it
100
+ * wants (push, setRoot, dismissAllModals + showModal, etc.).
101
+ * When set, `getModal` is ignored.
102
+ */
103
+ onLink?: (match: RouteMatch) => void;
104
+ /**
105
+ * Called when a received URL does not match any configured route or has
106
+ * no matching prefix. Useful for logging or routing to a "not found" flow.
107
+ */
108
+ fallback?: (url: string) => void;
109
+ /**
110
+ * Predicate evaluated before each link is processed. When it returns
111
+ * `false`, the link is queued and replayed once `setLinkingReady(true)`
112
+ * is called. Use this to defer links until e.g. authentication completes.
113
+ */
114
+ isReady?: () => boolean;
115
+ }