@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 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.
@@ -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.9'
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", "~> 1.1.9"
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
@@ -106,8 +106,25 @@ describe('setIdentity', () => {
106
106
  );
107
107
  });
108
108
 
109
- it('throws if uid is missing', () => {
110
- expect(() => DashX.setIdentity(null, 'token')).toThrow('uid is required');
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
- if (!uid || typeof uid !== 'string') {
62
- throw new Error(
63
- 'DashX.setIdentity: uid is required and must be a string'
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 || null);
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
- #if canImport(FirebaseMessaging)
123
- Messaging.messaging().setAPNSToken(deviceToken, type: .unknown)
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)
@@ -16,7 +16,7 @@ class DashXReactNative: RCTEventEmitter {
16
16
  }
17
17
 
18
18
  override func supportedEvents() -> [String] {
19
- return ["messageReceived", "linkReceived"]
19
+ return ["messageReceived", "linkReceived", "notificationClicked"]
20
20
  }
21
21
 
22
22
  @objc(configure:)
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.2.0",
4
+ "version": "1.3.0",
5
5
  "description": "DashX SDK for React Native",
6
6
  "main": "index.js",
7
7
  "types": "index.d.ts",