React Native
Ensure you have completed the Prerequisites section before continuing.
Requirements
- React Native: 0.74+
- Android: SDK 26+ (Android 8.0+)
- iOS: 13.0+
- Node.js: 16+
The SDK supports both the Old Architecture (bridge) and New Architecture (TurboModule) from a single install — no code changes or flags needed in your app.
This SDK uses native modules and is not compatible with Expo Go. If you're using Expo, use a development build.
Installation
If you're using npm:
npm install @dashx/react-native
If you're using yarn:
yarn add @dashx/react-native
Native setup (Push / FCM)
DashX push notifications are delivered via Firebase Cloud Messaging (FCM).
- Android
- iOS
- Add the Google Services plugin in
android/build.gradle:
buildscript {
dependencies {
// ...
classpath 'com.google.gms:google-services:4.4.2'
}
}
- Apply the plugin in
android/app/build.gradle:
apply plugin: 'com.google.gms.google-services'
-
Create/choose a Firebase project and add an Android app in the Firebase console.
-
Download
google-services.jsonand place it atandroid/app/google-services.json.
On Android 13+, you must request the runtime notification permission (POST_NOTIFICATIONS) before notifications can be shown. The SDK declares this permission in its manifest and provides requestNotificationPermission() to prompt the user.
- Add the DashX iOS SDK to your
ios/Podfile:
pod 'DashX/SDK', :git => 'https://github.com/dashxhq/dashx-ios.git', :tag => '1.5.1'
@dashx/react-native pulls in FirebaseMessaging for you — you don't need to declare it in your Podfile. Make sure your Podfile enables use_modular_headers! (or pins pod 'FirebaseMessaging', :modular_headers => true) so the Swift import resolves.
DashX iOS SDK 1.5.1 delivers pushes as native APNs alert banners with interruption-level: time-sensitive support that bypasses Focus / Reduce Interruptions / Scheduled Summary on iOS 18 / 26. For rich pushes with images, action buttons, and delivered-tracking when the app is killed, add a Notification Service Extension target to your iOS project. No extra JS-side wiring is needed — the RN bridge picks up the NSE-processed payload transparently.
The DashX iOS SDK is not published to CocoaPods trunk. You must add it from GitHub as shown above.
- Install pods:
cd ios && pod install
-
Create/choose a Firebase project and add an iOS app in the Firebase console.
-
Download
GoogleService-Info.plistand add it to your Xcode project (Copy items if needed). In the File Inspector, under Target Membership, enable your main iOS app target so the plist is bundled with the app (see Receive push notifications).
Wire up DashXNotificationHandler in your AppDelegate
The SDK ships a DashXNotificationHandler utility class with static methods you call from your own AppDelegate. This works with any AppDelegate — including Expo's ExpoAppDelegate.
import UIKit
import UserNotifications
import DashX
import DashXReactNative
import FirebaseCore
import FirebaseMessaging
@main
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate, MessagingDelegate {
var window: UIWindow?
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// Firebase must be configured before anything touches a FirebaseApp.
FirebaseApp.configure()
Messaging.messaging().delegate = self
UNUserNotificationCenter.current().delegate = self
// Unconditional registration — the actual permission prompt is driven
// from JS via `DashX.requestNotificationPermission()`. iOS still issues
// the APNs token once permission is granted, and DashX receives it via
// `handleDeviceToken` below.
application.registerForRemoteNotifications()
// Your existing React Native startup (RCTReactNativeFactory /
// RCTRootView etc.) goes here — see the default RN template for 0.77+.
return true
}
// MARK: - Device token
func application(
_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
) {
DashXNotificationHandler.handleDeviceToken(deviceToken)
}
// MARK: - Remote notifications (silent push)
func application(
_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable: Any],
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void
) {
DashXNotificationHandler.handleRemoteNotification(userInfo: userInfo, completionHandler: completionHandler)
}
// MARK: - Foreground notifications
func userNotificationCenter(
_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
) {
DashXNotificationHandler.handleForegroundNotification(notification, completionHandler: completionHandler)
}
// MARK: - Notification taps
func userNotificationCenter(
_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void
) {
let result = DashXNotificationHandler.handleNotificationResponse(response)
if let url = result.url {
// Handle deep link navigation
}
completionHandler()
}
// MARK: - FCM token
func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
guard let token = fcmToken else { return }
DashX.setFCMToken(to: token)
}
}
DashXNotificationHandler automatically:
- Bridges APNS tokens to FCM and registers with DashX (
handleDeviceToken) - Parses silent pushes, creates local notifications with action buttons and image attachments (
handleRemoteNotification) - Tracks delivered events and presents foreground notifications as banner + sound (
handleForegroundNotification) - Tracks clicked / dismissed events and extracts the notification URL for deep linking (
handleNotificationResponse) - Forwards all payloads to the JS
onPushNotificationReceivedlistener
Set FirebaseAppDelegateProxyEnabled = NO in your Info.plist
This tells Firebase not to swizzle the remote-notification methods so your AppDelegate gets the raw payloads:
<key>FirebaseAppDelegateProxyEnabled</key>
<false/>
New Architecture (optional)
If you want to opt into the New Architecture / TurboModule path:
- Android — in
android/gradle.properties:newArchEnabled=true - iOS — run
pod installwithRCT_NEW_ARCH_ENABLED=1:cd ios && RCT_NEW_ARCH_ENABLED=1 pod install
The same @dashx/react-native package and the same JavaScript API work on both architectures — no code changes needed.
Configuration
Initialize the SDK once, as early as possible (module-level initialization is recommended):
import DashX from '@dashx/react-native';
DashX.configure({
publicKey: 'your-public-key',
// baseURI: 'https://api.dashx.com/graphql', // optional
// targetEnvironment: 'production', // optional
});
Usage
Identify a user
DashX.identify({
firstName: 'John',
lastName: 'Doe',
email: 'john@example.com',
});
Set identity token
setIdentity)You can set uid without token and still use public features/resources. The second argument is the DashX Identity Token: a JWT from your backend, signed with your workspace private key (see User management). Setting token grants access to your private resources in DashX — only set it for users who need that access, and treat it as sensitive. Omit the token when the app only needs public features/resources.
Pass both when you need access to private resources:
DashX.setIdentity('user-uid', 'user-token');
Or just the UID for public features:
DashX.setIdentity('user-uid');
Reset
DashX.reset();
Track events
DashX.track('Button Clicked', { placement: 'top' });
Track screen views
DashX.screen('HomeScreen', { referrer: 'DeepLink' });
Messaging (push + preferences)
Subscribe/unsubscribe the device:
DashX.subscribe();
// `unsubscribe()` resolves with `{ success: boolean }`:
// - true → contact was found and unsubscribed.
// - false → no matching contact (anonymous UID rotated, FCM token stale,
// contact already unsubscribed). Device ends up unsubscribed
// either way; the boolean is for diagnostics.
// Promise rejects on transport / SDK-state failures (Firebase missing,
// configure() not yet called, network errors).
const { success } = await DashX.unsubscribe();
// Fire-and-forget also still works:
DashX.unsubscribe();
Request and check notification permissions:
// Request permission to show notifications
await DashX.requestNotificationPermission();
// Check current permission status without prompting
const status = await DashX.getNotificationPermissionStatus();
getNotificationPermissionStatus and requestNotificationPermission return a numeric status:
| Value | Status |
|---|---|
| 0 | Not determined |
| 1 | Denied |
| 2 | Authorized |
| 3 | Provisional (iOS only) |
| 4 | Ephemeral (iOS only) |
On Android, requestNotificationPermission uses the POST_NOTIFICATIONS runtime permission on API 33+. On older Android versions, permission is auto-granted and the method returns 2 (authorized).
For interruption-level: time-sensitive payloads to actually bypass Focus / Reduce Interruptions / Scheduled Summary, your iOS app target must have the Time Sensitive Notifications capability enabled in Xcode. Without it the option is silently dropped from the SDK's authorization request and the toggle never appears in Settings. See Receive push notifications → Time Sensitive Notifications for the setup steps.
await DashX.requestNotificationPermission({ fallbackToSettings: true });
When fallbackToSettings: true, the SDK detects platform-specific dead-ends and opens the system notification-settings page instead of firing a futile prompt:
- iOS — When the user is already determined (granted or denied), iOS treats
requestAuthorizationas a no-op. This matters most after a dashx-ios upgrade: users who granted permission under dashx-ios < 1.5.1 don't have the.timeSensitiveentitlement, so time-sensitive pushes downgrade to standard alerts and stay subject to Focus / Reduce Interruptions / Scheduled Summary filtering. iOS won't add new options to an existing grant — they need to flip the Time Sensitive Notifications toggle manually in Settings. - Android — When
POST_NOTIFICATIONSis permanently denied (Android 13+, after the user picks "Don't allow" twice),requestPermissions()auto-resolves DENIED without showing UI. The SDK detects this state and routes the user to the app's notification-settings page where they can re-enable.
Default false preserves the original ask-once behavior. Recommended pattern: surface this behind a "Notifications acting up?" affordance, or run it as a one-time prompt after the user upgrades to an app build with dashx-ios 1.5.1+. New installs get the right entitlements through the first-launch prompt and don't need this path.
Fetch and save stored preferences (requires an identified user via setIdentity with uid + JWT when private resources require it):
const prefs = await DashX.fetchStoredPreferences();
await DashX.saveStoredPreferences({
'new-post': { enabled: true },
'new-bookmark': { enabled: false },
});
Listen for incoming push notifications:
const subscription = DashX.onPushNotificationReceived((message) => {
console.log('Push notification received', message);
});
// later
subscription.remove();
DashX.onMessageReceived is a deprecated alias and will be removed in the next major version. Use onPushNotificationReceived going forward — it matches the @dashx/browser API and disambiguates from in-app notifications.
Listen for notification taps (iOS only). The callback receives a resolved NavigationAction so you can route deep links, screen navigations, and rich-landing URLs without parsing the raw payload yourself:
const subscription = DashX.onNotificationClicked(({ notification, action, actionIdentifier }) => {
// action is typed as: NavigationAction | null
// | { type: 'deepLink'; url: string }
// | { type: 'screen'; name: string; data?: Record<string, string> }
// | { type: 'richLanding'; url: string }
// | { type: 'clickAction'; action: string }
if (action?.type === 'deepLink') {
navigation.navigate(action.url);
} else if (action?.type === 'screen') {
navigation.navigate(action.name, action.data);
}
});
// later
subscription.remove();
CMS
Fetch a record by URN (the URN must be in the form {resource}/{uuid}):
const record = await DashX.fetchRecord('email/550e8400-e29b-41d4-a716-446655440000', {
preview: true,
language: 'en_US',
});
Search records:
const records = await DashX.searchRecords('email', {
filter: { identifier: { eq: 'welcome' } },
limit: 10,
preview: true,
language: 'en_US',
});
Assets
Upload a file:
const asset = await DashX.uploadAsset('/path/to/file', 'users', 'avatar');
Fetch an asset's status and URL:
const asset = await DashX.fetchAsset('asset-id');
fetchAsset is not available on Android and will reject with EUNSUPPORTED. uploadAsset works on both platforms.
iOS-specific features
The following methods are available only on iOS:
// Automatically track app lifecycle events (installed, updated, opened, backgrounded)
DashX.enableLifecycleTracking();
// Request App Tracking Transparency permission and enable IDFA collection
DashX.enableAdTracking();
// Listen for deep links / universal links
const subscription = DashX.onLinkReceived((link) => {
console.log('Link received', link);
});
// later
subscription.remove();
Track deep link opens
Record a dx_deep_link_opened analytics event and optionally forward the URL to the onLinkReceived listener:
DashX.processURL('https://example.com/promo', {
source: 'universal_link', // optional attribution source
forwardToLinkHandler: true, // default: true — forwards to onLinkReceived
});
| Option | Type | Default | Description |
|---|---|---|---|
source | string | undefined | Attribution source (e.g. "universal_link", "notification", "scene_url") |
forwardToLinkHandler | boolean | true | Whether to also forward the URL to the onLinkReceived listener |
Track notification navigation
Record a dx_notification_navigated event when the user taps a notification. Pass a navigation action describing where the tap leads:
// Deep link
DashX.trackNotificationNavigation(
{ type: 'deepLink', url: 'https://example.com/item/42' },
'notification-id-123'
);
// In-app screen
DashX.trackNotificationNavigation(
{ type: 'screen', name: 'OrderDetails', data: { orderId: '42' } },
'notification-id-456'
);
// Rich landing (in-app browser)
DashX.trackNotificationNavigation(
{ type: 'richLanding', url: 'https://promo.example.com' },
'notification-id-789'
);
// Click action
DashX.trackNotificationNavigation(
{ type: 'clickAction', action: 'OPEN_SETTINGS' },
'notification-id-000'
);
// Default (no specific action)
DashX.trackNotificationNavigation(null, 'notification-id-111');
| Action type | Fields | Description |
|---|---|---|
deepLink | url: string | Opens the URL externally |
screen | name: string, data?: Record<string, string> | In-app navigation to a named screen |
richLanding | url: string | Opens the URL in an in-app browser |
clickAction | action: string | Intent action (Android) or notification category (iOS) |
Error Handling
All promise-based methods reject with an error containing code and message properties:
try {
await DashX.fetchRecord('blog/invalid');
} catch (error) {
console.log(error.code); // 'EUNSPECIFIED'
console.log(error.message); // description of what went wrong
}
The SDK exports a DashXErrorCode enum for type-safe comparisons:
| Code | Meaning |
|---|---|
EUNSPECIFIED | An unspecified error occurred |
EUNSUPPORTED | The operation is not supported on this platform |
React Native Web
@dashx/react-native bridges to native iOS/Android modules and is not compatible with react-native-web — the native-module lookup fails at require time on web. For cross-platform apps that also deploy to the browser, use @dashx/browser on web and let Metro's platform extensions pick the right one at bundle time:
src/lib/dashx.ts // iOS + Android — re-exports @dashx/react-native
src/lib/dashx.web.ts // Web — re-exports @dashx/browser
export { default } from '@dashx/react-native';
export { default } from '@dashx/browser';
Then import from the local wrapper everywhere:
import DashX from './lib/dashx';
DashX.configure({ publicKey: '...', targetEnvironment: '...' });
Metro (and Expo Web) resolves .web.ts before .ts when bundling for web, so no custom resolver configuration is needed.
The two SDKs share the core methods (configure, identify, setIdentity, reset, track, fetchRecord, searchRecords, subscribe, unsubscribe, onPushNotificationReceived, etc.), but a few RN-only methods have no web equivalent — screen(), onLinkReceived, onNotificationClicked, requestNotificationPermission, and the iOS-specific helpers. Guard those callsites with Platform.OS or stub them in your web wrapper. Web push also requires the Firebase Web SDK and a service worker — see the JavaScript SDK docs for setup.
Firebase's messaging.onMessage listener lives in the current page's JS scope — it doesn't survive a reload. If your web flow calls subscribe() only behind an "Enable notifications" button, foreground pushes will silently stop reaching onPushNotificationReceived after a reload until the user clicks that button again (background pushes via the service worker keep working). Wire the listener at app mount with DashX.attachForegroundMessaging(messaging) — it's web-only, idempotent, and doesn't prompt for permission. On iOS and Android, @dashx/react-native handles foreground delivery through the native bridge and isn't affected.
Troubleshooting
Logging
DashX.setLogLevel(2); // 0 = off, 1 = errors, 2 = debug
Android notification channels
DashX.configure() automatically creates a default notification channel (default_dashx_notification_channel) with IMPORTANCE_HIGH, so heads-up banners work out of the box. For custom channels, create them in Application.onCreate() and have your backend send channel_id in the DashX payload.