Skip to content

Commit 2bdb750

Browse files
committed
Documentation update
1 parent e5c6976 commit 2bdb750

5 files changed

Lines changed: 102 additions & 4 deletions

File tree

.claude/skills/rnn-codebase/SKILL.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ Each controller type has a Presenter that applies options to views:
7777
| React view rendering || `ios/RNNReactView.mm` | `react/ReactView.java` |
7878
| Events to JS | `src/adapters/NativeEventsReceiver.ts` | `ios/RNNEventEmitter.mm` | `react/events/EventEmitter.java` |
7979
| Component registration | `src/components/ComponentRegistry.ts` |||
80+
| Deep linking (URL → screen) | `src/linking/` (`LinkingHandler`, `URLParser`, `RouteMatcher`, `DeferredLinkQueue`, `ModalLayoutBuilder`) | `ios/RNNAppDelegate.mm` (`dispatchDeepLinkURL:`, cold-start queue, `RCTContentDidAppearNotification`) | `NavigationActivity.onNewIntent``ReactGateway` |
8081

8182
### By directory
8283

@@ -149,3 +150,5 @@ API layout → OptionsCrawler.crawl() → LayoutProcessor.process()
149150
- Options that exist in JS types may not be implemented on both platforms — check the presenter
150151
- `passProps` are stored in JS `Store`, not sent to native (cleared before bridge crossing)
151152
- The `lib/` folder is generated — never edit it, edit `src/` instead
153+
- Deep links are processed only after the first `setRoot()` resolves; pre-bridge URLs on iOS are queued natively in `RNNAppDelegate` and flushed on `RCTContentDidAppearNotification` (bridgeless mode — `RCTJavaScriptDidLoadNotification` does NOT fire)
154+
- `ModalLayoutBuilder` strips React-reserved keys (`ref`, `key`) from URL query params before they reach `passProps`, to avoid React 19 ref-validation crashes

ARCHITECTURE.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ Core commands available through the Navigation API:
7575
- **Modal**: `showModal()`, `dismissModal()`, `dismissAllModals()`
7676
- **Overlay**: `showOverlay()`, `dismissOverlay()`, `dismissAllOverlays()`
7777
- **Options**: `setDefaultOptions()`, `mergeOptions()`, `updateProps()`
78+
- **Linking**: `setLinking()`, `handleDeepLink()`, `setLinkingReady()` - URL-to-screen routing
79+
80+
### Deep Linking
81+
URL-driven navigation is implemented entirely in the JS layer (`src/linking/`) and feeds into the standard command pipeline. Configure with `Navigation.setLinking({ prefixes, config: { screens } })`; matched URLs are presented as modals by default (preserving the user's current navigation state). Customize via `getModal`/`onLink` hooks; gate processing on auth via `isReady`/`setLinkingReady`. Native plumbing — `application:openURL:`, `application:continueUserActivity:`, cold-start URL queueing — lives in `RNNAppDelegate` (iOS) and `NavigationActivity.onNewIntent` (Android), so subclassing the base classes is sufficient. See [Deep Linking docs](https://wix.github.io/react-native-navigation/docs/deep-linking).
7882

7983
### Options System
8084
Styling and behavior is controlled via a hierarchical options object:

ios/ARCHITECTURE.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ Base class that user's AppDelegate must extend. Handles React Native and navigat
2828
- Creates `RCTRootViewFactory` and `ReactHost`
2929
- Calls `[ReactNativeNavigation bootstrapWithHost:]` to initialize navigation
3030
- Handles RN version differences (0.77, 0.78, 0.79+) via compile-time macros
31+
- **Deep linking plumbing**:
32+
- Implements `application:openURL:options:` and `application:continueUserActivity:restorationHandler:`; both call `-dispatchDeepLinkURL:`.
33+
- `-dispatchDeepLinkURL:` posts `RCTOpenURLNotification` directly if the React runtime is ready, otherwise enqueues the URL.
34+
- Observes `RCTContentDidAppearNotification` (the Fabric/bridgeless signal) to flush the queue. This solves the cold-start race where URLs (push notifications, OS link launches) arrive before `RCTLinkingManager` is listening.
35+
- Subclasses can call `-dispatchDeepLinkURL:` manually from notification delegates or any other URL source; the queueing behavior is reused automatically.
3136
3237
### ReactNativeNavigation Bootstrap
3338
**File**: `ReactNativeNavigation.h/mm`

src/ARCHITECTURE.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ src/
1818
├── components/ # React component management
1919
├── events/ # Event system
2020
├── interfaces/ # TypeScript type definitions
21+
├── linking/ # Deep-linking framework (URL → command)
2122
└── processors/ # Extensibility hooks
2223
```
2324

@@ -207,6 +208,35 @@ Notifies listeners of command lifecycle (start, complete).
207208
| `RNN.PreviewCompleted` | 3D Touch preview completed |
208209
| `RNN.CommandCompleted` | Navigation command finished |
209210

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+
210240
## Processors Layer
211241

212242
### OptionProcessorsStore

website/docs/docs/docs-deep-linking.mdx

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,15 @@ myapp://search?q=hello&page=2
110110
→ Search screen receives passProps: { q: 'hello', page: '2' }
111111
```
112112

113+
#### Reserved keys
114+
115+
The query/path keys `ref` and `key` are **not** forwarded as `passProps`. React reserves these names (`ref` is consumed by React itself; passing a string `ref` to a component crashes under React 19's stricter validation). RNN silently drops them and logs a dev-mode warning. Rename any conflicting URL parameters before they reach your links:
116+
117+
```
118+
myapp://user/42?ref=push → passProps: { id: '42' } (ref dropped + warning)
119+
myapp://user/42?source=push → passProps: { id: '42', source: 'push' }
120+
```
121+
113122
## Customizing presentation
114123

115124
By default, every matched link becomes a modal containing a stack with the matched chain pushed in order. If you need to customize either the layout or the navigation command itself, the config exposes two hooks.
@@ -240,11 +249,13 @@ async function onLoginSuccess() {
240249
Some links don't arrive via the system URL handler — push-notification payloads, branch.io tokens, App Clips. Feed them into the same pipeline with `Navigation.handleDeepLink(url)`:
241250

242251
```js
243-
Navigation.handleDeepLink('myapp://user/42?ref=notification');
252+
Navigation.handleDeepLink('myapp://user/42?source=notification');
244253
```
245254

246255
This runs the URL through parse → match → present exactly like a system-delivered link, including readiness gates.
247256

257+
If you'd rather hand the URL off natively (e.g. from a `UNUserNotificationCenterDelegate` you already maintain), call `[self dispatchDeepLinkURL:url]` inside your `RNNAppDelegate` subclass — same behavior, with cold-start queueing built in. See [Native setup → iOS → Notification taps](#ios) for an example.
258+
248259
## Complete example
249260

250261
```js
@@ -295,11 +306,11 @@ With this configuration:
295306

296307
## Native setup
297308

298-
The framework consumes URLs from React Native's `Linking` API. You still configure the URL schemes and universal links at the platform level.
309+
The framework consumes URLs from React Native's `Linking` API. You still configure the URL schemes and universal links at the platform level — but, unlike most React Native apps, you don't need to hand-write `application:openURL:` or `application:continueUserActivity:` glue. As long as your `AppDelegate` subclasses `RNNAppDelegate`, custom-scheme openings and universal links are forwarded to JS automatically, including cold-start URLs that arrive before the React runtime is ready.
299310

300311
### iOS
301312

302-
Add your URL scheme to `Info.plist`:
313+
**1. Register your URL scheme in `Info.plist`:**
303314

304315
```xml
305316
<key>CFBundleURLTypes</key>
@@ -313,7 +324,50 @@ Add your URL scheme to `Info.plist`:
313324
</array>
314325
```
315326

316-
For universal links, add the Associated Domains entitlement and host an `apple-app-site-association` file. See [Apple's documentation](https://developer.apple.com/documentation/xcode/supporting-associated-domains).
327+
**2. (Optional) Universal links.** Add the Associated Domains entitlement and host an `apple-app-site-association` file — see [Apple's docs](https://developer.apple.com/documentation/xcode/supporting-associated-domains). `RNNAppDelegate` already implements `application:continueUserActivity:restorationHandler:` and forwards browser-activity URLs through the same pipeline.
328+
329+
**3. (Optional) Notification taps.** Notifications aren't routed automatically because most apps already own `UNUserNotificationCenter.current.delegate` via Firebase / OneSignal / etc. From your existing notification handler, hand the URL off to RNN:
330+
331+
```objc
332+
#import "AppDelegate.h" // your subclass of RNNAppDelegate
333+
#import <UserNotifications/UserNotifications.h>
334+
335+
@interface AppDelegate () <UNUserNotificationCenterDelegate>
336+
@end
337+
338+
@implementation AppDelegate
339+
340+
- (BOOL)application:(UIApplication *)application
341+
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
342+
[super application:application didFinishLaunchingWithOptions:launchOptions];
343+
[UNUserNotificationCenter currentNotificationCenter].delegate = self;
344+
return YES;
345+
}
346+
347+
- (void)userNotificationCenter:(UNUserNotificationCenter *)center
348+
willPresentNotification:(UNNotification *)notification
349+
withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler {
350+
completionHandler(UNNotificationPresentationOptionBanner |
351+
UNNotificationPresentationOptionList |
352+
UNNotificationPresentationOptionSound);
353+
}
354+
355+
- (void)userNotificationCenter:(UNUserNotificationCenter *)center
356+
didReceiveNotificationResponse:(UNNotificationResponse *)response
357+
withCompletionHandler:(void (^)(void))completionHandler {
358+
NSString *urlString = response.notification.request.content.userInfo[@"url"];
359+
if ([urlString isKindOfClass:[NSString class]]) {
360+
[self dispatchDeepLinkURL:[NSURL URLWithString:urlString]];
361+
}
362+
completionHandler();
363+
}
364+
365+
@end
366+
```
367+
368+
`dispatchDeepLinkURL:` is inherited from `RNNAppDelegate`. It safely handles cold-start (URLs arriving before the React runtime is ready are queued and flushed automatically once content first renders) so you can call it from anywhere — push handlers, deferred-deep-link SDKs, branch.io callbacks, etc.
369+
370+
The JS equivalent is `Navigation.handleDeepLink(url)`; use whichever fits the layer you're working at.
317371
318372
### Android
319373
@@ -334,6 +388,8 @@ Add an intent filter to your `MainActivity` in `AndroidManifest.xml`:
334388
335389
For App Links, add `android:autoVerify="true"` and host an `assetlinks.json`. See [Android's documentation](https://developer.android.com/training/app-links).
336390
391+
`NavigationActivity` already forwards both cold-start (`getIntent()`) and warm (`onNewIntent`) URLs to React Native's `Linking` module — no extra Java/Kotlin needed. Notification taps that carry a deep-link URI (via `PendingIntent` with `ACTION_VIEW`) flow through the same `onNewIntent` path automatically.
392+
337393
## API reference
338394
339395
### `Navigation.setLinking(config)`

0 commit comments

Comments
 (0)