@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.
- package/.cursor/plans/deep_linking_implementation_plan_303c2ca9.plan.md +216 -0
- package/.gitignore +2 -2
- package/DashXReactNative.podspec +2 -2
- package/README.md +8 -478
- package/__tests__/index.test.js +130 -0
- package/android/build.gradle +1 -1
- package/index.d.ts +29 -0
- package/index.js +25 -0
- package/ios/DashXReactNative.swift +34 -0
- package/package.json +1 -1
|
@@ -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
package/DashXReactNative.podspec
CHANGED
|
@@ -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.
|
|
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.
|
|
48
|
+
s.dependency "DashX", "~> 1.1.9"
|
|
49
49
|
end
|
package/README.md
CHANGED
|
@@ -20,23 +20,16 @@
|
|
|
20
20
|
|
|
21
21
|
[](https://www.npmjs.com/package/@dashx/react-native)
|
|
22
22
|
[](https://github.com/dashxhq/dashx-react-native/blob/main/LICENSE)
|
|
23
|
-
[](#
|
|
23
|
+
[](#install)
|
|
24
24
|
|
|
25
25
|
_DashX SDK for React Native_
|
|
26
26
|
|
|
27
|
-
##
|
|
27
|
+
## Install
|
|
28
28
|
|
|
29
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
package/__tests__/index.test.js
CHANGED
|
@@ -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', () => {
|
package/android/build.gradle
CHANGED
|
@@ -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.
|
|
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) }
|