react-native-navigation 8.8.5 → 8.8.6-snapshot.2571

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 (131) hide show
  1. package/android/src/main/java/com/reactnativenavigation/NavigationActivity.java +14 -1
  2. package/android/src/main/java/com/reactnativenavigation/NavigationApplication.java +3 -0
  3. package/android/src/main/java/com/reactnativenavigation/NavigationPackage.kt +27 -8
  4. package/android/src/main/java/com/reactnativenavigation/customrow/BottomTabsCustomRow.kt +262 -0
  5. package/android/src/main/java/com/reactnativenavigation/customrow/BottomTabsCustomRowAttacher.kt +205 -0
  6. package/android/src/main/java/com/reactnativenavigation/customrow/BottomTabsCustomRowConfigStore.kt +32 -0
  7. package/android/src/main/java/com/reactnativenavigation/customrow/BottomTabsCustomRowLayout.kt +139 -0
  8. package/android/src/main/java/com/reactnativenavigation/customrow/BottomTabsCustomRowModule.kt +37 -0
  9. package/android/src/main/java/com/reactnativenavigation/customrow/BottomTabsCustomRowOptions.kt +68 -0
  10. package/android/src/main/java/com/reactnativenavigation/options/BottomTabOptions.java +4 -1
  11. package/android/src/main/java/com/reactnativenavigation/options/NavigationBarOptions.java +19 -1
  12. package/android/src/main/java/com/reactnativenavigation/react/ReactView.java +13 -0
  13. package/android/src/main/java/com/reactnativenavigation/react/events/ComponentType.java +2 -1
  14. package/android/src/main/java/com/reactnativenavigation/utils/SystemUiUtils.kt +63 -9
  15. package/android/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabPresenter.java +28 -0
  16. package/android/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabsController.java +77 -6
  17. package/android/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabsPresenter.kt +1 -0
  18. package/android/src/main/java/com/reactnativenavigation/viewcontrollers/component/ComponentViewController.java +2 -5
  19. package/android/src/main/java/com/reactnativenavigation/viewcontrollers/viewcontroller/Presenter.java +33 -13
  20. package/android/src/main/java/com/reactnativenavigation/views/bottomtabs/BottomTabs.java +76 -0
  21. package/android/src/main/java/com/reactnativenavigation/views/bottomtabs/CustomBottomTabItemView.kt +73 -0
  22. package/android/src/test/java/com/reactnativenavigation/presentation/PresenterTest.java +14 -0
  23. package/android/src/test/java/com/reactnativenavigation/utils/SystemUiUtilsTest.kt +64 -1
  24. package/ios/ARCHITECTURE.md +5 -0
  25. package/ios/BottomTabPresenter.h +7 -0
  26. package/ios/BottomTabPresenter.mm +27 -0
  27. package/ios/RNNAppDelegate.h +16 -0
  28. package/ios/RNNAppDelegate.mm +73 -0
  29. package/ios/RNNBottomTabOptions.h +2 -0
  30. package/ios/RNNBottomTabOptions.mm +5 -1
  31. package/ios/RNNBottomTabsController.h +2 -0
  32. package/ios/RNNBottomTabsController.mm +209 -1
  33. package/ios/RNNBottomTabsCustomRow.h +57 -0
  34. package/ios/RNNBottomTabsCustomRow.mm +252 -0
  35. package/ios/RNNBottomTabsCustomRowOptions.h +42 -0
  36. package/ios/RNNBottomTabsCustomRowOptions.mm +37 -0
  37. package/ios/RNNBottomTabsOptions.h +2 -0
  38. package/ios/RNNBottomTabsOptions.mm +2 -0
  39. package/ios/RNNComponentViewCreator.h +2 -1
  40. package/ios/RNNCustomTabBarItemView.h +26 -0
  41. package/ios/RNNCustomTabBarItemView.mm +83 -0
  42. package/ios/RNNReactRootViewCreator.mm +1 -0
  43. package/ios/RNNViewControllerFactory.mm +1 -0
  44. package/ios/ReactNativeNavigation.xcodeproj/project.pbxproj +24 -0
  45. package/lib/module/ARCHITECTURE.md +30 -0
  46. package/lib/module/Navigation.js +34 -1
  47. package/lib/module/Navigation.js.map +1 -1
  48. package/lib/module/NavigationDelegate.js +21 -0
  49. package/lib/module/NavigationDelegate.js.map +1 -1
  50. package/lib/module/adapters/AndroidCustomRowForwarder.js +75 -0
  51. package/lib/module/adapters/AndroidCustomRowForwarder.js.map +1 -0
  52. package/lib/module/commands/Commands.js +8 -0
  53. package/lib/module/commands/Commands.js.map +1 -1
  54. package/lib/module/index.js +1 -0
  55. package/lib/module/index.js.map +1 -1
  56. package/lib/module/interfaces/Options.js.map +1 -1
  57. package/lib/module/linking/DeferredLinkQueue.js +52 -0
  58. package/lib/module/linking/DeferredLinkQueue.js.map +1 -0
  59. package/lib/module/linking/DeferredLinkQueue.test.js +54 -0
  60. package/lib/module/linking/DeferredLinkQueue.test.js.map +1 -0
  61. package/lib/module/linking/LinkingHandler.js +139 -0
  62. package/lib/module/linking/LinkingHandler.js.map +1 -0
  63. package/lib/module/linking/LinkingHandler.test.js +384 -0
  64. package/lib/module/linking/LinkingHandler.test.js.map +1 -0
  65. package/lib/module/linking/ModalLayoutBuilder.js +56 -0
  66. package/lib/module/linking/ModalLayoutBuilder.js.map +1 -0
  67. package/lib/module/linking/ModalLayoutBuilder.test.js +154 -0
  68. package/lib/module/linking/ModalLayoutBuilder.test.js.map +1 -0
  69. package/lib/module/linking/RouteMatcher.js +104 -0
  70. package/lib/module/linking/RouteMatcher.js.map +1 -0
  71. package/lib/module/linking/RouteMatcher.test.js +164 -0
  72. package/lib/module/linking/RouteMatcher.test.js.map +1 -0
  73. package/lib/module/linking/URLParser.js +56 -0
  74. package/lib/module/linking/URLParser.js.map +1 -0
  75. package/lib/module/linking/URLParser.test.js +100 -0
  76. package/lib/module/linking/URLParser.test.js.map +1 -0
  77. package/lib/module/linking/types.js +4 -0
  78. package/lib/module/linking/types.js.map +1 -0
  79. package/lib/typescript/Navigation.d.ts +22 -0
  80. package/lib/typescript/Navigation.d.ts.map +1 -1
  81. package/lib/typescript/NavigationDelegate.d.ts +13 -0
  82. package/lib/typescript/NavigationDelegate.d.ts.map +1 -1
  83. package/lib/typescript/adapters/AndroidCustomRowForwarder.d.ts +23 -0
  84. package/lib/typescript/adapters/AndroidCustomRowForwarder.d.ts.map +1 -0
  85. package/lib/typescript/commands/Commands.d.ts +1 -0
  86. package/lib/typescript/commands/Commands.d.ts.map +1 -1
  87. package/lib/typescript/index.d.ts +1 -0
  88. package/lib/typescript/index.d.ts.map +1 -1
  89. package/lib/typescript/interfaces/Options.d.ts +90 -0
  90. package/lib/typescript/interfaces/Options.d.ts.map +1 -1
  91. package/lib/typescript/linking/DeferredLinkQueue.d.ts +26 -0
  92. package/lib/typescript/linking/DeferredLinkQueue.d.ts.map +1 -0
  93. package/lib/typescript/linking/DeferredLinkQueue.test.d.ts +2 -0
  94. package/lib/typescript/linking/DeferredLinkQueue.test.d.ts.map +1 -0
  95. package/lib/typescript/linking/LinkingHandler.d.ts +71 -0
  96. package/lib/typescript/linking/LinkingHandler.d.ts.map +1 -0
  97. package/lib/typescript/linking/LinkingHandler.test.d.ts +2 -0
  98. package/lib/typescript/linking/LinkingHandler.test.d.ts.map +1 -0
  99. package/lib/typescript/linking/ModalLayoutBuilder.d.ts +21 -0
  100. package/lib/typescript/linking/ModalLayoutBuilder.d.ts.map +1 -0
  101. package/lib/typescript/linking/ModalLayoutBuilder.test.d.ts +2 -0
  102. package/lib/typescript/linking/ModalLayoutBuilder.test.d.ts.map +1 -0
  103. package/lib/typescript/linking/RouteMatcher.d.ts +23 -0
  104. package/lib/typescript/linking/RouteMatcher.d.ts.map +1 -0
  105. package/lib/typescript/linking/RouteMatcher.test.d.ts +2 -0
  106. package/lib/typescript/linking/RouteMatcher.test.d.ts.map +1 -0
  107. package/lib/typescript/linking/URLParser.d.ts +16 -0
  108. package/lib/typescript/linking/URLParser.d.ts.map +1 -0
  109. package/lib/typescript/linking/URLParser.test.d.ts +2 -0
  110. package/lib/typescript/linking/URLParser.test.d.ts.map +1 -0
  111. package/lib/typescript/linking/types.d.ts +107 -0
  112. package/lib/typescript/linking/types.d.ts.map +1 -0
  113. package/package.json +1 -1
  114. package/src/ARCHITECTURE.md +30 -0
  115. package/src/Navigation.ts +36 -1
  116. package/src/NavigationDelegate.ts +22 -0
  117. package/src/adapters/AndroidCustomRowForwarder.ts +83 -0
  118. package/src/commands/Commands.ts +15 -0
  119. package/src/index.ts +1 -0
  120. package/src/interfaces/Options.ts +92 -0
  121. package/src/linking/DeferredLinkQueue.test.ts +60 -0
  122. package/src/linking/DeferredLinkQueue.ts +55 -0
  123. package/src/linking/LinkingHandler.test.ts +332 -0
  124. package/src/linking/LinkingHandler.ts +169 -0
  125. package/src/linking/ModalLayoutBuilder.test.ts +105 -0
  126. package/src/linking/ModalLayoutBuilder.ts +60 -0
  127. package/src/linking/RouteMatcher.test.ts +128 -0
  128. package/src/linking/RouteMatcher.ts +126 -0
  129. package/src/linking/URLParser.test.ts +105 -0
  130. package/src/linking/URLParser.ts +62 -0
  131. package/src/linking/types.ts +115 -0
@@ -0,0 +1,252 @@
1
+ #import "RNNBottomTabsCustomRow.h"
2
+
3
+ @interface RNNBottomTabsCustomRowCell : UIControl
4
+ @property(nonatomic, strong, nullable) RNNCustomTabBarItemView *itemView;
5
+ @property(nonatomic, assign) NSUInteger index;
6
+ @end
7
+
8
+ @implementation RNNBottomTabsCustomRowCell
9
+
10
+ - (void)setItemView:(RNNCustomTabBarItemView *)itemView {
11
+ if (_itemView == itemView) {
12
+ return;
13
+ }
14
+ [_itemView removeFromSuperview];
15
+ _itemView = itemView;
16
+ if (itemView) {
17
+ itemView.translatesAutoresizingMaskIntoConstraints = YES;
18
+ itemView.autoresizingMask =
19
+ UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
20
+ itemView.frame = self.bounds;
21
+ [self addSubview:itemView];
22
+ }
23
+ }
24
+
25
+ - (void)layoutSubviews {
26
+ [super layoutSubviews];
27
+ self.itemView.frame = self.bounds;
28
+ }
29
+
30
+ @end
31
+
32
+ @interface RNNBottomTabsCustomRow ()
33
+ @property(nonatomic, strong) NSMutableArray<RNNBottomTabsCustomRowCell *> *cells;
34
+ @property(nonatomic, strong) UIVisualEffectView *backgroundEffectView;
35
+ @property(nonatomic, strong) UIView *backgroundColorView;
36
+ @property(nonatomic, strong) RNNBottomTabsCustomRowOptions *options;
37
+ @end
38
+
39
+ @implementation RNNBottomTabsCustomRow
40
+
41
+ - (instancetype)initWithFrame:(CGRect)frame {
42
+ self = [super initWithFrame:frame];
43
+ if (self) {
44
+ _cells = [NSMutableArray new];
45
+ self.backgroundColor = UIColor.clearColor;
46
+
47
+ // Solid background layer (only made visible if user sets backgroundColor).
48
+ _backgroundColorView = [[UIView alloc] init];
49
+ _backgroundColorView.hidden = YES;
50
+ _backgroundColorView.clipsToBounds = YES;
51
+ if (@available(iOS 13.0, *)) {
52
+ _backgroundColorView.layer.cornerCurve = kCACornerCurveContinuous;
53
+ }
54
+ [self addSubview:_backgroundColorView];
55
+
56
+ // Visual effect (blur / glass) layer. Default depends on iOS version.
57
+ UIVisualEffect *effect = [RNNBottomTabsCustomRow defaultBackgroundEffect];
58
+ _backgroundEffectView = [[UIVisualEffectView alloc] initWithEffect:effect];
59
+ _backgroundEffectView.clipsToBounds = YES;
60
+ if (@available(iOS 13.0, *)) {
61
+ _backgroundEffectView.layer.cornerCurve = kCACornerCurveContinuous;
62
+ }
63
+ if (@available(iOS 26.0, *)) {
64
+ _backgroundEffectView.layer.cornerRadius = 28.0;
65
+ }
66
+ [self addSubview:_backgroundEffectView];
67
+ }
68
+ return self;
69
+ }
70
+
71
+ // `UIGlassEffect` is a new visual effect introduced in iOS 26. Reference it
72
+ // at runtime so this file still compiles against older SDKs and so we get a
73
+ // usable fallback on older OS versions.
74
+ + (UIVisualEffect *)defaultBackgroundEffect {
75
+ Class glassClass = NSClassFromString(@"UIGlassEffect");
76
+ if (glassClass) {
77
+ UIVisualEffect *glass = [[glassClass alloc] init];
78
+ if (glass) {
79
+ return glass;
80
+ }
81
+ }
82
+ if (@available(iOS 13.0, *)) {
83
+ return [UIBlurEffect effectWithStyle:UIBlurEffectStyleSystemChromeMaterial];
84
+ }
85
+ return [UIBlurEffect effectWithStyle:UIBlurEffectStyleLight];
86
+ }
87
+
88
+ + (UIVisualEffect *)blurBackgroundEffect {
89
+ if (@available(iOS 13.0, *)) {
90
+ return [UIBlurEffect effectWithStyle:UIBlurEffectStyleSystemChromeMaterial];
91
+ }
92
+ return [UIBlurEffect effectWithStyle:UIBlurEffectStyleLight];
93
+ }
94
+
95
+ - (void)applyOptions:(RNNBottomTabsCustomRowOptions *)options {
96
+ self.options = options;
97
+
98
+ UIColor *solidColor =
99
+ options.backgroundColor.hasValue ? options.backgroundColor.get : nil;
100
+ NSString *effectName =
101
+ options.backgroundEffect.hasValue ? options.backgroundEffect.get : nil;
102
+
103
+ BOOL useSolidColor = solidColor != nil;
104
+ self.backgroundColorView.hidden = !useSolidColor;
105
+ if (useSolidColor) {
106
+ self.backgroundColorView.backgroundColor = solidColor;
107
+ }
108
+
109
+ // Effect view stays unless explicitly disabled or overridden by solid color.
110
+ BOOL hideEffect = useSolidColor || [effectName isEqualToString:@"none"];
111
+ self.backgroundEffectView.hidden = hideEffect;
112
+ if (!hideEffect) {
113
+ if ([effectName isEqualToString:@"blur"]) {
114
+ self.backgroundEffectView.effect = [RNNBottomTabsCustomRow blurBackgroundEffect];
115
+ } else if ([effectName isEqualToString:@"glass"]) {
116
+ self.backgroundEffectView.effect = [RNNBottomTabsCustomRow defaultBackgroundEffect];
117
+ }
118
+ }
119
+
120
+ [self setNeedsLayout];
121
+ }
122
+
123
+ - (CGFloat)effectiveCornerRadius {
124
+ if (self.options.cornerRadius.hasValue) {
125
+ return [self.options.cornerRadius.get doubleValue];
126
+ }
127
+ if (@available(iOS 26.0, *)) {
128
+ return 28.0;
129
+ }
130
+ return 0.0;
131
+ }
132
+
133
+ - (CGFloat)effectiveHorizontalMargin {
134
+ if (self.options.horizontalMargin.hasValue) {
135
+ return [self.options.horizontalMargin.get doubleValue];
136
+ }
137
+ if (@available(iOS 26.0, *)) {
138
+ return 16.0;
139
+ }
140
+ return 0.0;
141
+ }
142
+
143
+ - (CGFloat)effectiveBottomMargin {
144
+ if (self.options.bottomMargin.hasValue) {
145
+ return [self.options.bottomMargin.get doubleValue];
146
+ }
147
+ return 0.0;
148
+ }
149
+
150
+ - (CGFloat)desiredRowHeightForNativeTabBarHeight:(CGFloat)nativeTabBarHeight
151
+ safeBottom:(CGFloat)safeBottom {
152
+ (void)safeBottom;
153
+ CGFloat contentHeight = nativeTabBarHeight;
154
+ if (@available(iOS 26.0, *)) {
155
+ contentHeight += 18.0; // default extra for iOS 26 floating bar look
156
+ }
157
+ if (self.options.height.hasValue) {
158
+ contentHeight = [self.options.height.get doubleValue];
159
+ }
160
+ // Safe area is applied by the controller when positioning the row frame.
161
+ return contentHeight + [self effectiveBottomMargin];
162
+ }
163
+
164
+ - (void)setItemViews:(NSArray<RNNCustomTabBarItemView *> *)itemViews {
165
+ for (RNNBottomTabsCustomRowCell *cell in self.cells) {
166
+ [cell removeFromSuperview];
167
+ }
168
+ [self.cells removeAllObjects];
169
+
170
+ UIView *cellContainer = self.backgroundEffectView.contentView;
171
+ for (NSUInteger i = 0; i < itemViews.count; i++) {
172
+ RNNBottomTabsCustomRowCell *cell = [[RNNBottomTabsCustomRowCell alloc] init];
173
+ cell.index = i;
174
+ cell.itemView = itemViews[i];
175
+ [cell addTarget:self
176
+ action:@selector(handleCellTap:)
177
+ forControlEvents:UIControlEventTouchUpInside];
178
+ [cellContainer addSubview:cell];
179
+ [self.cells addObject:cell];
180
+ }
181
+ [self setNeedsLayout];
182
+ }
183
+
184
+ - (void)setSelectedIndex:(NSUInteger)selectedIndex {
185
+ for (NSUInteger i = 0; i < self.cells.count; i++) {
186
+ RNNCustomTabBarItemView *itemView = self.cells[i].itemView;
187
+ [itemView setSelected:(i == selectedIndex)];
188
+ }
189
+ }
190
+
191
+ - (void)handleCellTap:(RNNBottomTabsCustomRowCell *)cell {
192
+ if (self.onTapAtIndex) {
193
+ self.onTapAtIndex(cell.index);
194
+ }
195
+ }
196
+
197
+ - (void)layoutSubviews {
198
+ [super layoutSubviews];
199
+
200
+ // Bottom safe area is already included in the row frame height via
201
+ // `desiredRowHeightForNativeTabBarHeight:safeBottom:` — do not inset it
202
+ // again here or the chrome shrinks by ~home-indicator height.
203
+ CGFloat bottomInset = [self effectiveBottomMargin];
204
+ CGRect content =
205
+ UIEdgeInsetsInsetRect(self.bounds, UIEdgeInsetsMake(0, 0, bottomInset, 0));
206
+
207
+ CGFloat horizontalMargin = [self effectiveHorizontalMargin];
208
+ if (horizontalMargin > 0) {
209
+ content = CGRectInset(content, horizontalMargin, 0);
210
+ }
211
+
212
+ CGFloat cornerRadius = [self effectiveCornerRadius];
213
+ self.backgroundEffectView.layer.cornerRadius = cornerRadius;
214
+ self.backgroundColorView.layer.cornerRadius = cornerRadius;
215
+
216
+ self.backgroundEffectView.frame = content;
217
+ self.backgroundColorView.frame = content;
218
+
219
+ if (self.cells.count == 0) {
220
+ return;
221
+ }
222
+
223
+ // Cells are subviews of the effect view's contentView (so the rounded
224
+ // mask clips them); their frames are relative to the effect view's
225
+ // bounds. The solid color layer sits behind and doesn't host cells.
226
+ UIView *cellContainer = self.backgroundColorView.hidden
227
+ ? self.backgroundEffectView.contentView
228
+ : self.backgroundColorView;
229
+ // Make sure cells live in the visible container.
230
+ if (self.cells.firstObject.superview != cellContainer) {
231
+ for (RNNBottomTabsCustomRowCell *cell in self.cells) {
232
+ [cell removeFromSuperview];
233
+ [cellContainer addSubview:cell];
234
+ }
235
+ }
236
+
237
+ CGFloat totalWidth = content.size.width;
238
+ CGFloat totalHeight = content.size.height;
239
+ CGFloat width = totalWidth / (CGFloat)self.cells.count;
240
+ for (NSUInteger i = 0; i < self.cells.count; i++) {
241
+ CGFloat x = floor((CGFloat)i * width);
242
+ CGFloat nextX = floor((CGFloat)(i + 1) * width);
243
+ self.cells[i].frame = CGRectMake(x, 0, nextX - x, totalHeight);
244
+ }
245
+ }
246
+
247
+ - (void)safeAreaInsetsDidChange {
248
+ [super safeAreaInsetsDidChange];
249
+ [self setNeedsLayout];
250
+ }
251
+
252
+ @end
@@ -0,0 +1,42 @@
1
+ #import "RNNOptions.h"
2
+
3
+ /**
4
+ * Visual options for the floating row that hosts custom-component bottom tab
5
+ * cells. Applies only when every tab declares `bottomTab.component`. All
6
+ * fields are optional; if omitted the row uses sensible defaults (iOS 26
7
+ * glass pill on iOS 26+, blur with no inset on older versions).
8
+ *
9
+ * The same option keys are exposed in JS as `bottomTabs.customRow` on both
10
+ * platforms. Android applies them via `RNNBottomTabsCustomRowModule`.
11
+ */
12
+ @interface RNNBottomTabsCustomRowOptions : RNNOptions
13
+
14
+ /**
15
+ * Override the row's content height. The native tab bar (and its safe-area
16
+ * inset) is preserved underneath — this only changes how tall the visible
17
+ * floating row appears. Defaults to the native tab bar content height (+18pt
18
+ * on iOS 26+ to match the larger native floating bar).
19
+ */
20
+ @property(nonatomic, strong) Number *height;
21
+
22
+ /** Solid background color for the row. When set, overrides `backgroundEffect`. */
23
+ @property(nonatomic, strong) Color *backgroundColor;
24
+
25
+ /**
26
+ * Visual effect for the row background. Values: `glass` | `blur` | `none`.
27
+ * Default: `glass` on iOS 26+, `blur` on older versions.
28
+ */
29
+ @property(nonatomic, strong) Text *backgroundEffect;
30
+
31
+ /** Corner radius of the row. Default: 28 on iOS 26+, 0 below. */
32
+ @property(nonatomic, strong) Number *cornerRadius;
33
+
34
+ /** Horizontal inset of the row from the screen edges. Default: 16 on iOS 26+, 0 below. */
35
+ @property(nonatomic, strong) Number *horizontalMargin;
36
+
37
+ /** Distance between the row's bottom edge and the safe-area bottom. Default: 0. */
38
+ @property(nonatomic, strong) Number *bottomMargin;
39
+
40
+ - (BOOL)hasValue;
41
+
42
+ @end
@@ -0,0 +1,37 @@
1
+ #import "RNNBottomTabsCustomRowOptions.h"
2
+
3
+ @implementation RNNBottomTabsCustomRowOptions
4
+
5
+ - (instancetype)initWithDict:(NSDictionary *)dict {
6
+ self = [super initWithDict:dict];
7
+ self.height = [NumberParser parse:dict key:@"height"];
8
+ self.backgroundColor = [ColorParser parse:dict key:@"backgroundColor"];
9
+ self.backgroundEffect = [TextParser parse:dict key:@"backgroundEffect"];
10
+ self.cornerRadius = [NumberParser parse:dict key:@"cornerRadius"];
11
+ self.horizontalMargin = [NumberParser parse:dict key:@"horizontalMargin"];
12
+ self.bottomMargin = [NumberParser parse:dict key:@"bottomMargin"];
13
+ return self;
14
+ }
15
+
16
+ - (void)mergeOptions:(RNNBottomTabsCustomRowOptions *)options {
17
+ if (options.height.hasValue)
18
+ self.height = options.height;
19
+ if (options.backgroundColor.hasValue)
20
+ self.backgroundColor = options.backgroundColor;
21
+ if (options.backgroundEffect.hasValue)
22
+ self.backgroundEffect = options.backgroundEffect;
23
+ if (options.cornerRadius.hasValue)
24
+ self.cornerRadius = options.cornerRadius;
25
+ if (options.horizontalMargin.hasValue)
26
+ self.horizontalMargin = options.horizontalMargin;
27
+ if (options.bottomMargin.hasValue)
28
+ self.bottomMargin = options.bottomMargin;
29
+ }
30
+
31
+ - (BOOL)hasValue {
32
+ return self.height.hasValue || self.backgroundColor.hasValue ||
33
+ self.backgroundEffect.hasValue || self.cornerRadius.hasValue ||
34
+ self.horizontalMargin.hasValue || self.bottomMargin.hasValue;
35
+ }
36
+
37
+ @end
@@ -1,4 +1,5 @@
1
1
  #import "BottomTabsAttachMode.h"
2
+ #import "RNNBottomTabsCustomRowOptions.h"
2
3
  #import "RNNOptions.h"
3
4
  #import "RNNShadowOptions.h"
4
5
 
@@ -22,6 +23,7 @@
22
23
  @property(nonatomic, strong) Color *borderColor;
23
24
  @property(nonatomic, strong) Number *borderWidth;
24
25
  @property(nonatomic, strong) RNNShadowOptions *shadow;
26
+ @property(nonatomic, strong) RNNBottomTabsCustomRowOptions *customRow;
25
27
  @property(nonatomic, strong) BottomTabsAttachMode *tabsAttachMode;
26
28
 
27
29
  - (BOOL)shouldDrawBehind;
@@ -26,6 +26,7 @@
26
26
  self.borderColor = [ColorParser parse:dict key:@"borderColor"];
27
27
  self.borderWidth = [NumberParser parse:dict key:@"borderWidth"];
28
28
  self.shadow = [[RNNShadowOptions alloc] initWithDict:dict[@"shadow"]];
29
+ self.customRow = [[RNNBottomTabsCustomRowOptions alloc] initWithDict:dict[@"customRow"]];
29
30
 
30
31
  return self;
31
32
  }
@@ -69,6 +70,7 @@
69
70
  self.borderWidth = options.borderWidth;
70
71
 
71
72
  [self.shadow mergeOptions:options.shadow];
73
+ [self.customRow mergeOptions:options.customRow];
72
74
  }
73
75
 
74
76
  - (BOOL)shouldDrawBehind {
@@ -9,7 +9,8 @@ typedef enum RNNComponentType {
9
9
  RNNComponentTypeComponent,
10
10
  RNNComponentTypeTopBarTitle,
11
11
  RNNComponentTypeTopBarButton,
12
- RNNComponentTypeTopBarBackground
12
+ RNNComponentTypeTopBarBackground,
13
+ RNNComponentTypeBottomTabItem
13
14
  } RNNComponentType;
14
15
 
15
16
  @protocol RNNComponentViewCreator
@@ -0,0 +1,26 @@
1
+ #import "RNNReactView.h"
2
+ #import <UIKit/UIKit.h>
3
+
4
+ /**
5
+ * Hosts an `RNNReactView` that renders a user-supplied React component as a
6
+ * bottom tab item. The view is laid out on top of the underlying
7
+ * `UITabBarButton` and forwards touches to it so native selection,
8
+ * accessibility focus and `selectTabOnPress: false` keep working.
9
+ *
10
+ * The hosted component receives `componentId`, `tabIndex`, `selected` and
11
+ * `badge` props. Selected state is updated via `setSelected:`.
12
+ */
13
+ @interface RNNCustomTabBarItemView : UIView
14
+
15
+ @property(nonatomic, readonly, strong) RNNReactView *reactView;
16
+
17
+ - (instancetype)initWithReactView:(RNNReactView *)reactView
18
+ tabIndex:(NSUInteger)tabIndex
19
+ selected:(BOOL)selected
20
+ badge:(NSString *)badge;
21
+
22
+ - (void)setSelected:(BOOL)selected;
23
+
24
+ - (void)setBadge:(NSString *)badge;
25
+
26
+ @end
@@ -0,0 +1,83 @@
1
+ #import "RNNCustomTabBarItemView.h"
2
+
3
+ @interface RNNCustomTabBarItemView ()
4
+
5
+ @property(nonatomic, readwrite, strong) RNNReactView *reactView;
6
+ @property(nonatomic, assign) NSUInteger tabIndex;
7
+ @property(nonatomic, assign) BOOL isSelected;
8
+ @property(nonatomic, copy) NSString *badge;
9
+
10
+ @end
11
+
12
+ @implementation RNNCustomTabBarItemView
13
+
14
+ - (instancetype)initWithReactView:(RNNReactView *)reactView
15
+ tabIndex:(NSUInteger)tabIndex
16
+ selected:(BOOL)selected
17
+ badge:(NSString *)badge {
18
+ self = [super initWithFrame:CGRectZero];
19
+ if (self) {
20
+ _reactView = reactView;
21
+ _tabIndex = tabIndex;
22
+ _isSelected = selected;
23
+ _badge = [badge copy];
24
+
25
+ self.backgroundColor = [UIColor clearColor];
26
+ self.userInteractionEnabled = NO;
27
+
28
+ _reactView.backgroundColor = [UIColor clearColor];
29
+ _reactView.userInteractionEnabled = NO;
30
+ [self addSubview:_reactView];
31
+
32
+ [self updateProps];
33
+ }
34
+ return self;
35
+ }
36
+
37
+ - (void)layoutSubviews {
38
+ [super layoutSubviews];
39
+ self.reactView.frame = self.bounds;
40
+ }
41
+
42
+ - (CGSize)sizeThatFits:(CGSize)size {
43
+ return size;
44
+ }
45
+
46
+ - (void)setSelected:(BOOL)selected {
47
+ if (self.isSelected == selected) {
48
+ return;
49
+ }
50
+ self.isSelected = selected;
51
+ [self updateProps];
52
+ }
53
+
54
+ - (void)setBadge:(NSString *)badge {
55
+ if (badge == _badge || [badge isEqualToString:_badge]) {
56
+ return;
57
+ }
58
+ _badge = [badge copy];
59
+ [self updateProps];
60
+ }
61
+
62
+ - (void)updateProps {
63
+ NSMutableDictionary *props = [NSMutableDictionary dictionary];
64
+ #ifdef RCT_NEW_ARCH_ENABLED
65
+ // RNNReactView's `properties` setter is a no-op on Fabric (it writes to
66
+ // the auto-synthesized ivar instead of the underlying surface). Update
67
+ // the React tree by writing directly to the surface so prop changes
68
+ // propagate to JS.
69
+ [props addEntriesFromDictionary:(self.reactView.surface.properties ?: @{})];
70
+ #else
71
+ [props addEntriesFromDictionary:(self.reactView.appProperties ?: @{})];
72
+ #endif
73
+ props[@"tabIndex"] = @(self.tabIndex);
74
+ props[@"selected"] = @(self.isSelected);
75
+ props[@"badge"] = self.badge ?: [NSNull null];
76
+ #ifdef RCT_NEW_ARCH_ENABLED
77
+ self.reactView.surface.properties = props;
78
+ #else
79
+ self.reactView.appProperties = props;
80
+ #endif
81
+ }
82
+
83
+ @end
@@ -71,6 +71,7 @@
71
71
  return RNNReactButtonView.class;
72
72
  case RNNComponentTypeTopBarBackground:
73
73
  return RNNReactBackgroundView.class;
74
+ case RNNComponentTypeBottomTabItem:
74
75
  case RNNComponentTypeComponent:
75
76
  default:
76
77
  return RNNComponentRootView.class;
@@ -234,6 +234,7 @@
234
234
  presenter:presenter
235
235
  bottomTabPresenter:bottomTabPresenter
236
236
  dotIndicatorPresenter:dotIndicatorPresenter
237
+ componentRegistry:_componentRegistry
237
238
  eventEmitter:_eventEmitter
238
239
  childViewControllers:childViewControllers
239
240
  bottomTabsAttacher:bottomTabsAttacher];
@@ -73,6 +73,12 @@
73
73
  5012242B217372B3000F5F98 /* ImageParser.mm in Sources */ = {isa = PBXBuildFile; fileRef = 50122429217372B3000F5F98 /* ImageParser.mm */; };
74
74
  5016E8EF20209690009D4F7C /* RNNCustomTitleView.h in Headers */ = {isa = PBXBuildFile; fileRef = 5016E8ED2020968F009D4F7C /* RNNCustomTitleView.h */; };
75
75
  5016E8F020209690009D4F7C /* RNNCustomTitleView.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5016E8EE2020968F009D4F7C /* RNNCustomTitleView.mm */; };
76
+ EE4A5C0001000000ABCD0001 /* RNNCustomTabBarItemView.h in Headers */ = {isa = PBXBuildFile; fileRef = EE4A5C0001000000ABCD0003 /* RNNCustomTabBarItemView.h */; };
77
+ EE4A5C0001000000ABCD0002 /* RNNCustomTabBarItemView.mm in Sources */ = {isa = PBXBuildFile; fileRef = EE4A5C0001000000ABCD0004 /* RNNCustomTabBarItemView.mm */; };
78
+ EE4A5C0001000000ABCD0011 /* RNNBottomTabsCustomRow.h in Headers */ = {isa = PBXBuildFile; fileRef = EE4A5C0001000000ABCD0013 /* RNNBottomTabsCustomRow.h */; };
79
+ EE4A5C0001000000ABCD0012 /* RNNBottomTabsCustomRow.mm in Sources */ = {isa = PBXBuildFile; fileRef = EE4A5C0001000000ABCD0014 /* RNNBottomTabsCustomRow.mm */; };
80
+ EE4A5C0001000000ABCD0021 /* RNNBottomTabsCustomRowOptions.h in Headers */ = {isa = PBXBuildFile; fileRef = EE4A5C0001000000ABCD0023 /* RNNBottomTabsCustomRowOptions.h */; };
81
+ EE4A5C0001000000ABCD0022 /* RNNBottomTabsCustomRowOptions.mm in Sources */ = {isa = PBXBuildFile; fileRef = EE4A5C0001000000ABCD0024 /* RNNBottomTabsCustomRowOptions.mm */; };
76
82
  50175CD1207A2AA1004FE91B /* RNNComponentOptions.h in Headers */ = {isa = PBXBuildFile; fileRef = 50175CCF207A2AA1004FE91B /* RNNComponentOptions.h */; };
77
83
  50175CD2207A2AA1004FE91B /* RNNComponentOptions.mm in Sources */ = {isa = PBXBuildFile; fileRef = 50175CD0207A2AA1004FE91B /* RNNComponentOptions.mm */; };
78
84
  5017D9E1239D2C6C00B74047 /* BottomTabsAttachModeFactory.h in Headers */ = {isa = PBXBuildFile; fileRef = 5017D9DF239D2C6C00B74047 /* BottomTabsAttachModeFactory.h */; };
@@ -568,6 +574,12 @@
568
574
  50122429217372B3000F5F98 /* ImageParser.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ImageParser.mm; sourceTree = "<group>"; };
569
575
  5016E8ED2020968F009D4F7C /* RNNCustomTitleView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNNCustomTitleView.h; sourceTree = "<group>"; };
570
576
  5016E8EE2020968F009D4F7C /* RNNCustomTitleView.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNNCustomTitleView.mm; sourceTree = "<group>"; };
577
+ EE4A5C0001000000ABCD0003 /* RNNCustomTabBarItemView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNNCustomTabBarItemView.h; sourceTree = "<group>"; };
578
+ EE4A5C0001000000ABCD0004 /* RNNCustomTabBarItemView.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNNCustomTabBarItemView.mm; sourceTree = "<group>"; };
579
+ EE4A5C0001000000ABCD0013 /* RNNBottomTabsCustomRow.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNNBottomTabsCustomRow.h; sourceTree = "<group>"; };
580
+ EE4A5C0001000000ABCD0014 /* RNNBottomTabsCustomRow.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNNBottomTabsCustomRow.mm; sourceTree = "<group>"; };
581
+ EE4A5C0001000000ABCD0023 /* RNNBottomTabsCustomRowOptions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNNBottomTabsCustomRowOptions.h; sourceTree = "<group>"; };
582
+ EE4A5C0001000000ABCD0024 /* RNNBottomTabsCustomRowOptions.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNNBottomTabsCustomRowOptions.mm; sourceTree = "<group>"; };
571
583
  50175CCF207A2AA1004FE91B /* RNNComponentOptions.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RNNComponentOptions.h; sourceTree = "<group>"; };
572
584
  50175CD0207A2AA1004FE91B /* RNNComponentOptions.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNNComponentOptions.mm; sourceTree = "<group>"; };
573
585
  5017D9DF239D2C6C00B74047 /* BottomTabsAttachModeFactory.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BottomTabsAttachModeFactory.h; sourceTree = "<group>"; };
@@ -1663,6 +1675,12 @@
1663
1675
  children = (
1664
1676
  5016E8ED2020968F009D4F7C /* RNNCustomTitleView.h */,
1665
1677
  5016E8EE2020968F009D4F7C /* RNNCustomTitleView.mm */,
1678
+ EE4A5C0001000000ABCD0003 /* RNNCustomTabBarItemView.h */,
1679
+ EE4A5C0001000000ABCD0004 /* RNNCustomTabBarItemView.mm */,
1680
+ EE4A5C0001000000ABCD0013 /* RNNBottomTabsCustomRow.h */,
1681
+ EE4A5C0001000000ABCD0014 /* RNNBottomTabsCustomRow.mm */,
1682
+ EE4A5C0001000000ABCD0023 /* RNNBottomTabsCustomRowOptions.h */,
1683
+ EE4A5C0001000000ABCD0024 /* RNNBottomTabsCustomRowOptions.mm */,
1666
1684
  E8A5CD601F49114F00E89D0D /* RNNElement.h */,
1667
1685
  E8A5CD611F49114F00E89D0D /* RNNElement.mm */,
1668
1686
  E8AEDB3A1F55A1C2000F5A6A /* RNNElementView.h */,
@@ -1737,6 +1755,9 @@
1737
1755
  E5F6C3A422DB4D0F0093C2CE /* UIColor+RNNUtils.h in Headers */,
1738
1756
  5038A3C1216E1E66009280BC /* RNNFontAttributesCreator.h in Headers */,
1739
1757
  5016E8EF20209690009D4F7C /* RNNCustomTitleView.h in Headers */,
1758
+ EE4A5C0001000000ABCD0001 /* RNNCustomTabBarItemView.h in Headers */,
1759
+ EE4A5C0001000000ABCD0011 /* RNNBottomTabsCustomRow.h in Headers */,
1760
+ EE4A5C0001000000ABCD0021 /* RNNBottomTabsCustomRowOptions.h in Headers */,
1740
1761
  500623A525B7003A0086AB39 /* RNNShadowOptions.h in Headers */,
1741
1762
  50415CBA20553B8E00BB682E /* RNNScreenTransition.h in Headers */,
1742
1763
  5039558B2174829400B0A663 /* IntNumberParser.h in Headers */,
@@ -2042,6 +2063,9 @@
2042
2063
  50CB3B6A1FDE911400AA153B /* RNNSideMenuOptions.mm in Sources */,
2043
2064
  503A8A0623BB850A0094D1C4 /* TimeInterval.mm in Sources */,
2044
2065
  5016E8F020209690009D4F7C /* RNNCustomTitleView.mm in Sources */,
2066
+ EE4A5C0001000000ABCD0002 /* RNNCustomTabBarItemView.mm in Sources */,
2067
+ EE4A5C0001000000ABCD0012 /* RNNBottomTabsCustomRow.mm in Sources */,
2068
+ EE4A5C0001000000ABCD0022 /* RNNBottomTabsCustomRowOptions.mm in Sources */,
2045
2069
  5061B6C823D48449008B9827 /* VerticalRotationTransition.mm in Sources */,
2046
2070
  5022EDB62405224B00852BA6 /* BottomTabPresenter.mm in Sources */,
2047
2071
  503955982174864E00B0A663 /* NullDouble.mm in Sources */,
@@ -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
@@ -21,6 +21,7 @@ import { Deprecations } from "./commands/Deprecations.js";
21
21
  import { LayoutProcessor } from "./processors/LayoutProcessor.js";
22
22
  import { LayoutProcessorsStore } from "./processors/LayoutProcessorsStore.js";
23
23
  import { OptionsCrawler } from "./commands/OptionsCrawler.js";
24
+ import { LinkingHandler } from "./linking/LinkingHandler.js";
24
25
  export class NavigationRoot {
25
26
  TouchablePreview = TouchablePreview;
26
27
  constructor(nativeCommandsSender, nativeEventsReceiver, appRegistryService) {
@@ -42,6 +43,7 @@ export class NavigationRoot {
42
43
  this.optionsCrawler = new OptionsCrawler(this.store, this.uniqueIdProvider);
43
44
  this.commands = new Commands(this.store, this.nativeCommandsSender, this.layoutTreeParser, this.layoutTreeCrawler, this.commandsObserver, this.uniqueIdProvider, optionsProcessor, layoutProcessor, this.optionsCrawler);
44
45
  this.eventsRegistry = new EventsRegistry(this.nativeEventsReceiver, this.commandsObserver, this.componentEventsObserver);
46
+ this.linkingHandler = new LinkingHandler(layout => this.commands.showModal(layout));
45
47
  this.componentEventsObserver.registerOnceForAllComponentEvents();
46
48
  }
47
49
 
@@ -84,7 +86,9 @@ export class NavigationRoot {
84
86
  * Reset the app to a new layout
85
87
  */
86
88
  setRoot(layout) {
87
- return this.commands.setRoot(layout);
89
+ const result = this.commands.setRoot(layout);
90
+ result.then(() => this.linkingHandler.setRootReady()).catch(() => {});
91
+ return result;
88
92
  }
89
93
 
90
94
  /**
@@ -193,6 +197,35 @@ export class NavigationRoot {
193
197
  return this.commands.getLaunchArgs();
194
198
  }
195
199
 
200
+ /**
201
+ * Configure deep link handling. Maps URL prefixes and path patterns to
202
+ * RNN components. By default each matched link is presented as a modal
203
+ * (wrapped in a stack so a topBar close button can be configured); supply
204
+ * `getModal` to customize the modal layout or `onLink` to bypass the
205
+ * default behavior entirely.
206
+ */
207
+ setLinking(config) {
208
+ this.linkingHandler.configure(config);
209
+ }
210
+
211
+ /**
212
+ * Manually run a URL through the deep link pipeline as if it had been
213
+ * delivered by the OS. Useful for URLs received from push notifications
214
+ * or other non-`Linking` sources.
215
+ */
216
+ handleDeepLink(url) {
217
+ this.linkingHandler.handleURL(url);
218
+ }
219
+
220
+ /**
221
+ * Signal that the app is ready to process deep links (e.g. after the
222
+ * user has authenticated). When set to `true`, any links that were
223
+ * queued while not ready are replayed in order.
224
+ */
225
+ setLinkingReady(ready) {
226
+ this.linkingHandler.setLinkingReady(ready);
227
+ }
228
+
196
229
  /**
197
230
  * Obtain the events registry instance
198
231
  */