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,107 @@
1
+ import { Layout } from '../interfaces/Layout';
2
+ /**
3
+ * A single matched segment of a deep link route.
4
+ */
5
+ export interface RouteMatchSegment {
6
+ /** Name of the registered RNN component this segment maps to. */
7
+ screen: string;
8
+ /** Path parameters extracted from this segment's pattern (e.g. `:id`). */
9
+ params: Record<string, string>;
10
+ }
11
+ /**
12
+ * Result of matching a URL against the configured `screens` tree.
13
+ */
14
+ export interface RouteMatch {
15
+ /** Original URL that was matched. */
16
+ url: string;
17
+ /**
18
+ * Ordered chain of matched screens, from outermost to innermost.
19
+ * For a nested route like `settings/notifications`, this contains
20
+ * `[{ screen: 'Settings' }, { screen: 'Notifications' }]`.
21
+ */
22
+ path: RouteMatchSegment[];
23
+ /** Query string parameters from the URL. */
24
+ queryParams: Record<string, string>;
25
+ }
26
+ /**
27
+ * Internal representation of a parsed URL after prefix stripping.
28
+ */
29
+ export interface ParsedURL {
30
+ /** Decoded path with leading/trailing slashes removed. */
31
+ path: string;
32
+ /** Query string parameters. */
33
+ queryParams: Record<string, string>;
34
+ }
35
+ /**
36
+ * Internal compiled representation of a single screen entry in the route tree.
37
+ */
38
+ export interface RouteNode {
39
+ /** Screen name (the key from `screens`). */
40
+ screen: string;
41
+ /** Path pattern as written by the user, or `null` for grouping nodes. */
42
+ pattern: string | null;
43
+ /** Pattern split into segments, ready for matching. */
44
+ segments: string[];
45
+ /** Nested children. */
46
+ children: RouteNode[];
47
+ }
48
+ /**
49
+ * Per-screen configuration. Either a path pattern string, or an object with
50
+ * an optional path and nested screens.
51
+ *
52
+ * Examples:
53
+ * `'home'` - leaf route
54
+ * `'user/:id'` - leaf with path parameter
55
+ * `{ path: 'settings', screens: {...} }` - nested route
56
+ * `{ screens: {...} }` - grouping node (no path)
57
+ */
58
+ export type ScreenConfig = string | {
59
+ path?: string;
60
+ screens?: ScreensConfig;
61
+ };
62
+ export interface ScreensConfig {
63
+ [screenName: string]: ScreenConfig;
64
+ }
65
+ /**
66
+ * Configuration passed to `Navigation.setLinking`.
67
+ *
68
+ * When a deep link is received, RNN parses it, matches it against the
69
+ * `screens` tree, and by default presents the matched chain as a modal
70
+ * (wrapped in a stack so a topBar is available for a close button).
71
+ *
72
+ * To customize the modal layout, provide `getModal`. To bypass the modal
73
+ * behavior entirely (e.g. push onto an existing stack), provide `onLink`.
74
+ */
75
+ export interface LinkingConfig {
76
+ /** URL prefixes your app handles (custom schemes and universal-link hosts). */
77
+ prefixes: string[];
78
+ /** Screen-to-path mapping. */
79
+ config: {
80
+ screens: ScreensConfig;
81
+ };
82
+ /**
83
+ * Customize the modal layout for a matched route. Return `undefined` to
84
+ * skip presenting a modal for this match (useful for conditional skipping).
85
+ * When omitted, the default builder wraps the matched chain in a stack.
86
+ */
87
+ getModal?: (match: RouteMatch) => Layout | undefined;
88
+ /**
89
+ * Full escape hatch. When provided, RNN does not present a modal; the
90
+ * handler is responsible for executing whatever navigation commands it
91
+ * wants (push, setRoot, dismissAllModals + showModal, etc.).
92
+ * When set, `getModal` is ignored.
93
+ */
94
+ onLink?: (match: RouteMatch) => void;
95
+ /**
96
+ * Called when a received URL does not match any configured route or has
97
+ * no matching prefix. Useful for logging or routing to a "not found" flow.
98
+ */
99
+ fallback?: (url: string) => void;
100
+ /**
101
+ * Predicate evaluated before each link is processed. When it returns
102
+ * `false`, the link is queued and replayed once `setLinkingReady(true)`
103
+ * is called. Use this to defer links until e.g. authentication completes.
104
+ */
105
+ isReady?: () => boolean;
106
+ }
107
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/linking/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAE9C;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,iEAAiE;IACjE,MAAM,EAAE,MAAM,CAAC;IACf,0EAA0E;IAC1E,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAChC;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,qCAAqC;IACrC,GAAG,EAAE,MAAM,CAAC;IACZ;;;;OAIG;IACH,IAAI,EAAE,iBAAiB,EAAE,CAAC;IAC1B,4CAA4C;IAC5C,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACrC;AAED;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB,0DAA0D;IAC1D,IAAI,EAAE,MAAM,CAAC;IACb,+BAA+B;IAC/B,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACrC;AAED;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB,4CAA4C;IAC5C,MAAM,EAAE,MAAM,CAAC;IACf,yEAAyE;IACzE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,uDAAuD;IACvD,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,uBAAuB;IACvB,QAAQ,EAAE,SAAS,EAAE,CAAC;CACvB;AAED;;;;;;;;;GASG;AACH,MAAM,MAAM,YAAY,GACpB,MAAM,GACN;IACE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,aAAa,CAAC;CACzB,CAAC;AAEN,MAAM,WAAW,aAAa;IAC5B,CAAC,UAAU,EAAE,MAAM,GAAG,YAAY,CAAC;CACpC;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,aAAa;IAC5B,+EAA+E;IAC/E,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,8BAA8B;IAC9B,MAAM,EAAE;QACN,OAAO,EAAE,aAAa,CAAC;KACxB,CAAC;IACF;;;;OAIG;IACH,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,MAAM,GAAG,SAAS,CAAC;IACrD;;;;;OAKG;IACH,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;IACrC;;;OAGG;IACH,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,OAAO,CAAC;CACzB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-navigation",
3
- "version": "8.8.6",
3
+ "version": "8.8.7-snapshot.2601",
4
4
  "description": "React Native Navigation - truly native navigation for iOS and Android",
5
5
  "license": "MIT",
6
6
  "nativePackage": true,
@@ -18,6 +18,7 @@ src/
18
18
  ├── components/ # React component management
19
19
  ├── events/ # Event system
20
20
  ├── interfaces/ # TypeScript type definitions
21
+ ├── linking/ # Deep-linking framework (URL → command)
21
22
  └── processors/ # Extensibility hooks
22
23
  ```
23
24
 
@@ -207,6 +208,35 @@ Notifies listeners of command lifecycle (start, complete).
207
208
  | `RNN.PreviewCompleted` | 3D Touch preview completed |
208
209
  | `RNN.CommandCompleted` | Navigation command finished |
209
210
 
211
+ ## Linking Layer
212
+
213
+ Implements URL-to-screen routing on top of the standard command pipeline. The whole feature is JS-only; it consumes URLs from RN's `Linking` module and dispatches `showModal` (or a user-supplied command) when a match is found.
214
+
215
+ **Public surface** (proxied through `NavigationDelegate`):
216
+ - `Navigation.setLinking(config)` — configure prefixes, screen map, and customization hooks
217
+ - `Navigation.handleDeepLink(url)` — manually feed a URL (push notifications, branch.io, App Clips)
218
+ - `Navigation.setLinkingReady(ready)` — user-controlled gate for deferred deep links
219
+
220
+ **Internal modules** (`src/linking/`):
221
+
222
+ | File | Purpose |
223
+ |------|---------|
224
+ | `LinkingHandler.ts` | Orchestrator. Subscribes to `Linking.url`/`getInitialURL`, drives the queue, dispatches matches. Instantiated by `NavigationRoot`. |
225
+ | `URLParser.ts` | Strips prefix (longest match), removes fragment, decodes path segments + query params. |
226
+ | `RouteMatcher.ts` | Compiles `ScreensConfig` into a `RouteNode` tree; matches paths to screen chains with parameter extraction. |
227
+ | `DeferredLinkQueue.ts` | FIFO queue. Holds URLs until both gates open (root-ready + user-ready). |
228
+ | `ModalLayoutBuilder.ts` | Default presentation: wraps the matched chain in a `stack`, merges params into `passProps`, filters React-reserved keys (`ref`, `key`). |
229
+ | `types.ts` | Public types: `LinkingConfig`, `RouteMatch`, `ScreensConfig`, etc. (re-exported via `src/index.ts`). |
230
+
231
+ **Readiness gates** — a URL is dispatched only when both are true:
232
+ 1. **Root-ready** (automatic): the first `Navigation.setRoot()` has resolved. `NavigationRoot.setRoot` calls `linkingHandler.setRootReady()` after the promise resolves.
233
+ 2. **User-ready** (optional): `config.isReady()` returns `true`, or `setLinkingReady(true)` was called.
234
+
235
+ **Resolution priority** when a match is found:
236
+ 1. `config.onLink(match)` — full escape hatch, RNN does nothing else
237
+ 2. `config.getModal(match)` — custom modal layout
238
+ 3. Default `ModalLayoutBuilder` output
239
+
210
240
  ## Processors Layer
211
241
 
212
242
  ### OptionProcessorsStore
package/src/Navigation.ts CHANGED
@@ -28,6 +28,8 @@ import { LayoutProcessorsStore } from './processors/LayoutProcessorsStore';
28
28
  import { CommandName } from './interfaces/CommandName';
29
29
  import { OptionsCrawler } from './commands/OptionsCrawler';
30
30
  import { OptionsProcessor as OptionProcessor } from './interfaces/Processors';
31
+ import { LinkingHandler } from './linking/LinkingHandler';
32
+ import { LinkingConfig } from './linking/types';
31
33
 
32
34
  export class NavigationRoot {
33
35
  public readonly TouchablePreview = TouchablePreview;
@@ -45,6 +47,7 @@ export class NavigationRoot {
45
47
  private readonly componentEventsObserver: ComponentEventsObserver;
46
48
  private readonly componentWrapper: ComponentWrapper;
47
49
  private readonly optionsCrawler: OptionsCrawler;
50
+ private readonly linkingHandler: LinkingHandler;
48
51
 
49
52
  constructor(
50
53
  private readonly nativeCommandsSender: NativeCommandsSender,
@@ -96,6 +99,7 @@ export class NavigationRoot {
96
99
  this.commandsObserver,
97
100
  this.componentEventsObserver
98
101
  );
102
+ this.linkingHandler = new LinkingHandler((layout) => this.commands.showModal(layout));
99
103
 
100
104
  this.componentEventsObserver.registerOnceForAllComponentEvents();
101
105
  }
@@ -168,7 +172,9 @@ export class NavigationRoot {
168
172
  * Reset the app to a new layout
169
173
  */
170
174
  public setRoot(layout: LayoutRoot): Promise<string> {
171
- return this.commands.setRoot(layout);
175
+ const result = this.commands.setRoot(layout);
176
+ result.then(() => this.linkingHandler.setRootReady()).catch(() => {});
177
+ return result;
172
178
  }
173
179
 
174
180
  /**
@@ -280,6 +286,35 @@ export class NavigationRoot {
280
286
  return this.commands.getLaunchArgs();
281
287
  }
282
288
 
289
+ /**
290
+ * Configure deep link handling. Maps URL prefixes and path patterns to
291
+ * RNN components. By default each matched link is presented as a modal
292
+ * (wrapped in a stack so a topBar close button can be configured); supply
293
+ * `getModal` to customize the modal layout or `onLink` to bypass the
294
+ * default behavior entirely.
295
+ */
296
+ public setLinking(config: LinkingConfig): void {
297
+ this.linkingHandler.configure(config);
298
+ }
299
+
300
+ /**
301
+ * Manually run a URL through the deep link pipeline as if it had been
302
+ * delivered by the OS. Useful for URLs received from push notifications
303
+ * or other non-`Linking` sources.
304
+ */
305
+ public handleDeepLink(url: string): void {
306
+ this.linkingHandler.handleURL(url);
307
+ }
308
+
309
+ /**
310
+ * Signal that the app is ready to process deep links (e.g. after the
311
+ * user has authenticated). When set to `true`, any links that were
312
+ * queued while not ready are replayed in order.
313
+ */
314
+ public setLinkingReady(ready: boolean): void {
315
+ this.linkingHandler.setLinkingReady(ready);
316
+ }
317
+
283
318
  /**
284
319
  * Obtain the events registry instance
285
320
  */
@@ -10,6 +10,7 @@ import { NavigationRoot } from './Navigation';
10
10
  import { NativeCommandsSender } from './adapters/NativeCommandsSender';
11
11
  import { NativeEventsReceiver } from './adapters/NativeEventsReceiver';
12
12
  import { AppRegistryService } from './adapters/AppRegistryService';
13
+ import { LinkingConfig } from './linking/types';
13
14
 
14
15
  export class NavigationDelegate {
15
16
  private concreteNavigation: NavigationRoot;
@@ -204,6 +205,27 @@ export class NavigationDelegate {
204
205
  return this.concreteNavigation.getLaunchArgs();
205
206
  }
206
207
 
208
+ /**
209
+ * Configure deep link handling.
210
+ */
211
+ public setLinking(config: LinkingConfig): void {
212
+ this.concreteNavigation.setLinking(config);
213
+ }
214
+
215
+ /**
216
+ * Manually feed a URL into the deep link pipeline.
217
+ */
218
+ public handleDeepLink(url: string): void {
219
+ this.concreteNavigation.handleDeepLink(url);
220
+ }
221
+
222
+ /**
223
+ * Signal readiness to process deep links. Flushes any queued links.
224
+ */
225
+ public setLinkingReady(ready: boolean): void {
226
+ this.concreteNavigation.setLinkingReady(ready);
227
+ }
228
+
207
229
  /**
208
230
  * Obtain the events registry instance
209
231
  */
@@ -0,0 +1,83 @@
1
+ import { NativeModules, Platform } from 'react-native';
2
+
3
+ import { Options } from '../interfaces/Options';
4
+
5
+ /**
6
+ * Walks a Layout / Options tree looking for `bottomTabs.customRow`
7
+ * configuration and forwards it to the Android-only native module
8
+ * `RNNBottomTabsCustomRowModule`.
9
+ *
10
+ * On iOS this is a no-op — the existing native options parser already picks
11
+ * up `bottomTabs.customRow` and applies it to the iOS custom row.
12
+ *
13
+ * The original layout/options object is *never* modified — both pipelines
14
+ * (iOS native parser, Android native parser which currently ignores the
15
+ * field) keep seeing the same data.
16
+ */
17
+ export class AndroidCustomRowForwarder {
18
+ forwardFromLayout(layout: any) {
19
+ if (!this.shouldForward()) return;
20
+ const config = this.findCustomRowInLayout(layout);
21
+ if (config) this.send(config);
22
+ }
23
+
24
+ forwardFromLayouts(layouts: any[]) {
25
+ if (!this.shouldForward()) return;
26
+ for (const layout of layouts) {
27
+ const config = this.findCustomRowInLayout(layout);
28
+ if (config) {
29
+ this.send(config);
30
+ return;
31
+ }
32
+ }
33
+ }
34
+
35
+ forwardFromOptions(options: Options | undefined) {
36
+ if (!this.shouldForward() || !options) return;
37
+ const config = this.extractFromOptions(options);
38
+ if (config) this.send(config);
39
+ }
40
+
41
+ private shouldForward(): boolean {
42
+ return Platform.OS === 'android';
43
+ }
44
+
45
+ private findCustomRowInLayout(layout: any): object | null {
46
+ if (!layout || typeof layout !== 'object') return null;
47
+ // Walk both the raw layout shape (`layout.options`) and the
48
+ // post-`layoutTreeParser.parse` shape (`layout.data.options`).
49
+ const direct =
50
+ this.extractFromOptions(layout.options) ??
51
+ this.extractFromOptions(layout.data?.options);
52
+ if (direct) return direct;
53
+ const children = layout.children;
54
+ if (Array.isArray(children)) {
55
+ for (const child of children) {
56
+ const found = this.findCustomRowInLayout(child);
57
+ if (found) return found;
58
+ }
59
+ }
60
+ return null;
61
+ }
62
+
63
+ private extractFromOptions(options: Options | undefined): object | null {
64
+ if (!options || typeof options !== 'object') return null;
65
+ const bottomTabs: any = (options as any).bottomTabs;
66
+ if (!bottomTabs || typeof bottomTabs !== 'object') return null;
67
+ const customRow = bottomTabs.customRow;
68
+ if (!customRow || typeof customRow !== 'object') return null;
69
+ return customRow;
70
+ }
71
+
72
+ private send(config: object): void {
73
+ const nativeModule = (NativeModules as any).RNNBottomTabsCustomRowModule;
74
+ if (!nativeModule || typeof nativeModule.configure !== 'function') {
75
+ return;
76
+ }
77
+ try {
78
+ nativeModule.configure(config);
79
+ } catch {
80
+ // Native module not ready yet; attacher will pick up config on next rescan.
81
+ }
82
+ }
83
+ }
@@ -13,8 +13,11 @@ import { Store } from '../components/Store';
13
13
  import { LayoutProcessor } from '../processors/LayoutProcessor';
14
14
  import { CommandName } from '../interfaces/CommandName';
15
15
  import { OptionsCrawler } from './OptionsCrawler';
16
+ import { AndroidCustomRowForwarder } from '../adapters/AndroidCustomRowForwarder';
16
17
 
17
18
  export class Commands {
19
+ private readonly androidCustomRowForwarder = new AndroidCustomRowForwarder();
20
+
18
21
  constructor(
19
22
  private readonly store: Store,
20
23
  private readonly nativeCommandsSender: NativeCommandsSender,
@@ -59,6 +62,8 @@ export class Commands {
59
62
  this.layoutTreeCrawler.crawl(overlayLayout, CommandName.SetRoot);
60
63
  });
61
64
 
65
+ this.androidCustomRowForwarder.forwardFromLayouts([root, ...modals, ...overlays]);
66
+
62
67
  const result = this.nativeCommandsSender.setRoot(commandId, { root, modals, overlays });
63
68
  return result;
64
69
  }
@@ -67,6 +72,8 @@ export class Commands {
67
72
  const input = cloneDeep(options);
68
73
  this.optionsProcessor.processDefaultOptions(input, CommandName.SetDefaultOptions);
69
74
 
75
+ this.androidCustomRowForwarder.forwardFromOptions(input);
76
+
70
77
  this.nativeCommandsSender.setDefaultOptions(input);
71
78
  this.commandsObserver.notify(CommandName.SetDefaultOptions, { options });
72
79
  }
@@ -82,6 +89,8 @@ export class Commands {
82
89
  `Navigation.mergeOptions was invoked on component with id: ${componentId} before it is mounted, this can cause UI issues and should be avoided.\n Use static options instead.`
83
90
  );
84
91
 
92
+ this.androidCustomRowForwarder.forwardFromOptions(input);
93
+
85
94
  this.nativeCommandsSender.mergeOptions(componentId, input);
86
95
  this.commandsObserver.notify(CommandName.MergeOptions, { componentId, options });
87
96
  }
@@ -101,6 +110,8 @@ export class Commands {
101
110
  this.commandsObserver.notify(CommandName.ShowModal, { commandId, layout: layoutNode });
102
111
  this.layoutTreeCrawler.crawl(layoutNode, CommandName.ShowModal);
103
112
 
113
+ this.androidCustomRowForwarder.forwardFromLayout(layoutNode);
114
+
104
115
  const result = this.nativeCommandsSender.showModal(commandId, layoutNode);
105
116
  return result;
106
117
  }
@@ -135,6 +146,8 @@ export class Commands {
135
146
  this.commandsObserver.notify(CommandName.Push, { commandId, componentId, layout });
136
147
  this.layoutTreeCrawler.crawl(layout, CommandName.Push);
137
148
 
149
+ this.androidCustomRowForwarder.forwardFromLayout(layout);
150
+
138
151
  const result = this.nativeCommandsSender.push(commandId, componentId, layout);
139
152
  return result;
140
153
  }
@@ -181,6 +194,8 @@ export class Commands {
181
194
  this.layoutTreeCrawler.crawl(layoutNode, CommandName.SetStackRoot);
182
195
  });
183
196
 
197
+ this.androidCustomRowForwarder.forwardFromLayouts(input);
198
+
184
199
  const result = this.nativeCommandsSender.setStackRoot(commandId, componentId, input);
185
200
  return result;
186
201
  }
package/src/index.ts CHANGED
@@ -16,3 +16,4 @@ export * from './interfaces/NavigationFunctionComponent';
16
16
  export * from './interfaces/CommandName';
17
17
  export * from './interfaces/Processors';
18
18
  export * from './interfaces/ProcessorSubscription';
19
+ export * from './linking/types';
@@ -999,6 +999,54 @@ export interface OptionsBottomTabs {
999
999
  * Control the shadow of the Bottom tabs bar
1000
1000
  */
1001
1001
  shadow?: ShadowOptions;
1002
+ /**
1003
+ * Visual options for the floating row that hosts custom React-component
1004
+ * bottom tab cells. Only takes effect when every `bottomTab.component`
1005
+ * is set on that `bottomTabs` layout.
1006
+ *
1007
+ * Same JS shape on iOS and Android. iOS applies options via the native
1008
+ * parser; Android receives them through `RNNBottomTabsCustomRowModule`.
1009
+ */
1010
+ customRow?: BottomTabsCustomRowOptions;
1011
+ }
1012
+
1013
+ export interface BottomTabsCustomRowOptions {
1014
+ /**
1015
+ * Content height of the row in points (iOS) / dp (Android). Excludes
1016
+ * safe-area inset. Total row height = `height` + safe bottom + `bottomMargin`.
1017
+ *
1018
+ * Default: native tab content height; on iOS 26+ an extra 18pt when omitted.
1019
+ */
1020
+ height?: number;
1021
+ /**
1022
+ * Solid background color for the row. When set, overrides
1023
+ * `backgroundEffect`.
1024
+ */
1025
+ backgroundColor?: Color;
1026
+ /**
1027
+ * Visual effect for the row background.
1028
+ * - `glass`: iOS 26+ `UIGlassEffect`.
1029
+ * - `blur`: `UIBlurEffect` with `systemChromeMaterial`.
1030
+ * - `none`: fully transparent.
1031
+ *
1032
+ * Default: `glass` on iOS 26+, `blur` on older versions.
1033
+ */
1034
+ backgroundEffect?: 'glass' | 'blur' | 'none';
1035
+ /**
1036
+ * Corner radius applied to the row's background.
1037
+ * Default: 28 on iOS 26+, 0 below.
1038
+ */
1039
+ cornerRadius?: number;
1040
+ /**
1041
+ * Horizontal inset from the screen edges.
1042
+ * Default: 16 on iOS 26+, 0 below.
1043
+ */
1044
+ horizontalMargin?: number;
1045
+ /**
1046
+ * Distance between the row's bottom edge and the safe-area bottom.
1047
+ * Default: 0.
1048
+ */
1049
+ bottomMargin?: number;
1002
1050
  }
1003
1051
 
1004
1052
  export interface ShadowOptions {
@@ -1035,6 +1083,45 @@ export type ImageResource = ImageSourcePropType | string | ImageSystemSource;
1035
1083
  export interface OptionsBottomTab {
1036
1084
  dotIndicator?: DotIndicatorOptions;
1037
1085
 
1086
+ /**
1087
+ * Render a React component as the tab item content (icon + text area).
1088
+ *
1089
+ * When set, the native icon, text and font props (`text`, `icon`,
1090
+ * `selectedIcon`, `sfSymbol`, `sfSelectedSymbol`, `iconColor`,
1091
+ * `selectedIconColor`, `iconWidth`, `iconHeight`, `iconInsets`,
1092
+ * `fontFamily`, `fontWeight`, `fontSize`, `selectedFontSize`,
1093
+ * `textColor`, `selectedTextColor`) are ignored for that tab.
1094
+ *
1095
+ * For the custom rendering to take effect every tab in the
1096
+ * `bottomTabs` layout must declare a `component`. If only some
1097
+ * tabs declare one a warning is logged and native rendering is
1098
+ * used for all tabs.
1099
+ *
1100
+ * The component receives the following initial props and
1101
+ * subsequent prop updates:
1102
+ * - `componentId`: stable id for this tab item instance
1103
+ * - `tabIndex`: position of the tab (0-based)
1104
+ * - `selected`: whether this tab is currently selected
1105
+ * - `badge`: current badge text (mirrors `bottomTab.badge`)
1106
+ *
1107
+ * Native still owns selection, hide/show, drawBehind, animations
1108
+ * and the dot indicator. To switch tabs from inside the component
1109
+ * use `Navigation.mergeOptions(parentId, { bottomTabs: { currentTabIndex } })`.
1110
+ */
1111
+ component?: {
1112
+ /**
1113
+ * The registered name of the component (passed to
1114
+ * `Navigation.registerComponent`).
1115
+ */
1116
+ name: string;
1117
+ /**
1118
+ * Props passed once when the component is created. Updates from
1119
+ * native (`selected`, `badge`) are delivered as separate prop
1120
+ * updates.
1121
+ */
1122
+ passProps?: object;
1123
+ };
1124
+
1038
1125
  /**
1039
1126
  * Set the text to display below the icon
1040
1127
  */
@@ -0,0 +1,60 @@
1
+ import { DeferredLinkQueue } from './DeferredLinkQueue';
2
+
3
+ describe('DeferredLinkQueue', () => {
4
+ let uut: DeferredLinkQueue;
5
+ let flushed: string[];
6
+
7
+ beforeEach(() => {
8
+ uut = new DeferredLinkQueue();
9
+ flushed = [];
10
+ uut.setFlushCallback((url) => flushed.push(url));
11
+ });
12
+
13
+ it('starts not ready', () => {
14
+ expect(uut.isReady()).toBe(false);
15
+ });
16
+
17
+ it('enqueues URLs while not ready', () => {
18
+ uut.enqueue('myapp://a');
19
+ uut.enqueue('myapp://b');
20
+ expect(uut.pending()).toEqual(['myapp://a', 'myapp://b']);
21
+ expect(flushed).toEqual([]);
22
+ });
23
+
24
+ it('flushes queued URLs in order when becoming ready', () => {
25
+ uut.enqueue('myapp://a');
26
+ uut.enqueue('myapp://b');
27
+ uut.setReady(true);
28
+ expect(flushed).toEqual(['myapp://a', 'myapp://b']);
29
+ expect(uut.pending()).toEqual([]);
30
+ });
31
+
32
+ it('does not re-flush when already ready', () => {
33
+ uut.setReady(true);
34
+ uut.enqueue('myapp://a');
35
+ uut.setReady(true);
36
+ expect(flushed).toEqual([]);
37
+ expect(uut.pending()).toEqual(['myapp://a']);
38
+ });
39
+
40
+ it('flushes again after toggling not-ready → ready', () => {
41
+ uut.setReady(true);
42
+ uut.setReady(false);
43
+ uut.enqueue('myapp://x');
44
+ uut.setReady(true);
45
+ expect(flushed).toEqual(['myapp://x']);
46
+ });
47
+
48
+ it('clear() drops pending URLs', () => {
49
+ uut.enqueue('myapp://a');
50
+ uut.clear();
51
+ uut.setReady(true);
52
+ expect(flushed).toEqual([]);
53
+ });
54
+
55
+ it('flush() is a no-op when no callback is registered', () => {
56
+ const fresh = new DeferredLinkQueue();
57
+ fresh.enqueue('myapp://a');
58
+ expect(() => fresh.flush()).not.toThrow();
59
+ });
60
+ });
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Holds URLs while the linking pipeline is not ready and replays them in
3
+ * order once it becomes ready.
4
+ *
5
+ * Readiness is set by the owning `LinkingHandler` and reflects the combined
6
+ * state of:
7
+ * - whether the first `setRoot` has resolved (so a modal can be presented),
8
+ * - whether the user-supplied `isReady` predicate (if any) returns `true`.
9
+ */
10
+ export class DeferredLinkQueue {
11
+ private queue: string[] = [];
12
+ private ready = false;
13
+ private flushCallback: ((url: string) => void) | null = null;
14
+
15
+ public setFlushCallback(callback: (url: string) => void): void {
16
+ this.flushCallback = callback;
17
+ }
18
+
19
+ public setReady(ready: boolean): void {
20
+ const becameReady = !this.ready && ready;
21
+ this.ready = ready;
22
+ if (becameReady) {
23
+ this.flush();
24
+ }
25
+ }
26
+
27
+ public isReady(): boolean {
28
+ return this.ready;
29
+ }
30
+
31
+ /**
32
+ * Enqueue a URL for later processing. Should only be called when the
33
+ * queue is not ready; the handler decides when to enqueue vs process.
34
+ */
35
+ public enqueue(url: string): void {
36
+ this.queue.push(url);
37
+ }
38
+
39
+ public flush(): void {
40
+ if (!this.flushCallback) return;
41
+ const pending = [...this.queue];
42
+ this.queue = [];
43
+ for (const url of pending) {
44
+ this.flushCallback(url);
45
+ }
46
+ }
47
+
48
+ public clear(): void {
49
+ this.queue = [];
50
+ }
51
+
52
+ public pending(): string[] {
53
+ return [...this.queue];
54
+ }
55
+ }