@dashx/react-native 1.2.0 → 1.3.0
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/.DS_Store +0 -0
- package/CHANGELOG.md +34 -0
- package/DashXReactNative.podspec +9 -2
- package/__tests__/index.test.js +30 -2
- package/index.d.ts +22 -2
- package/index.js +18 -5
- package/ios/DashXNotificationHandler.swift +48 -13
- package/ios/DashXReactNative.swift +1 -1
- package/package.json +1 -1
package/.DS_Store
CHANGED
|
Binary file
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@dashx/react-native` are documented in this file. Format loosely follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), versions follow [SemVer](https://semver.org/).
|
|
4
|
+
|
|
5
|
+
## [1.3.0] — 2026-04-20
|
|
6
|
+
|
|
7
|
+
### Breaking
|
|
8
|
+
- **`DashX` pod → `DashX/SDK`.** The podspec now depends on the `DashX/SDK` subspec of [dashx-ios 1.3.1+](https://github.com/dashxhq/dashx-ios) with no version constraint. Consumers must update their Podfile:
|
|
9
|
+
```ruby
|
|
10
|
+
# before
|
|
11
|
+
pod 'DashX', :git => 'https://github.com/dashxhq/dashx-ios.git', :tag => '1.1.9'
|
|
12
|
+
|
|
13
|
+
# after
|
|
14
|
+
pod 'DashX/SDK', :git => 'https://github.com/dashxhq/dashx-ios.git', :tag => '1.3.1'
|
|
15
|
+
```
|
|
16
|
+
- **`FirebaseMessaging` is now a hard dependency.** Previously the bridge guarded Firebase usage with `#if canImport(FirebaseMessaging)`, but `canImport` doesn't see consumer-Podfile pods at this pod's compile time, so the guard silently elided every FCM code path — push registration stopped working on fresh installs. Consumers must enable modular headers in their Podfile:
|
|
17
|
+
```ruby
|
|
18
|
+
use_modular_headers!
|
|
19
|
+
# or, per-pod:
|
|
20
|
+
pod 'FirebaseMessaging', :modular_headers => true
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
- `DashX.onNotificationClicked(callback)` — fires when the user taps a DashX notification or one of its action buttons. Callback receives `{ notification, action, actionIdentifier }` where `action` is a resolved `NavigationAction` (`deepLink` / `screen` / `richLanding` / `clickAction`) or `null`.
|
|
25
|
+
- `DashXNotificationResponseResult.navigationAction` — new Swift-only field on the helper result type returned by `DashXNotificationHandler.handleNotificationResponse(_:)`.
|
|
26
|
+
- `NotificationClickedEvent` exported from `index.d.ts`.
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
- **iOS dev builds silently failing to receive pushes.** `Messaging.messaging().setAPNSToken(_, type: .unknown)` sometimes mis-tagged the APNs environment on dev-signed binaries, producing an FCM token whose pushes 410'd at Apple. The bridge now forces `.sandbox` in `DEBUG` configurations and `.prod` otherwise.
|
|
30
|
+
- **`DashX.setIdentity(uid, token)` now passes `null` / `""` / `undefined` through to the native SDK unchanged** (apart from normalizing `undefined → null`, since the RN bridge can't carry `undefined`). Previously the JS wrapper threw on `null` / `""` / `undefined` for `uid`, contradicting the nullable signatures on iOS (`NSString?`) and Android (`String?`). TypeScript signature updated: `setIdentity(uid: string | null, token?: string | null): void`.
|
|
31
|
+
|
|
32
|
+
## [1.2.0] — 2026-04-16
|
|
33
|
+
|
|
34
|
+
Previous release — see git log.
|
package/DashXReactNative.podspec
CHANGED
|
@@ -21,8 +21,15 @@ Pod::Spec.new do |s|
|
|
|
21
21
|
install_modules_dependencies(s)
|
|
22
22
|
|
|
23
23
|
# DashX iOS SDK — consumers must provide the source in their Podfile:
|
|
24
|
-
# pod 'DashX', :git => 'https://github.com/dashxhq/dashx-ios.git', :tag => '1.1
|
|
24
|
+
# pod 'DashX/SDK', :git => 'https://github.com/dashxhq/dashx-ios.git', :tag => '1.3.1'
|
|
25
25
|
# For local development:
|
|
26
26
|
# pod 'DashX', :path => '../dashx-ios'
|
|
27
|
-
s.dependency "DashX"
|
|
27
|
+
s.dependency "DashX/SDK"
|
|
28
|
+
|
|
29
|
+
# Hard dependency so that `canImport(FirebaseMessaging)`
|
|
30
|
+
# evaluates at this pod's compile time, where consumer-Podfile pods aren't
|
|
31
|
+
# visible — so an "optional" guard silently elides every Firebase code path.
|
|
32
|
+
# Consumers should set `use_modular_headers!` (or `:modular_headers => true`
|
|
33
|
+
# on FirebaseMessaging) in their Podfile so this Swift import resolves.
|
|
34
|
+
s.dependency "FirebaseMessaging"
|
|
28
35
|
end
|
package/__tests__/index.test.js
CHANGED
|
@@ -106,8 +106,25 @@ describe('setIdentity', () => {
|
|
|
106
106
|
);
|
|
107
107
|
});
|
|
108
108
|
|
|
109
|
-
it('
|
|
110
|
-
|
|
109
|
+
it('passes null uid through to native', () => {
|
|
110
|
+
DashX.setIdentity(null, 'token');
|
|
111
|
+
expect(mockNativeModule.setIdentity).toHaveBeenCalledWith(null, 'token');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('normalizes undefined uid to null', () => {
|
|
115
|
+
DashX.setIdentity(undefined, 'token');
|
|
116
|
+
expect(mockNativeModule.setIdentity).toHaveBeenCalledWith(null, 'token');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('passes empty-string uid through to native without coercing to null', () => {
|
|
120
|
+
DashX.setIdentity('', 'token');
|
|
121
|
+
expect(mockNativeModule.setIdentity).toHaveBeenCalledWith('', 'token');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('throws if uid is not a string or null', () => {
|
|
125
|
+
expect(() => DashX.setIdentity(42, 'token')).toThrow(
|
|
126
|
+
'uid must be a string or null'
|
|
127
|
+
);
|
|
111
128
|
});
|
|
112
129
|
|
|
113
130
|
it('passes null token through to native', () => {
|
|
@@ -115,10 +132,21 @@ describe('setIdentity', () => {
|
|
|
115
132
|
expect(mockNativeModule.setIdentity).toHaveBeenCalledWith('uid', null);
|
|
116
133
|
});
|
|
117
134
|
|
|
135
|
+
it('passes empty-string token through to native without coercing to null', () => {
|
|
136
|
+
DashX.setIdentity('uid', '');
|
|
137
|
+
expect(mockNativeModule.setIdentity).toHaveBeenCalledWith('uid', '');
|
|
138
|
+
});
|
|
139
|
+
|
|
118
140
|
it('passes undefined token as null to native', () => {
|
|
119
141
|
DashX.setIdentity('uid');
|
|
120
142
|
expect(mockNativeModule.setIdentity).toHaveBeenCalledWith('uid', null);
|
|
121
143
|
});
|
|
144
|
+
|
|
145
|
+
it('throws if token is not a string or null', () => {
|
|
146
|
+
expect(() => DashX.setIdentity('uid', 42)).toThrow(
|
|
147
|
+
'token must be a string or null'
|
|
148
|
+
);
|
|
149
|
+
});
|
|
122
150
|
});
|
|
123
151
|
|
|
124
152
|
// --- reset ---
|
package/index.d.ts
CHANGED
|
@@ -74,6 +74,12 @@ export type NavigationAction =
|
|
|
74
74
|
| { type: 'richLanding'; url: string }
|
|
75
75
|
| { type: 'clickAction'; action: string };
|
|
76
76
|
|
|
77
|
+
export interface NotificationClickedEvent {
|
|
78
|
+
notification: NotificationMessage;
|
|
79
|
+
action: NavigationAction | null;
|
|
80
|
+
actionIdentifier: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
77
83
|
export interface ProcessURLOptions {
|
|
78
84
|
source?: string;
|
|
79
85
|
forwardToLinkHandler?: boolean;
|
|
@@ -92,9 +98,11 @@ export interface DashXInstance {
|
|
|
92
98
|
identify(options: IdentifyOptions): void;
|
|
93
99
|
|
|
94
100
|
/**
|
|
95
|
-
* Set user identity using a UID and authentication token.
|
|
101
|
+
* Set user identity using a UID and authentication token. Both are
|
|
102
|
+
* nullable — passing `null` clears that field on the native SDK. Empty
|
|
103
|
+
* strings are forwarded as-is (no coercion to `null`).
|
|
96
104
|
*/
|
|
97
|
-
setIdentity(uid: string, token?: string | null): void;
|
|
105
|
+
setIdentity(uid: string | null, token?: string | null): void;
|
|
98
106
|
|
|
99
107
|
/**
|
|
100
108
|
* Clear the current user identity and reset SDK state.
|
|
@@ -224,6 +232,18 @@ export interface DashXInstance {
|
|
|
224
232
|
* Returns a subscription that can be removed via `.remove()`.
|
|
225
233
|
*/
|
|
226
234
|
onLinkReceived(callback: (url: string) => void): EmitterSubscription;
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Register a listener for notification tap events. Fired when the user taps
|
|
238
|
+
* a DashX notification or one of its action buttons. The callback receives
|
|
239
|
+
* the full notification payload, the resolved `NavigationAction` (parsed
|
|
240
|
+
* from the payload; `null` if no navigation intent could be resolved), and
|
|
241
|
+
* the raw `actionIdentifier` from `UNNotificationResponse`.
|
|
242
|
+
* Returns a subscription that can be removed via `.remove()`.
|
|
243
|
+
*/
|
|
244
|
+
onNotificationClicked(
|
|
245
|
+
callback: (event: NotificationClickedEvent) => void
|
|
246
|
+
): EmitterSubscription;
|
|
227
247
|
}
|
|
228
248
|
|
|
229
249
|
declare const DashX: DashXInstance;
|
package/index.js
CHANGED
|
@@ -58,12 +58,16 @@ const DashX = {
|
|
|
58
58
|
},
|
|
59
59
|
|
|
60
60
|
setIdentity(uid, token) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
61
|
+
// Both uid and token are nullable passthroughs to the native SDK. Only
|
|
62
|
+
// validate TYPE here — the native side decides semantics. `undefined` is
|
|
63
|
+
// normalized to `null` because the RN bridge can't carry undefined.
|
|
64
|
+
if (uid !== null && uid !== undefined && typeof uid !== 'string') {
|
|
65
|
+
throw new Error('DashX.setIdentity: uid must be a string or null');
|
|
66
|
+
}
|
|
67
|
+
if (token !== null && token !== undefined && typeof token !== 'string') {
|
|
68
|
+
throw new Error('DashX.setIdentity: token must be a string or null');
|
|
65
69
|
}
|
|
66
|
-
return getNativeDashX().setIdentity(uid, token
|
|
70
|
+
return getNativeDashX().setIdentity(uid ?? null, token ?? null);
|
|
67
71
|
},
|
|
68
72
|
|
|
69
73
|
reset() {
|
|
@@ -214,6 +218,15 @@ const DashX = {
|
|
|
214
218
|
}
|
|
215
219
|
return getEventEmitter().addListener('linkReceived', callback);
|
|
216
220
|
},
|
|
221
|
+
|
|
222
|
+
onNotificationClicked(callback) {
|
|
223
|
+
if (typeof callback !== 'function') {
|
|
224
|
+
throw new Error(
|
|
225
|
+
'DashX.onNotificationClicked: callback must be a function'
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
return getEventEmitter().addListener('notificationClicked', callback);
|
|
229
|
+
},
|
|
217
230
|
};
|
|
218
231
|
|
|
219
232
|
export default DashX;
|
|
@@ -2,9 +2,7 @@ import Foundation
|
|
|
2
2
|
import UIKit
|
|
3
3
|
import UserNotifications
|
|
4
4
|
import DashX
|
|
5
|
-
#if canImport(FirebaseMessaging)
|
|
6
5
|
import FirebaseMessaging
|
|
7
|
-
#endif
|
|
8
6
|
|
|
9
7
|
/// The result of handling a notification response.
|
|
10
8
|
/// Consumers can use `url` to navigate and `actionIdentifier` for custom action handling.
|
|
@@ -14,11 +12,15 @@ public final class DashXNotificationResponseResult: NSObject {
|
|
|
14
12
|
@objc public let actionIdentifier: String
|
|
15
13
|
/// The DashX notification URL, if present in the payload.
|
|
16
14
|
@objc public let url: URL?
|
|
15
|
+
/// The resolved navigation intent for the tap / action button.
|
|
16
|
+
/// Non-@objc (the enum isn't representable in Obj-C); read via Swift.
|
|
17
|
+
public let navigationAction: NavigationAction?
|
|
17
18
|
|
|
18
|
-
init(userInfo: [AnyHashable: Any], actionIdentifier: String, url: URL?) {
|
|
19
|
+
init(userInfo: [AnyHashable: Any], actionIdentifier: String, url: URL?, navigationAction: NavigationAction?) {
|
|
19
20
|
self.userInfo = userInfo
|
|
20
21
|
self.actionIdentifier = actionIdentifier
|
|
21
22
|
self.url = url
|
|
23
|
+
self.navigationAction = navigationAction
|
|
22
24
|
}
|
|
23
25
|
}
|
|
24
26
|
|
|
@@ -30,9 +32,7 @@ public final class DashXNotificationHandler: NSObject {
|
|
|
30
32
|
userInfo: [AnyHashable: Any],
|
|
31
33
|
completionHandler: @escaping (UIBackgroundFetchResult) -> Void
|
|
32
34
|
) {
|
|
33
|
-
#if canImport(FirebaseMessaging)
|
|
34
35
|
Messaging.messaging().appDidReceiveMessage(userInfo)
|
|
35
|
-
#endif
|
|
36
36
|
|
|
37
37
|
DashXEventEmitter.instance.dispatch(
|
|
38
38
|
name: "messageReceived",
|
|
@@ -88,9 +88,7 @@ public final class DashXNotificationHandler: NSObject {
|
|
|
88
88
|
public static func handleNotificationResponse(_ response: UNNotificationResponse) -> DashXNotificationResponseResult {
|
|
89
89
|
let userInfo = response.notification.request.content.userInfo
|
|
90
90
|
|
|
91
|
-
#if canImport(FirebaseMessaging)
|
|
92
91
|
Messaging.messaging().appDidReceiveMessage(userInfo)
|
|
93
|
-
#endif
|
|
94
92
|
|
|
95
93
|
DashXEventEmitter.instance.dispatch(
|
|
96
94
|
name: "messageReceived",
|
|
@@ -104,11 +102,25 @@ public final class DashXNotificationHandler: NSObject {
|
|
|
104
102
|
}
|
|
105
103
|
|
|
106
104
|
let notificationUrl = userInfo.dashxNotificationUrl()
|
|
105
|
+
let dashxData = userInfo.dashxNotificationData()
|
|
106
|
+
let navigationAction = dashxData?.navigationAction(forActionIdentifier: response.actionIdentifier)
|
|
107
|
+
|
|
108
|
+
if response.actionIdentifier != UNNotificationDismissActionIdentifier {
|
|
109
|
+
DashXEventEmitter.instance.dispatch(
|
|
110
|
+
name: "notificationClicked",
|
|
111
|
+
body: [
|
|
112
|
+
"notification": bridgeSafePayload(from: userInfo),
|
|
113
|
+
"action": jsonDictionary(from: navigationAction) ?? NSNull(),
|
|
114
|
+
"actionIdentifier": response.actionIdentifier,
|
|
115
|
+
]
|
|
116
|
+
)
|
|
117
|
+
}
|
|
107
118
|
|
|
108
119
|
return DashXNotificationResponseResult(
|
|
109
120
|
userInfo: userInfo,
|
|
110
121
|
actionIdentifier: response.actionIdentifier,
|
|
111
|
-
url: notificationUrl
|
|
122
|
+
url: notificationUrl,
|
|
123
|
+
navigationAction: navigationAction
|
|
112
124
|
)
|
|
113
125
|
}
|
|
114
126
|
|
|
@@ -119,14 +131,21 @@ public final class DashXNotificationHandler: NSObject {
|
|
|
119
131
|
/// `application(_:didRegisterForRemoteNotificationsWithDeviceToken:)`.
|
|
120
132
|
@objc
|
|
121
133
|
public static func handleDeviceToken(_ deviceToken: Data) {
|
|
122
|
-
|
|
123
|
-
|
|
134
|
+
// Force the APNs environment explicitly rather than using `.unknown`
|
|
135
|
+
// (auto-detect). Firebase's auto-detection reads the token's aps
|
|
136
|
+
// entitlement from the signed binary and sometimes mis-tags dev
|
|
137
|
+
// builds as production, producing an FCM token whose pushes silently
|
|
138
|
+
// fail at APNs with 410 Unregistered.
|
|
139
|
+
#if DEBUG
|
|
140
|
+
Messaging.messaging().setAPNSToken(deviceToken, type: .sandbox)
|
|
141
|
+
#else
|
|
142
|
+
Messaging.messaging().setAPNSToken(deviceToken, type: .prod)
|
|
143
|
+
#endif
|
|
124
144
|
Messaging.messaging().token { token, error in
|
|
125
145
|
if let token = token {
|
|
126
146
|
DashX.setFCMToken(to: token)
|
|
127
147
|
}
|
|
128
148
|
}
|
|
129
|
-
#endif
|
|
130
149
|
}
|
|
131
150
|
|
|
132
151
|
// MARK: - Foreground Notification
|
|
@@ -142,9 +161,7 @@ public final class DashXNotificationHandler: NSObject {
|
|
|
142
161
|
) {
|
|
143
162
|
let userInfo = notification.request.content.userInfo
|
|
144
163
|
|
|
145
|
-
#if canImport(FirebaseMessaging)
|
|
146
164
|
Messaging.messaging().appDidReceiveMessage(userInfo)
|
|
147
|
-
#endif
|
|
148
165
|
|
|
149
166
|
DashX.trackMessage(message: userInfo, event: .delivered)
|
|
150
167
|
|
|
@@ -197,6 +214,24 @@ public final class DashXNotificationHandler: NSObject {
|
|
|
197
214
|
task.resume()
|
|
198
215
|
}
|
|
199
216
|
|
|
217
|
+
/// Serializes a ``NavigationAction`` into the JS-bridge shape mirrored by
|
|
218
|
+
/// `NavigationAction` in `index.d.ts`. Returns `nil` for `nil` input.
|
|
219
|
+
private static func jsonDictionary(from action: NavigationAction?) -> [String: Any]? {
|
|
220
|
+
guard let action else { return nil }
|
|
221
|
+
switch action {
|
|
222
|
+
case .deepLink(let url):
|
|
223
|
+
return ["type": "deepLink", "url": url.absoluteString]
|
|
224
|
+
case .screen(let name, let data):
|
|
225
|
+
var dict: [String: Any] = ["type": "screen", "name": name]
|
|
226
|
+
if let data { dict["data"] = data }
|
|
227
|
+
return dict
|
|
228
|
+
case .richLanding(let url):
|
|
229
|
+
return ["type": "richLanding", "url": url.absoluteString]
|
|
230
|
+
case .clickAction(let actionString):
|
|
231
|
+
return ["type": "clickAction", "action": actionString]
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
200
235
|
private static func bridgeSafePayload(from userInfo: [AnyHashable: Any]) -> [String: Any] {
|
|
201
236
|
userInfo.reduce(into: [String: Any]()) { result, pair in
|
|
202
237
|
let key = String(describing: pair.key)
|