react-native-navigation 8.8.6 → 8.8.7
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.
- package/android/src/main/java/com/reactnativenavigation/NavigationApplication.java +3 -0
- package/android/src/main/java/com/reactnativenavigation/NavigationPackage.kt +27 -8
- package/android/src/main/java/com/reactnativenavigation/customrow/BottomTabsCustomRow.kt +262 -0
- package/android/src/main/java/com/reactnativenavigation/customrow/BottomTabsCustomRowAttacher.kt +205 -0
- package/android/src/main/java/com/reactnativenavigation/customrow/BottomTabsCustomRowConfigStore.kt +32 -0
- package/android/src/main/java/com/reactnativenavigation/customrow/BottomTabsCustomRowLayout.kt +139 -0
- package/android/src/main/java/com/reactnativenavigation/customrow/BottomTabsCustomRowModule.kt +37 -0
- package/android/src/main/java/com/reactnativenavigation/customrow/BottomTabsCustomRowOptions.kt +68 -0
- package/android/src/main/java/com/reactnativenavigation/options/BottomTabOptions.java +4 -1
- package/android/src/main/java/com/reactnativenavigation/react/ReactView.java +13 -0
- package/android/src/main/java/com/reactnativenavigation/react/events/ComponentType.java +2 -1
- package/android/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabPresenter.java +28 -0
- package/android/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabsController.java +59 -0
- package/android/src/main/java/com/reactnativenavigation/views/bottomtabs/BottomTabs.java +76 -0
- package/android/src/main/java/com/reactnativenavigation/views/bottomtabs/CustomBottomTabItemView.kt +73 -0
- package/android/src/main/java/com/reactnativenavigation/views/stack/topbar/titlebar/TitleBarReactButtonView.java +27 -11
- package/android/src/test/java/com/reactnativenavigation/views/TitleAndButtonsContainerTest.kt +15 -1
- package/android/src/test/java/com/reactnativenavigation/views/TitleBarReactButtonViewTest.java +135 -0
- package/ios/ARCHITECTURE.md +5 -0
- package/ios/BottomTabPresenter.h +7 -0
- package/ios/BottomTabPresenter.mm +27 -0
- package/ios/RNNAppDelegate.h +16 -0
- package/ios/RNNAppDelegate.mm +73 -0
- package/ios/RNNBottomTabOptions.h +2 -0
- package/ios/RNNBottomTabOptions.mm +5 -1
- package/ios/RNNBottomTabsController.h +2 -0
- package/ios/RNNBottomTabsController.mm +209 -1
- package/ios/RNNBottomTabsCustomRow.h +57 -0
- package/ios/RNNBottomTabsCustomRow.mm +252 -0
- package/ios/RNNBottomTabsCustomRowOptions.h +42 -0
- package/ios/RNNBottomTabsCustomRowOptions.mm +37 -0
- package/ios/RNNBottomTabsOptions.h +2 -0
- package/ios/RNNBottomTabsOptions.mm +2 -0
- package/ios/RNNComponentViewCreator.h +2 -1
- package/ios/RNNCustomTabBarItemView.h +26 -0
- package/ios/RNNCustomTabBarItemView.mm +83 -0
- package/ios/RNNReactRootViewCreator.mm +1 -0
- package/ios/RNNViewControllerFactory.mm +1 -0
- package/ios/ReactNativeNavigation.xcodeproj/project.pbxproj +24 -0
- package/lib/module/ARCHITECTURE.md +30 -0
- package/lib/module/Navigation.js +34 -1
- package/lib/module/Navigation.js.map +1 -1
- package/lib/module/NavigationDelegate.js +21 -0
- package/lib/module/NavigationDelegate.js.map +1 -1
- package/lib/module/adapters/AndroidCustomRowForwarder.js +75 -0
- package/lib/module/adapters/AndroidCustomRowForwarder.js.map +1 -0
- package/lib/module/commands/Commands.js +8 -0
- package/lib/module/commands/Commands.js.map +1 -1
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/interfaces/Options.js.map +1 -1
- package/lib/module/linking/DeferredLinkQueue.js +52 -0
- package/lib/module/linking/DeferredLinkQueue.js.map +1 -0
- package/lib/module/linking/DeferredLinkQueue.test.js +54 -0
- package/lib/module/linking/DeferredLinkQueue.test.js.map +1 -0
- package/lib/module/linking/LinkingHandler.js +139 -0
- package/lib/module/linking/LinkingHandler.js.map +1 -0
- package/lib/module/linking/LinkingHandler.test.js +384 -0
- package/lib/module/linking/LinkingHandler.test.js.map +1 -0
- package/lib/module/linking/ModalLayoutBuilder.js +56 -0
- package/lib/module/linking/ModalLayoutBuilder.js.map +1 -0
- package/lib/module/linking/ModalLayoutBuilder.test.js +154 -0
- package/lib/module/linking/ModalLayoutBuilder.test.js.map +1 -0
- package/lib/module/linking/RouteMatcher.js +104 -0
- package/lib/module/linking/RouteMatcher.js.map +1 -0
- package/lib/module/linking/RouteMatcher.test.js +164 -0
- package/lib/module/linking/RouteMatcher.test.js.map +1 -0
- package/lib/module/linking/URLParser.js +56 -0
- package/lib/module/linking/URLParser.js.map +1 -0
- package/lib/module/linking/URLParser.test.js +100 -0
- package/lib/module/linking/URLParser.test.js.map +1 -0
- package/lib/module/linking/types.js +4 -0
- package/lib/module/linking/types.js.map +1 -0
- package/lib/typescript/Navigation.d.ts +22 -0
- package/lib/typescript/Navigation.d.ts.map +1 -1
- package/lib/typescript/NavigationDelegate.d.ts +13 -0
- package/lib/typescript/NavigationDelegate.d.ts.map +1 -1
- package/lib/typescript/adapters/AndroidCustomRowForwarder.d.ts +23 -0
- package/lib/typescript/adapters/AndroidCustomRowForwarder.d.ts.map +1 -0
- package/lib/typescript/commands/Commands.d.ts +1 -0
- package/lib/typescript/commands/Commands.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +1 -0
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/interfaces/Options.d.ts +85 -0
- package/lib/typescript/interfaces/Options.d.ts.map +1 -1
- package/lib/typescript/linking/DeferredLinkQueue.d.ts +26 -0
- package/lib/typescript/linking/DeferredLinkQueue.d.ts.map +1 -0
- package/lib/typescript/linking/DeferredLinkQueue.test.d.ts +2 -0
- package/lib/typescript/linking/DeferredLinkQueue.test.d.ts.map +1 -0
- package/lib/typescript/linking/LinkingHandler.d.ts +71 -0
- package/lib/typescript/linking/LinkingHandler.d.ts.map +1 -0
- package/lib/typescript/linking/LinkingHandler.test.d.ts +2 -0
- package/lib/typescript/linking/LinkingHandler.test.d.ts.map +1 -0
- package/lib/typescript/linking/ModalLayoutBuilder.d.ts +21 -0
- package/lib/typescript/linking/ModalLayoutBuilder.d.ts.map +1 -0
- package/lib/typescript/linking/ModalLayoutBuilder.test.d.ts +2 -0
- package/lib/typescript/linking/ModalLayoutBuilder.test.d.ts.map +1 -0
- package/lib/typescript/linking/RouteMatcher.d.ts +23 -0
- package/lib/typescript/linking/RouteMatcher.d.ts.map +1 -0
- package/lib/typescript/linking/RouteMatcher.test.d.ts +2 -0
- package/lib/typescript/linking/RouteMatcher.test.d.ts.map +1 -0
- package/lib/typescript/linking/URLParser.d.ts +16 -0
- package/lib/typescript/linking/URLParser.d.ts.map +1 -0
- package/lib/typescript/linking/URLParser.test.d.ts +2 -0
- package/lib/typescript/linking/URLParser.test.d.ts.map +1 -0
- package/lib/typescript/linking/types.d.ts +107 -0
- package/lib/typescript/linking/types.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/ARCHITECTURE.md +30 -0
- package/src/Navigation.ts +36 -1
- package/src/NavigationDelegate.ts +22 -0
- package/src/adapters/AndroidCustomRowForwarder.ts +83 -0
- package/src/commands/Commands.ts +15 -0
- package/src/index.ts +1 -0
- package/src/interfaces/Options.ts +87 -0
- package/src/linking/DeferredLinkQueue.test.ts +60 -0
- package/src/linking/DeferredLinkQueue.ts +55 -0
- package/src/linking/LinkingHandler.test.ts +332 -0
- package/src/linking/LinkingHandler.ts +169 -0
- package/src/linking/ModalLayoutBuilder.test.ts +105 -0
- package/src/linking/ModalLayoutBuilder.ts +60 -0
- package/src/linking/RouteMatcher.test.ts +128 -0
- package/src/linking/RouteMatcher.ts +126 -0
- package/src/linking/URLParser.test.ts +105 -0
- package/src/linking/URLParser.ts +62 -0
- 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
package/src/ARCHITECTURE.md
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|
package/src/commands/Commands.ts
CHANGED
|
@@ -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
|
@@ -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
|
+
}
|