@dashx/react-native 1.1.4 → 1.1.5

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.
@@ -0,0 +1,216 @@
1
+ ---
2
+ name: Deep Linking Implementation Plan
3
+ overview: Implement MoEngage-like deep linking features in dashx-ios and dashx-android SDKs, covering notification-driven deep links, universal/app links, rich landing pages, structured navigation actions, and host-app callbacks.
4
+ todos:
5
+ - id: phase-1-payload
6
+ content: "Phase 1: Extend payload models on both platforms (ActionButton URLs, screenName, screenData, NavigationAction type)"
7
+ status: pending
8
+ - id: phase-2-callbacks
9
+ content: "Phase 2: Notification click callback API -- DashXAppDelegate override (iOS) and DashXNotificationListener interface (Android)"
10
+ status: pending
11
+ - id: phase-3-action-buttons
12
+ content: "Phase 3: Per-action-button deep links on iOS (UNNotificationAction) and Android (NotificationCompat.Action)"
13
+ status: pending
14
+ - id: phase-4-rich-landing
15
+ content: "Phase 4: Rich Landing -- SFSafariViewController (iOS) and Chrome Custom Tabs (Android)"
16
+ status: completed
17
+ - id: phase-5-scene-delegate
18
+ content: "Phase 5: SceneDelegate deep link support -- openURLContexts and continue userActivity (iOS only)"
19
+ status: completed
20
+ - id: phase-6-analytics
21
+ content: "Phase 6: Deep link source tracking -- processURL / processDeepLink analytics events"
22
+ status: completed
23
+ - id: phase-7-docs
24
+ content: "Phase 7: Documentation and demo app updates for all deep linking features"
25
+ status: completed
26
+ isProject: false
27
+ ---
28
+
29
+ # Deep Linking Implementation Plan
30
+
31
+ ## Current State vs MoEngage: Feature Comparison
32
+
33
+ ### What We Have Today
34
+
35
+ **dashx-ios** ([DashXAppDelegate.swift](dashx-ios/Sources/DashXFirebase/DashXAppDelegate.swift), [DashXClient.swift](dashx-ios/Sources/DashX/DashXClient.swift), [DashXData.swift](dashx-ios/Sources/DashX/DashXData.swift)):
36
+ - Notification `url` field in `DashXNotificationData` -- opens via `handleLink(url:)` callback
37
+ - Universal link forwarding via `DashXClient.handleUserActivity()` + `linkHandler` closure (AppDelegate only)
38
+ - `notificationClicked(message:, actionIdentifier:)` open method for subclass overrides
39
+ - `notificationDeliveredInForeground(message:)` callback
40
+ - Message tracking: delivered / clicked / dismissed
41
+ - `DashXSceneDelegate` exists but only handles ad tracking -- no deep link or URL handling
42
+
43
+ **dashx-android** ([NotificationProcessor.kt](dashx-android/dashx/src/main/java/com/dashx/sdk/NotificationProcessor.kt), [DashXFirebaseMessagingService.kt](dashx-android/dashx/src/main/java/com/dashx/sdk/DashXFirebaseMessagingService.kt)):
44
+ - Notification `url` field in `DashXPayload` -- opens via system `ACTION_VIEW` intent
45
+ - `click_action` field -- resolves as Activity class name or Intent action string
46
+ - `NotificationReceiver` (translucent Activity) delegates to `NotificationProcessor.handleClick()`
47
+ - Message tracking: DELIVERED / OPENED / DISMISSED
48
+ - No callback mechanism for the host app to intercept deep link navigation
49
+
50
+ ### What MoEngage Has That We Don't
51
+
52
+ | Feature | MoEngage | DashX iOS | DashX Android |
53
+ |---------|----------|-----------|---------------|
54
+ | Custom URL scheme handling | Auto via swizzle or `processURL()` | Not handled | N/A (system intent) |
55
+ | SceneDelegate deep link support | `scene(_:openURLContexts:)` + `scene(_:continue:)` | Missing | N/A |
56
+ | Rich Landing (in-app browser) | SFSafariViewController / Chrome Custom Tabs | Missing | Missing |
57
+ | Navigate to Screen (structured) | `screenName` + `screenData` in payload | Missing | Missing |
58
+ | Per-action-button deep links | Each button has its own URL/action | Buttons have no URL | No action buttons at all |
59
+ | Notification click callback with control | `onNotificationClicked()` returns bool to control redirection | Partial (subclass override, no control flag) | Missing entirely |
60
+ | Notification received callback | `onNotificationReceived()` | Partial (foreground only) | Missing |
61
+ | Notification cleared callback | `onNotificationCleared()` | Has `dismissed` tracking | Has `DISMISSED` tracking |
62
+ | Deep link URL tracking/analytics | `processURL()` tracks source attribution | Missing | Missing |
63
+ | Customizable notification builder | `customizeNotification()` / `customizeNotificationBuilder()` | Not exposed | Not exposed |
64
+ | Self-handled notification | `onSelfHandledNotificationReceived()` | N/A | Missing |
65
+ | AppDelegate proxy/swizzling | Auto-intercepts deep link AppDelegate methods | Not implemented | N/A |
66
+
67
+ ---
68
+
69
+ ## Implementation Plan
70
+
71
+ ### Phase 1: Enhanced Payload Models (Both Platforms)
72
+
73
+ Extend the notification payload models to support structured navigation.
74
+
75
+ **iOS** -- Update `ActionButton` and `DashXNotificationData` in [DashXData.swift](dashx-ios/Sources/DashX/DashXData.swift):
76
+ - Add `url: String?` and `clickAction: String?` to `ActionButton`
77
+ - Add `screenName: String?` and `screenData: [String: String]?` to `DashXNotificationData`
78
+ - Add `clickAction: String?` (currently iOS has no `click_action` at all)
79
+
80
+ **Android** -- Update `DashXPayload` in [DashXFirebaseMessagingService.kt](dashx-android/dashx/src/main/java/com/dashx/sdk/DashXFirebaseMessagingService.kt):
81
+ - Add `screenName: String?` (`screen_name`) and `screenData: Map<String, String>?` (`screen_data`) to `DashXPayload`
82
+ - Create an `ActionButton` model (currently Android has no action button support)
83
+ - Add `actionButtons: List<ActionButton>?` (`action_buttons`) to `DashXPayload`
84
+
85
+ Create a shared `NavigationAction` model on both platforms:
86
+
87
+ ```kotlin
88
+ // Android
89
+ sealed class NavigationAction {
90
+ data class DeepLink(val url: String) : NavigationAction()
91
+ data class Screen(val name: String, val data: Map<String, String>?) : NavigationAction()
92
+ data class RichLanding(val url: String) : NavigationAction()
93
+ }
94
+ ```
95
+
96
+ ```swift
97
+ // iOS
98
+ public enum NavigationAction {
99
+ case deepLink(url: URL)
100
+ case screen(name: String, data: [String: String]?)
101
+ case richLanding(url: URL)
102
+ }
103
+ ```
104
+
105
+ ### Phase 2: Notification Click Callback API (Both Platforms)
106
+
107
+ Provide a callback mechanism that gives the host app full control over deep link handling.
108
+
109
+ **iOS** -- In [DashXAppDelegate.swift](dashx-ios/Sources/DashXFirebase/DashXAppDelegate.swift):
110
+ - Add `open func onNotificationClicked(message:, action: NavigationAction?, actionIdentifier: String) -> Bool` -- return `true` to suppress SDK default handling
111
+ - Refactor `userNotificationCenter(_:didReceive:)` to resolve `NavigationAction` from the payload, call the callback, and only proceed with default handling if it returns `false`
112
+ - Deprecate the existing `notificationClicked(message:actionIdentifier:)` (keep for backward compat)
113
+
114
+ **Android** -- Create a new `DashXNotificationListener` interface in a new file `DashXNotificationListener.kt`:
115
+ ```kotlin
116
+ interface DashXNotificationListener {
117
+ fun onNotificationReceived(payload: DashXPayload) {}
118
+ fun onNotificationClicked(payload: DashXPayload, action: NavigationAction?): Boolean = false
119
+ fun onNotificationDismissed(payload: DashXPayload) {}
120
+ }
121
+ ```
122
+ - Add `DashX.registerNotificationListener(listener)` and `DashX.unregisterNotificationListener()`
123
+ - Wire into `NotificationProcessor.handleClick()` and `DashXFirebaseMessagingService.onMessageReceived()`
124
+ - If `onNotificationClicked` returns `true`, skip the SDK's default URL/click_action handling
125
+
126
+ ### Phase 3: Action Button Deep Links
127
+
128
+ **iOS** -- In [DashXAppDelegate.swift](dashx-ios/Sources/DashXFirebase/DashXAppDelegate.swift):
129
+ - When building `UNNotificationAction` items from `actionButtons`, store the per-button URL/action in `userInfo` (via `UNNotificationCategory`)
130
+ - In `didReceive response`, match `response.actionIdentifier` to the button's identifier and resolve its `NavigationAction`
131
+
132
+ **Android**:
133
+ - Parse `action_buttons` from `DashXPayload` and build `NotificationCompat.Action` items
134
+ - Attach per-button `PendingIntent` with the button's URL or action
135
+ - Handle button clicks in `NotificationProcessor` by resolving the specific button's `NavigationAction`
136
+
137
+ ### Phase 4: Rich Landing (In-App Browser)
138
+
139
+ When a notification's action is `richLanding` (or a URL is explicitly marked for in-app display):
140
+
141
+ **iOS**:
142
+ - Add a helper in `DashXClient` or a new `DashXBrowser.swift` that presents `SFSafariViewController` for a given URL
143
+ - Wire it as the default handler when `NavigationAction.richLanding` is resolved and the callback doesn't suppress it
144
+
145
+ **Android**:
146
+ - Add `implementation 'androidx.browser:browser:1.x.x'` as a dependency
147
+ - Create `DashXBrowser.kt` that opens `CustomTabsIntent` for the URL
148
+ - Wire as default handler for `NavigationAction.RichLanding`
149
+
150
+ ### Phase 5: SceneDelegate Deep Link Support (iOS Only)
151
+
152
+ Update [DashXSceneDelegate.swift](dashx-ios/Sources/DashX/DashXSceneDelegate.swift) to handle both custom URL schemes and universal links:
153
+
154
+ ```swift
155
+ @available(iOS 13.0, *)
156
+ open func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
157
+ guard let url = URLContexts.first?.url else { return }
158
+ DashXClient.instance.linkHandler?(url)
159
+ }
160
+
161
+ @available(iOS 13.0, *)
162
+ open func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
163
+ DashXClient.instance.handleUserActivity(userActivity: userActivity)
164
+ }
165
+ ```
166
+
167
+ ### Phase 6: Deep Link Source Tracking / Analytics
168
+
169
+ Add a `trackDeepLink(url:, source:)` method on both platforms to record deep link opens as analytics events.
170
+
171
+ **iOS** -- In [DashXClient.swift](dashx-ios/Sources/DashX/DashXClient.swift):
172
+ - Add `public func processURL(_ url: URL, source: String? = nil)` that:
173
+ 1. Fires a `dx_deep_link_opened` event with URL, source, timestamp
174
+ 2. Invokes `linkHandler?(url)`
175
+ - Update `handleUserActivity` and the notification click path to go through `processURL`
176
+
177
+ **Android** -- In `DashX.kt`:
178
+ - Add `fun processDeepLink(uri: Uri, source: String? = null)` with equivalent behavior
179
+ - Update `NotificationProcessor.handleClick()` to call `processDeepLink` before opening the URL
180
+
181
+ ### Phase 7: Documentation and Demo App Updates
182
+
183
+ - Update [dashx-ios/README.md](dashx-ios/README.md) and [dashx-android/README.md](dashx-android/README.md) with deep linking integration guides
184
+ - Update [dashx-demo-rn](dashx-demo-rn/), [dashx-demo-ios](dashx-demo-ios/), and [dashx-demo-android](dashx-demo-android/) to demonstrate:
185
+ - Handling deep links from push notifications
186
+ - Custom URL scheme setup
187
+ - Universal Links / App Links configuration
188
+ - Rich landing page behavior
189
+ - Navigate-to-screen with custom data
190
+
191
+ ---
192
+
193
+ ## Architecture: Notification Click Flow (After Implementation)
194
+
195
+ ```mermaid
196
+ flowchart TD
197
+ NotifTap[User Taps Notification] --> ParsePayload[Parse DashX Payload]
198
+ ParsePayload --> ResolveAction[Resolve NavigationAction]
199
+ ResolveAction --> TrackClick[trackMessage: CLICKED/OPENED]
200
+ TrackClick --> CallbackCheck{Host App Callback}
201
+ CallbackCheck -->|returns true| Done[Host App Handles]
202
+ CallbackCheck -->|returns false| ActionType{Action Type?}
203
+ ActionType -->|DeepLink| OpenURL[Open URL via system / linkHandler]
204
+ ActionType -->|RichLanding| InAppBrowser[SFSafari / CustomTabs]
205
+ ActionType -->|Screen| ScreenNav[Invoke screenHandler callback]
206
+ ActionType -->|None| FallbackClick[Call legacy notificationClicked]
207
+ ```
208
+
209
+ ---
210
+
211
+ ## Key Design Decisions
212
+
213
+ - **Callback-first, not swizzle**: Unlike MoEngage's AppDelegate proxy approach, we keep our subclass/callback pattern. Swizzling is fragile and conflicts with other SDKs. The host app subclasses `DashXAppDelegate`/`DashXSceneDelegate` (iOS) or registers a `DashXNotificationListener` (Android).
214
+ - **NavigationAction as a first-class type**: Rather than passing raw strings, provide a sealed/enum type so host apps can pattern-match on the navigation intent.
215
+ - **Backward compatible**: The existing `handleLink(url:)`, `notificationClicked(message:actionIdentifier:)` (iOS), and `click_action`/`url` behavior (Android) continue to work. New APIs are additive.
216
+ - **Rich landing is opt-in**: URLs default to system handling (external browser / `ACTION_VIEW`). Rich landing requires explicit `rich_landing` flag in the payload or a configuration option.
package/.gitignore CHANGED
@@ -42,5 +42,5 @@ buck-out/
42
42
  \.buckd/
43
43
  *.keystore
44
44
 
45
- # VS Code
46
- .vscode/
45
+ # Cursor plans
46
+ .cursor
@@ -42,8 +42,8 @@ Pod::Spec.new do |s|
42
42
 
43
43
  s.dependency "React-RCTAppDelegate"
44
44
  # DashX iOS SDK — consumers must provide the source in their Podfile:
45
- # pod 'DashX', :git => 'https://github.com/dashxhq/dashx-ios.git', :tag => '1.1.5'
45
+ # pod 'DashX', :git => 'https://github.com/dashxhq/dashx-ios.git', :tag => '1.1.9'
46
46
  # For local development:
47
47
  # pod 'DashX', :path => '../dashx-ios'
48
- s.dependency "DashX", "~> 1.1.5"
48
+ s.dependency "DashX", "~> 1.1.9"
49
49
  end
package/README.md CHANGED
@@ -20,23 +20,16 @@
20
20
 
21
21
  [![npm version](https://img.shields.io/npm/v/@dashx/react-native.svg)](https://www.npmjs.com/package/@dashx/react-native)
22
22
  [![license](https://img.shields.io/npm/l/@dashx/react-native.svg)](https://github.com/dashxhq/dashx-react-native/blob/main/LICENSE)
23
- [![platforms](https://img.shields.io/badge/platforms-Android%20%7C%20iOS-brightgreen.svg)](#requirements)
23
+ [![platforms](https://img.shields.io/badge/platforms-Android%20%7C%20iOS-brightgreen.svg)](#install)
24
24
 
25
25
  _DashX SDK for React Native_
26
26
 
27
- ## Requirements
27
+ ## Install
28
28
 
29
- | Platform | Minimum Version |
30
- |----------|----------------|
31
- | iOS | 13.0+ |
32
- | Android | SDK 26 (Android 8.0)+ |
33
- | React Native | 0.71+ |
34
- | Node.js | 16.0+ |
29
+ Requires **React Native 0.71+**, **iOS 13.0+**, **Android SDK 26+**, and **Node.js 16+**.
35
30
 
36
31
  > **Note:** This SDK uses native modules and is **not compatible with Expo Go**. Use a [development build](https://docs.expo.dev/develop/development-builds/introduction/) if you are using Expo.
37
32
 
38
- ## Install
39
-
40
33
  ```sh
41
34
  npm install @dashx/react-native
42
35
  ```
@@ -47,480 +40,17 @@ or
47
40
  yarn add @dashx/react-native
48
41
  ```
49
42
 
50
- ### Setup for Android
51
-
52
- DashX requires Google Services for Firebase Cloud Messaging:
53
-
54
- 1. Add the `google-services` plugin in your `/android/build.gradle`:
55
-
56
- ```gradle
57
- buildscript {
58
- dependencies {
59
- // ... other dependencies
60
- classpath 'com.google.gms:google-services:4.4.2'
61
- }
62
- }
63
- ```
64
-
65
- 2. Apply the plugin in your `/android/app/build.gradle`:
66
-
67
- ```gradle
68
- apply plugin: 'com.google.gms.google-services'
69
- ```
70
-
71
- 3. Add your Android app on [Firebase Console](https://console.firebase.google.com/): **Project Overview > Add App > Android**
72
-
73
- 4. Download `google-services.json` and place it at `/android/app/google-services.json`
74
-
75
- ### Setup for iOS
76
-
77
- #### Step 1: Add pods to your Podfile
78
-
79
- The DashX iOS SDK is not published to CocoaPods trunk. Add it directly from GitHub along with the Firebase pods required for FCM push notifications.
80
-
81
- Add `use_modular_headers!` at the top of your `ios/Podfile`, then add the pods inside the target block:
82
-
83
- ```ruby
84
- use_modular_headers!
85
-
86
- target 'YourApp' do
87
- # ... other pods
88
-
89
- pod 'DashX', :git => 'https://github.com/dashxhq/dashx-ios.git', :tag => '1.1.4'
90
- pod 'FirebaseCore'
91
- pod 'FirebaseMessaging'
92
-
93
- use_react_native!(...)
94
- end
95
- ```
96
-
97
- Then run:
98
-
99
- ```sh
100
- cd ios && pod install
101
- ```
102
-
103
- #### Step 2: Add GoogleService-Info.plist
104
-
105
- 1. Add your iOS app on [Firebase Console](https://console.firebase.google.com/): **Project Overview > Add App > iOS**
106
- 2. Download `GoogleService-Info.plist` and add it to your Xcode project (right-click project root > **Add Files to "YourApp"**, ensure **Copy items if needed** is checked)
107
-
108
- #### Step 3: Enable Push Notifications capability
109
-
110
- In Xcode, select your app target > **Signing & Capabilities** > **+ Capability** > add **Push Notifications**.
111
-
112
- #### Step 4: Configure your AppDelegate
113
-
114
- The SDK ships `DashXRCTAppDelegate` which handles permission requests, notification display, and universal link forwarding. Subclass it in your `AppDelegate.swift` and configure Firebase directly:
115
-
116
- ```swift
117
- import DashXReactNative
118
- import ReactAppDependencyProvider
119
- import FirebaseCore
120
- import FirebaseMessaging
121
- import DashX
122
- import UserNotifications
123
-
124
- @main
125
- class AppDelegate: DashXRCTAppDelegate {
126
- override var moduleName: String { "YourAppName" }
127
-
128
- override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
129
- self.dependencyProvider = RCTAppDependencyProvider()
130
- FirebaseApp.configure()
131
- Messaging.messaging().delegate = self
132
- return super.application(application, didFinishLaunchingWithOptions: launchOptions)
133
- }
134
-
135
- // Forward the APNS token to Firebase so it can exchange it for an FCM token
136
- override func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
137
- Messaging.messaging().setAPNSToken(deviceToken, type: .unknown)
138
- }
139
-
140
- override func bundleURL() -> URL? {
141
- #if DEBUG
142
- RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index")
143
- #else
144
- Bundle.main.url(forResource: "main", withExtension: "jsbundle")
145
- #endif
146
- }
147
- }
148
-
149
- // Receive the FCM token and pass it to DashX
150
- extension AppDelegate: MessagingDelegate {
151
- func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
152
- guard let token = fcmToken else { return }
153
- DashX.setFCMToken(to: token)
154
- }
155
- }
156
- ```
157
-
158
- `DashXRCTAppDelegate` handles the rest automatically:
159
- - Requests notification permission on launch
160
- - Displays notifications (banner + sound by default, even while app is in the foreground)
161
- - Tracks delivery, clicks, and dismissals
162
- - Forwards universal links to the JS `onLinkReceived` listener
163
-
164
- **Override points** (all optional):
165
-
166
- ```swift
167
- /// Control foreground notification presentation (default: [.banner, .sound])
168
- override func notificationDeliveredInForeground(message: [AnyHashable: Any]) -> UNNotificationPresentationOptions {
169
- return [.banner, .sound, .badge]
170
- }
171
-
172
- // Called when the user taps a notification or one of its action buttons
173
- override func notificationClicked(message: [AnyHashable: Any], actionIdentifier: String) {
174
- // custom tap handling
175
- }
176
-
177
- // Called when a universal/deep link arrives
178
- override func handleLink(url: URL) {
179
- // custom link handling
180
- }
181
- ```
182
-
183
- #### Step 5: Subscribe in JavaScript
184
-
185
- Call `DashX.subscribe()` at app startup so the SDK registers the device as soon as the FCM token is available:
186
-
187
- ```js
188
- import { useEffect } from 'react';
189
- import DashX from '@dashx/react-native';
190
-
191
- DashX.configure({ publicKey: 'your-public-key' });
192
-
193
- export default function App() {
194
- useEffect(() => {
195
- DashX.subscribe();
196
- }, []);
197
-
198
- // ...
199
- }
200
- ```
201
-
202
- > **Without Firebase:** If you are not using FCM, omit the Firebase pods and the Firebase-related `AppDelegate` code. The SDK will still handle APNS permission requests and notification display via `DashXRCTAppDelegate`.
203
-
204
- ## Usage
205
-
206
- ```js
207
- import DashX from '@dashx/react-native';
208
- ```
209
-
210
- ### Configure
211
-
212
- Initialize the SDK once at app startup. Call `configure` at the top level of your entry file (e.g. `App.js`) so it runs before any component mounts:
213
-
214
- ```js
215
- // App.js — call at module level, before the component
216
- import DashX from '@dashx/react-native';
217
-
218
- DashX.configure({
219
- publicKey: 'your-public-key',
220
- // baseURI: 'https://api.dashx.com', // optional, custom API endpoint
221
- // targetEnvironment: 'production', // optional
222
- });
223
-
224
- export default function App() {
225
- // ...
226
- }
227
- ```
228
-
229
- Calling from `useEffect` in your root component also works, but module-level init is preferred so the SDK is ready as early as possible.
230
-
231
- | Option | Type | Required | Description |
232
- |--------|------|----------|-------------|
233
- | `publicKey` | `string` | Yes | Your DashX public key |
234
- | `baseURI` | `string` | No | Custom API base URI |
235
- | `targetEnvironment` | `string` | No | Target environment name |
236
-
237
- ### Identify a user
238
-
239
- ```js
240
- DashX.identify({
241
- firstName: 'John',
242
- lastName: 'Doe',
243
- email: 'john@example.com',
244
- });
245
- ```
246
-
247
- ### Set identity with token
248
-
249
- ```js
250
- DashX.setIdentity('user-uid', 'user-token');
251
- ```
252
-
253
- ### Reset
254
-
255
- Clear the current identity:
256
-
257
- ```js
258
- DashX.reset();
259
- ```
260
-
261
- ### Track events
262
-
263
- ```js
264
- DashX.track('Item Purchased', {
265
- itemId: 'sku-123',
266
- price: 9.99,
267
- });
268
- ```
269
-
270
- ### Track screen views
271
-
272
- ```js
273
- DashX.screen('HomeScreen', {
274
- referrer: 'DeepLink',
275
- });
276
- ```
277
-
278
- ### Fetch a record
279
-
280
- Fetch a single record by URN (e.g. `"article/uuid"`). The URN must use the full UUID form of the record ID.
281
-
282
- ```js
283
- const record = await DashX.fetchRecord('article/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx', {
284
- preview: false,
285
- language: 'en',
286
- // fields, include, exclude as arrays of objects if needed
287
- });
288
- ```
289
-
290
- | Option | Type | Description |
291
- |--------|------|-------------|
292
- | `preview` | `boolean` | Include draft/preview content |
293
- | `language` | `string` | Content language |
294
- | `fields` | `object[]` | Fields to return |
295
- | `include` | `object[]` | Related data to include |
296
- | `exclude` | `object[]` | Fields to exclude |
297
-
298
- ### Search records
299
-
300
- ```js
301
- const records = await DashX.searchRecords('article', {
302
- filter: { status: 'published' },
303
- order: [{ field: 'createdAt', direction: 'desc' }],
304
- limit: 10,
305
- page: 1,
306
- preview: false,
307
- language: 'en',
308
- });
309
- ```
310
-
311
- | Option | Type | Description |
312
- |--------|------|-------------|
313
- | `filter` | `object` | Filter criteria |
314
- | `order` | `object[]` | Sort order (e.g. `[{ field, direction }]`) |
315
- | `limit` | `number` | Max records to return |
316
- | `page` | `number` | Page number for pagination |
317
- | `preview` | `boolean` | Preview/draft content |
318
- | `language` | `string` | Content language |
319
- | `fields` | `object[]` | Fields to return |
320
- | `include` | `object[]` | Related data to include |
321
- | `exclude` | `object[]` | Fields to exclude |
322
-
323
- ### Push notifications
324
-
325
- Subscribe the current device to push notifications:
326
-
327
- ```js
328
- DashX.subscribe();
329
- ```
330
-
331
- Unsubscribe:
332
-
333
- ```js
334
- DashX.unsubscribe();
335
- ```
336
-
337
- ### Request notification permission (iOS)
338
-
339
- Prompt the user for push notification permission. Returns a `NotificationPermissionStatus` integer matching `UNAuthorizationStatus`:
340
-
341
- | Value | Meaning |
342
- |-------|---------|
343
- | `0` | Not determined |
344
- | `1` | Denied |
345
- | `2` | Authorized |
346
- | `3` | Provisional |
347
- | `4` | Ephemeral |
348
-
349
- ```js
350
- const status = await DashX.requestNotificationPermission();
351
- if (status === 2) {
352
- DashX.subscribe();
353
- }
354
- ```
355
-
356
- To check the current status without prompting the user:
357
-
358
- ```js
359
- const status = await DashX.getNotificationPermissionStatus();
360
- ```
361
-
362
- ### Listen for incoming messages
363
-
364
- Register a listener to handle push notifications when they arrive:
365
-
366
- ```js
367
- import { useEffect } from 'react';
368
-
369
- useEffect(() => {
370
- const subscription = DashX.onMessageReceived((message) => {
371
- console.log('Notification received:', message);
372
- });
373
-
374
- return () => subscription.remove();
375
- }, []);
376
- ```
377
-
378
- ### Listen for deep links / universal links (iOS)
379
-
380
- Register a listener that fires when the SDK receives a universal link. On the native side, `DashXRCTAppDelegate` forwards links via `DashX.handleUserActivity` automatically.
381
-
382
- ```js
383
- useEffect(() => {
384
- const subscription = DashX.onLinkReceived((url) => {
385
- console.log('Link received:', url);
386
- });
387
-
388
- return () => subscription.remove();
389
- }, []);
390
- ```
391
-
392
- ### Notification preferences
393
-
394
- Fetch the user's stored notification preferences:
43
+ ## Documentation
395
44
 
396
- ```js
397
- const preferences = await DashX.fetchStoredPreferences();
398
- ```
45
+ For detailed documentation, visit [React Native SDK documentation](https://docs.dashx.com/sdks/client-side/react-native-sdk).
399
46
 
400
- Save updated preferences:
401
-
402
- ```js
403
- await DashX.saveStoredPreferences({
404
- marketing_emails: true,
405
- product_updates: false,
406
- });
407
- ```
47
+ ## Deep linking and push navigation
408
48
 
409
- ### Upload an asset (iOS)
410
-
411
- Upload a local file to DashX and associate it with a resource and attribute:
412
-
413
- ```js
414
- const response = await DashX.uploadAsset(
415
- '/path/to/local/file.jpg',
416
- 'user', // resource name
417
- 'avatar' // attribute name
418
- );
419
- // response: { status, data: { asset: { status, url, playbackIds } } }
420
- ```
421
-
422
- ### Fetch an asset (iOS)
423
-
424
- Retrieve the status and URL of a previously uploaded asset by its ID:
425
-
426
- ```js
427
- const asset = await DashX.fetchAsset('asset-id');
428
- // asset: { status, data: { asset: { status, url, playbackIds } } }
429
- ```
430
-
431
- ### Lifecycle tracking (iOS)
432
-
433
- Enable automatic app lifecycle event tracking (app opened, backgrounded, etc.):
434
-
435
- ```js
436
- DashX.enableLifecycleTracking(); // no-op on Android
437
- ```
438
-
439
- ### Ad tracking / ATT (iOS)
440
-
441
- Request App Tracking Transparency permission and enable IDFA tracking:
442
-
443
- ```js
444
- DashX.enableAdTracking(); // no-op on Android
445
- ```
446
-
447
- Make sure `NSUserTrackingUsageDescription` is set in your `Info.plist`.
448
-
449
- ### Logging
450
-
451
- Set the SDK log level for debugging:
452
-
453
- ```js
454
- DashX.setLogLevel(1); // 0 = off, 1 = errors, 2 = debug
455
- ```
456
-
457
- ## API Reference
458
-
459
- | Method | Signature | Platforms | Returns | Description |
460
- |--------|-----------|-----------|---------|-------------|
461
- | `configure` | `(options: object)` | iOS, Android | `void` | Initialize the SDK with your public key |
462
- | `identify` | `(options: object)` | iOS, Android | `void` | Identify the current user with traits |
463
- | `setIdentity` | `(uid: string, token: string)` | iOS, Android | `void` | Set user identity with UID and auth token |
464
- | `reset` | `()` | iOS, Android | `void` | Clear the current user identity |
465
- | `track` | `(event: string, data?: object)` | iOS, Android | `void` | Track a named event with optional data |
466
- | `screen` | `(screenName: string, data?: object)` | iOS, Android | `void` | Track a screen view |
467
- | `fetchRecord` | `(urn: string, options?: object)` | iOS, Android | `Promise<object>` | Fetch a single record by URN (`"resource/uuid"`) |
468
- | `searchRecords` | `(resource: string, options?: object)` | iOS, Android | `Promise<object[]>` | Search records with filter, order, limit, page, etc. |
469
- | `subscribe` | `()` | iOS, Android | `void` | Subscribe device for push notifications |
470
- | `unsubscribe` | `()` | iOS, Android | `void` | Unsubscribe device from push notifications |
471
- | `fetchStoredPreferences` | `()` | iOS, Android | `Promise<object>` | Fetch stored notification preferences |
472
- | `saveStoredPreferences` | `(data: object)` | iOS, Android | `Promise<object>` | Save notification preferences |
473
- | `setLogLevel` | `(level: number)` | iOS, Android | `void` | Set SDK log verbosity (0=off, 1=errors, 2=debug) |
474
- | `onMessageReceived` | `(callback: function)` | iOS, Android | `EmitterSubscription` | Listen for incoming push notifications |
475
- | `requestNotificationPermission` | `()` | iOS | `Promise<number>` | Prompt for push permission; resolves with `UNAuthorizationStatus` (0-4) |
476
- | `getNotificationPermissionStatus` | `()` | iOS | `Promise<number>` | Get current push permission status without prompting |
477
- | `onLinkReceived` | `(callback: function)` | iOS | `EmitterSubscription` | Listen for universal/deep link events |
478
- | `uploadAsset` | `(filePath: string, resource: string, attribute: string)` | iOS | `Promise<AssetResponse>` | Upload a local file and associate it with a resource attribute |
479
- | `fetchAsset` | `(assetId: string)` | iOS | `Promise<AssetResponse>` | Fetch status and URL of an uploaded asset |
480
- | `enableLifecycleTracking` | `()` | iOS | `void` | Enable automatic app lifecycle event tracking |
481
- | `enableAdTracking` | `()` | iOS | `void` | Request ATT permission and enable IDFA tracking |
482
-
483
- For detailed usage and advanced features, refer to the [DashX documentation](https://docs.dashx.com).
484
-
485
- ### Error Codes
486
-
487
- Promise rejections from native methods use a consistent error code across both platforms:
488
-
489
- | Code | Description |
490
- |------|-------------|
491
- | `EUNSPECIFIED` | An unspecified error occurred |
492
-
493
- The error code is available on the `code` property of the rejected error:
494
-
495
- ```js
496
- try {
497
- await DashX.fetchRecord('article/123');
498
- } catch (error) {
499
- console.log(error.code); // "EUNSPECIFIED"
500
- console.log(error.message); // human-readable description
501
- }
502
- ```
503
-
504
- A `DashXErrorCode` enum is exported from the TypeScript definitions for type-safe comparisons.
49
+ See the [Deep Linking & Push Navigation](https://docs.dashx.com/apps/messaging/deep-linking) guide for setup instructions, payload fields, and code examples.
505
50
 
506
51
  ## Contributing
507
52
 
508
- Please follow [these steps](https://github.com/dashxhq/dashx-js/tree/master/examples/react-native#setting-up-development-environment) to set up the development environment.
509
-
510
- You'll also need Apollo CLI to generate files:
511
-
512
- ```sh
513
- npm i -g apollo
514
- ```
515
-
516
- ## Publishing
517
-
518
- Use `yarn publish` to bump the version and release, then push the commit:
519
-
520
- ```sh
521
- yarn publish
522
- git push origin main
523
- ```
53
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and workflow.
524
54
 
525
55
  ## License
526
56
 
@@ -16,6 +16,8 @@ const mockNativeModule = {
16
16
  fetchAsset: jest.fn(),
17
17
  enableLifecycleTracking: jest.fn(),
18
18
  enableAdTracking: jest.fn(),
19
+ processURL: jest.fn(),
20
+ trackNotificationNavigation: jest.fn(),
19
21
  requestNotificationPermission: jest.fn(),
20
22
  getNotificationPermissionStatus: jest.fn(),
21
23
  addListener: jest.fn(),
@@ -32,6 +34,7 @@ jest.mock('react-native', () => ({
32
34
  addListener: mockAddListener,
33
35
  })),
34
36
  Platform: {
37
+ OS: 'ios',
35
38
  select: jest.fn((obj) => obj.default || ''),
36
39
  },
37
40
  }));
@@ -325,6 +328,133 @@ describe('fetchAsset', () => {
325
328
  });
326
329
  });
327
330
 
331
+ // --- processURL ---
332
+
333
+ describe('processURL', () => {
334
+ it('calls native processURL with defaults', () => {
335
+ DashX.processURL('https://example.com/path');
336
+ expect(mockNativeModule.processURL).toHaveBeenCalledWith(
337
+ 'https://example.com/path',
338
+ null,
339
+ true
340
+ );
341
+ });
342
+
343
+ it('passes source option', () => {
344
+ DashX.processURL('https://example.com', { source: 'universal_link' });
345
+ expect(mockNativeModule.processURL).toHaveBeenCalledWith(
346
+ 'https://example.com',
347
+ 'universal_link',
348
+ true
349
+ );
350
+ });
351
+
352
+ it('passes forwardToLinkHandler as false', () => {
353
+ DashX.processURL('https://example.com', { forwardToLinkHandler: false });
354
+ expect(mockNativeModule.processURL).toHaveBeenCalledWith(
355
+ 'https://example.com',
356
+ null,
357
+ false
358
+ );
359
+ });
360
+
361
+ it('throws if url is missing', () => {
362
+ expect(() => DashX.processURL(null)).toThrow('url is required');
363
+ });
364
+
365
+ it('throws if url is not a string', () => {
366
+ expect(() => DashX.processURL(123)).toThrow(
367
+ 'url is required and must be a string'
368
+ );
369
+ });
370
+
371
+ it('no-ops on Android', () => {
372
+ const { Platform } = require('react-native');
373
+ Platform.OS = 'android';
374
+ DashX.processURL('https://example.com');
375
+ expect(mockNativeModule.processURL).not.toHaveBeenCalled();
376
+ Platform.OS = 'ios';
377
+ });
378
+ });
379
+
380
+ // --- trackNotificationNavigation ---
381
+
382
+ describe('trackNotificationNavigation', () => {
383
+ it('calls native with deepLink action', () => {
384
+ DashX.trackNotificationNavigation(
385
+ { type: 'deepLink', url: 'https://example.com' },
386
+ 'notif-1'
387
+ );
388
+ expect(mockNativeModule.trackNotificationNavigation).toHaveBeenCalledWith(
389
+ { type: 'deepLink', url: 'https://example.com' },
390
+ 'notif-1'
391
+ );
392
+ });
393
+
394
+ it('calls native with screen action', () => {
395
+ DashX.trackNotificationNavigation(
396
+ { type: 'screen', name: 'Home', data: { ref: 'banner' } },
397
+ 'notif-2'
398
+ );
399
+ expect(mockNativeModule.trackNotificationNavigation).toHaveBeenCalledWith(
400
+ { type: 'screen', name: 'Home', data: { ref: 'banner' } },
401
+ 'notif-2'
402
+ );
403
+ });
404
+
405
+ it('calls native with richLanding action', () => {
406
+ DashX.trackNotificationNavigation(
407
+ { type: 'richLanding', url: 'https://promo.example.com' },
408
+ 'notif-3'
409
+ );
410
+ expect(mockNativeModule.trackNotificationNavigation).toHaveBeenCalledWith(
411
+ { type: 'richLanding', url: 'https://promo.example.com' },
412
+ 'notif-3'
413
+ );
414
+ });
415
+
416
+ it('calls native with clickAction action', () => {
417
+ DashX.trackNotificationNavigation(
418
+ { type: 'clickAction', action: 'OPEN_SETTINGS' },
419
+ 'notif-4'
420
+ );
421
+ expect(mockNativeModule.trackNotificationNavigation).toHaveBeenCalledWith(
422
+ { type: 'clickAction', action: 'OPEN_SETTINGS' },
423
+ 'notif-4'
424
+ );
425
+ });
426
+
427
+ it('calls native with null action when omitted', () => {
428
+ DashX.trackNotificationNavigation(null, 'notif-5');
429
+ expect(mockNativeModule.trackNotificationNavigation).toHaveBeenCalledWith(
430
+ null,
431
+ 'notif-5'
432
+ );
433
+ });
434
+
435
+ it('calls native with null for both when both omitted', () => {
436
+ DashX.trackNotificationNavigation();
437
+ expect(mockNativeModule.trackNotificationNavigation).toHaveBeenCalledWith(
438
+ null,
439
+ null
440
+ );
441
+ });
442
+
443
+ it('throws if action is not an object', () => {
444
+ expect(() => DashX.trackNotificationNavigation('bad')).toThrow(
445
+ 'action must be an object or null'
446
+ );
447
+ });
448
+
449
+ it('no-ops on Android', () => {
450
+ const { Platform } = require('react-native');
451
+ Platform.OS = 'android';
452
+ DashX.trackNotificationNavigation({ type: 'deepLink', url: 'https://example.com' });
453
+ expect(mockNativeModule.trackNotificationNavigation).not.toHaveBeenCalled();
454
+ Platform.OS = 'ios';
455
+ });
456
+ });
457
+
328
458
  // --- requestNotificationPermission / getNotificationPermissionStatus ---
329
459
 
330
460
  describe('requestNotificationPermission', () => {
@@ -55,5 +55,5 @@ dependencies {
55
55
  // Ensure Firebase Messaging types are available for FCM service subclass
56
56
  implementation 'com.google.firebase:firebase-messaging-ktx:23.1.2'
57
57
 
58
- implementation "com.dashx:dashx-android:1.1.7"
58
+ implementation "com.dashx:dashx-android:1.2.4"
59
59
  }
package/index.d.ts CHANGED
@@ -66,6 +66,17 @@ export interface NotificationMessage {
66
66
  [key: string]: unknown;
67
67
  }
68
68
 
69
+ export type NavigationAction =
70
+ | { type: 'deepLink'; url: string }
71
+ | { type: 'screen'; name: string; data?: Record<string, string> }
72
+ | { type: 'richLanding'; url: string }
73
+ | { type: 'clickAction'; action: string };
74
+
75
+ export interface ProcessURLOptions {
76
+ source?: string;
77
+ forwardToLinkHandler?: boolean;
78
+ }
79
+
69
80
  export interface DashXInstance {
70
81
  /**
71
82
  * Initialize the SDK with your public key and optional configuration.
@@ -176,6 +187,24 @@ export interface DashXInstance {
176
187
  */
177
188
  enableAdTracking(): void;
178
189
 
190
+ /**
191
+ * Track a deep link open event and optionally forward the URL to the link handler (iOS only).
192
+ * @param url The deep link URL string.
193
+ * @param options.source Optional attribution source (e.g. "universal_link", "notification").
194
+ * @param options.forwardToLinkHandler Whether to forward to the link handler callback (default: true).
195
+ */
196
+ processURL(url: string, options?: ProcessURLOptions): void;
197
+
198
+ /**
199
+ * Track a notification navigation event when the user taps a notification (iOS only).
200
+ * @param action The navigation action taken, or null/undefined for default.
201
+ * @param notificationId Optional notification ID to associate with the event.
202
+ */
203
+ trackNotificationNavigation(
204
+ action?: NavigationAction | null,
205
+ notificationId?: string
206
+ ): void;
207
+
179
208
  /**
180
209
  * Request push notification permission from the user.
181
210
  * Resolves with a NotificationPermissionStatus integer.
package/index.js CHANGED
@@ -177,6 +177,31 @@ const DashX = {
177
177
  return getNativeDashX().enableAdTracking();
178
178
  },
179
179
 
180
+ processURL(url, options = {}) {
181
+ if (Platform.OS !== 'ios') return;
182
+ if (!url || typeof url !== 'string') {
183
+ throw new Error('DashX.processURL: url is required and must be a string');
184
+ }
185
+ return getNativeDashX().processURL(
186
+ url,
187
+ options.source || null,
188
+ options.forwardToLinkHandler !== false
189
+ );
190
+ },
191
+
192
+ trackNotificationNavigation(action, notificationId) {
193
+ if (Platform.OS !== 'ios') return;
194
+ if (action != null && typeof action !== 'object') {
195
+ throw new Error(
196
+ 'DashX.trackNotificationNavigation: action must be an object or null'
197
+ );
198
+ }
199
+ return getNativeDashX().trackNotificationNavigation(
200
+ action || null,
201
+ notificationId || null
202
+ );
203
+ },
204
+
180
205
  requestNotificationPermission() {
181
206
  return getNativeDashX().requestNotificationPermission();
182
207
  },
@@ -183,6 +183,40 @@ class DashXReactNative: RCTEventEmitter {
183
183
  DashX.enableAdTracking()
184
184
  }
185
185
 
186
+ @objc(processURL:source:forwardToLinkHandler:)
187
+ func processURL(_ url: String, source: NSString?, forwardToLinkHandler: Bool) {
188
+ guard let parsedURL = URL(string: url) else { return }
189
+ DashX.processURL(parsedURL, source: source as String?, forwardToLinkHandler: forwardToLinkHandler)
190
+ }
191
+
192
+ @objc(trackNotificationNavigation:notificationId:)
193
+ func trackNotificationNavigation(_ action: NSDictionary?, notificationId: NSString?) {
194
+ var navigationAction: NavigationAction? = nil
195
+ if let action = action, let type = action["type"] as? String {
196
+ switch type {
197
+ case "deepLink":
198
+ if let urlString = action["url"] as? String, let url = URL(string: urlString) {
199
+ navigationAction = .deepLink(url: url)
200
+ }
201
+ case "screen":
202
+ if let name = action["name"] as? String {
203
+ navigationAction = .screen(name: name, data: action["data"] as? [String: String])
204
+ }
205
+ case "richLanding":
206
+ if let urlString = action["url"] as? String, let url = URL(string: urlString) {
207
+ navigationAction = .richLanding(url: url)
208
+ }
209
+ case "clickAction":
210
+ if let actionStr = action["action"] as? String {
211
+ navigationAction = .clickAction(action: actionStr)
212
+ }
213
+ default:
214
+ break
215
+ }
216
+ }
217
+ DashX.trackNotificationNavigation(navigationAction, notificationId: notificationId as String?)
218
+ }
219
+
186
220
  @objc(requestNotificationPermission:rejecter:)
187
221
  func requestNotificationPermission(_ resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
188
222
  DashX.requestNotificationPermission { status in resolve(status.rawValue) }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@dashx/react-native",
3
3
  "title": "DashX React Native",
4
- "version": "1.1.4",
4
+ "version": "1.1.5",
5
5
  "description": "DashX SDK for React Native",
6
6
  "main": "index.js",
7
7
  "types": "index.d.ts",