@joryio/react-native-sdk 0.0.0-stage → 1.3.0-beta.1
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/CHANGELOG.md +5 -0
- package/LICENSE +54 -0
- package/README.md +385 -2
- package/android/build.gradle +90 -0
- package/android/src/main/java/io/joryio/reactnative/JoryioModule.kt +1188 -0
- package/android/src/main/java/io/joryio/reactnative/JoryioPackage.kt +27 -0
- package/ios/JoryioModule.m +113 -0
- package/ios/JoryioModule.swift +979 -0
- package/joryio-react-native.podspec +36 -0
- package/lib/index.d.ts +1098 -0
- package/lib/index.js +1928 -0
- package/lib/index.js.map +1 -0
- package/package.json +62 -4
- package/react-native.config.js +25 -0
- package/src/index.ts +2355 -0
package/CHANGELOG.md
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
Joryio SDK Commercial License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Joryio. All rights reserved.
|
|
4
|
+
|
|
5
|
+
This software development kit, including its source code, compiled forms,
|
|
6
|
+
documentation and samples (the "SDK"), is proprietary to Joryio
|
|
7
|
+
and is licensed, not sold.
|
|
8
|
+
|
|
9
|
+
1. Grant. Subject to the terms below, Joryio grants you a limited,
|
|
10
|
+
non-exclusive, non-transferable, non-sublicensable, revocable license to
|
|
11
|
+
use the SDK solely to integrate it into your own applications and websites
|
|
12
|
+
in order to use the Joryio service. This license is available only to
|
|
13
|
+
customers who hold an active Joryio subscription or another active written
|
|
14
|
+
agreement with Joryio, and only for as long as that subscription or
|
|
15
|
+
agreement remains in effect.
|
|
16
|
+
|
|
17
|
+
2. Distribution. You may distribute the SDK only as embedded in, and as an
|
|
18
|
+
integral part of, your own applications and websites that use the Joryio
|
|
19
|
+
service. You may not distribute the SDK on its own, as a library, or as
|
|
20
|
+
part of any toolkit, framework or product whose purpose is to let others
|
|
21
|
+
build software.
|
|
22
|
+
|
|
23
|
+
3. Restrictions. Except as expressly permitted above, you may not: (a) copy,
|
|
24
|
+
modify, publish, redistribute, sell, rent, lease or otherwise make the SDK
|
|
25
|
+
available to any third party; (b) sublicense the SDK; (c) use the SDK, or
|
|
26
|
+
any part of it, with any service other than the Joryio service, or to build
|
|
27
|
+
a product that competes with the Joryio service; or (d) remove or alter any
|
|
28
|
+
copyright, license or proprietary notice in the SDK. All rights not
|
|
29
|
+
expressly granted are reserved by Joryio.
|
|
30
|
+
|
|
31
|
+
4. Termination. This license ends automatically when your Joryio subscription
|
|
32
|
+
or agreement ends, or if you breach these terms. On termination you must
|
|
33
|
+
stop using the SDK and stop distributing new copies of applications or
|
|
34
|
+
websites that include it.
|
|
35
|
+
|
|
36
|
+
5. No warranty. The SDK is provided "as is" and "as available", without
|
|
37
|
+
warranty of any kind, express or implied, including any warranty of
|
|
38
|
+
merchantability, fitness for a particular purpose, title or
|
|
39
|
+
non-infringement, except as expressly set out in your agreement with
|
|
40
|
+
Joryio.
|
|
41
|
+
|
|
42
|
+
6. Limitation of liability. Joryio's liability arising out of or relating to
|
|
43
|
+
the SDK is limited as set out in your subscription or agreement with
|
|
44
|
+
Joryio. Where no such limitation applies, and to the maximum extent
|
|
45
|
+
permitted by law, Joryio will not be liable for any indirect, incidental,
|
|
46
|
+
special, consequential or punitive damages, or for any loss of profits,
|
|
47
|
+
revenue, data or goodwill.
|
|
48
|
+
|
|
49
|
+
7. Governing terms. Your use of the SDK is also governed by the Joryio Terms
|
|
50
|
+
of Service (https://joryio.com/terms) and by your agreement with Joryio. If
|
|
51
|
+
this license conflicts with your written agreement with Joryio, your
|
|
52
|
+
written agreement prevails.
|
|
53
|
+
|
|
54
|
+
Questions about this license: support@joryio.com
|
package/README.md
CHANGED
|
@@ -1,3 +1,386 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Joryio React Native SDK
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The Joryio SDK for React Native apps: one TypeScript API over the native Joryio iOS and Android SDKs for identity, event tracking, push notifications and in-app messages.
|
|
4
|
+
|
|
5
|
+
Every call forwards to the native SDK, so batching, offline queueing, retries and in-app rendering behave exactly as in a native integration. For platform details, see the [iOS SDK](https://github.com/joryio/joryio-ios-sdk) and [Android SDK](https://github.com/joryio/joryio-android-sdk).
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Event tracking** and screen views, batched and queued offline by the native SDKs
|
|
10
|
+
- **Identity** - anonymous ID out of the box, `identify`, labelled aliases with `addAlias`
|
|
11
|
+
- **User attributes** - set, increment and unset
|
|
12
|
+
- **Push notifications** - register APNs / FCM tokens, delivery receipts, click tracking, permission prompts
|
|
13
|
+
- **In-app messages** drawn by the native SDKs with no JavaScript, or rendered by your own React components
|
|
14
|
+
- **E-commerce helpers** that send the standard event names Joryio reports on
|
|
15
|
+
- **Subscription preferences** per channel and subscription group
|
|
16
|
+
- **Privacy controls** - opt-out and tracking consent
|
|
17
|
+
- **SDK authentication** (optional) - a customer-minted JWT on every ingest request
|
|
18
|
+
- **TypeScript** types included
|
|
19
|
+
|
|
20
|
+
## Requirements
|
|
21
|
+
|
|
22
|
+
- React Native 0.72 or newer. Old and New Architecture both work; on the New Architecture the module runs through React Native's interop layer for legacy modules, which is on by default
|
|
23
|
+
- iOS 14.0+
|
|
24
|
+
- Android 6.0 (API level 23)+
|
|
25
|
+
- A Joryio workspace with an **iOS app** and an **Android app**, each with its own SDK key (`jry_sdk_ios_...`, `jry_sdk_android_...`) from **Settings → Apps** in the dashboard
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install @joryio/react-native-sdk
|
|
31
|
+
# or
|
|
32
|
+
yarn add @joryio/react-native-sdk
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The package is autolinked on both platforms. Do not also register `JoryioPackage` by hand.
|
|
36
|
+
|
|
37
|
+
### iOS
|
|
38
|
+
|
|
39
|
+
The native dependency is the `Joryio/UI` pod from CocoaPods (tracking, identity, push and in-app display). The SDK needs iOS 14.0, while some React Native versions default lower, so raise the platform in `ios/Podfile`:
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
platform :ios, [min_ios_version_supported, '14.0'].max
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Then:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
cd ios && pod install
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Android
|
|
52
|
+
|
|
53
|
+
Nothing to add. The bridge depends on `io.joryio:joryio-android-ui` from Maven Central, at the same version as this package; React Native projects already list `mavenCentral()` in their repositories.
|
|
54
|
+
|
|
55
|
+
### Expo
|
|
56
|
+
|
|
57
|
+
The SDK is a native module, so it does not run in Expo Go. Use a development build (`npx expo prebuild` or EAS Build). Expo's default iOS deployment target can be lower than the 14.0 the SDK needs; raise it with `expo-build-properties` in `app.json`:
|
|
58
|
+
|
|
59
|
+
```json
|
|
60
|
+
{
|
|
61
|
+
"expo": {
|
|
62
|
+
"plugins": [["expo-build-properties", { "ios": { "deploymentTarget": "14.0" } }]]
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Building the native SDKs from source (optional)
|
|
68
|
+
|
|
69
|
+
If you build the native SDKs from a checkout instead of the registries:
|
|
70
|
+
|
|
71
|
+
- **Android** - include both modules in `android/settings.gradle` under these names, and the bridge uses them instead of Maven Central:
|
|
72
|
+
|
|
73
|
+
```groovy
|
|
74
|
+
include ':joryio-sdk'
|
|
75
|
+
project(':joryio-sdk').projectDir = new File(rootProject.projectDir, '../path/to/joryio-android-sdk/joryio')
|
|
76
|
+
include ':joryio-sdk-ui'
|
|
77
|
+
project(':joryio-sdk-ui').projectDir = new File(rootProject.projectDir, '../path/to/joryio-android-sdk/joryio-ui')
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
- **iOS** - point CocoaPods at the checkout in `ios/Podfile`: `pod 'Joryio', :path => '../path/to/joryio-ios-sdk'`. The bridge pins the `Joryio/UI` pod to its own version exactly, so the checkout must be at the same version.
|
|
81
|
+
|
|
82
|
+
If you link this package from a source checkout rather than installing it from npm, run `npm run build` in it once: `main` points at the compiled `lib/`, which is built on publish and is not in the repository.
|
|
83
|
+
|
|
84
|
+
## Quick start
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
import { Platform } from 'react-native';
|
|
88
|
+
import Joryio from '@joryio/react-native-sdk';
|
|
89
|
+
|
|
90
|
+
// Once, at app start (e.g. in App.tsx)
|
|
91
|
+
await Joryio.initialize(
|
|
92
|
+
Platform.select({
|
|
93
|
+
ios: 'jry_sdk_ios_YOUR_IOS_KEY',
|
|
94
|
+
android: 'jry_sdk_android_YOUR_ANDROID_KEY',
|
|
95
|
+
})!,
|
|
96
|
+
'api-eu1.joryio.com', // your workspace's API host
|
|
97
|
+
{
|
|
98
|
+
enableDebug: __DEV__,
|
|
99
|
+
},
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
// Track events
|
|
103
|
+
Joryio.track('Product Added', { productId: '123', price: 29.99 });
|
|
104
|
+
Joryio.trackScreen('ProductDetail', { productId: '123' });
|
|
105
|
+
|
|
106
|
+
// Identify users
|
|
107
|
+
Joryio.identify('user-123');
|
|
108
|
+
Joryio.setAttributes({ firstName: 'Jane', plan: 'premium' });
|
|
109
|
+
Joryio.incrementAttribute('loginCount', 1);
|
|
110
|
+
|
|
111
|
+
// Attach an id from another system - works before identify() too
|
|
112
|
+
Joryio.addAlias('crm_id', 'C-1042');
|
|
113
|
+
|
|
114
|
+
// Logout
|
|
115
|
+
Joryio.reset();
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Calls made before `initialize()` resolves are held and sent once it does (at most 1,000, the same cap as the native SDKs; past that the oldest are dropped, with a warning - data calls first, and an unsubscribe, `unregisterPush()` or a privacy decision only when nothing else is left). If `initialize()` fails, the SDK logs why and keeps holding calls the same way until a later `initialize()` succeeds - you can call it again with corrected values, your `onInAppMessage` and `onSdkAuthenticationError` listeners and any unsubscribes made meanwhile are kept, and nothing throws into your app. Use the API host shown for your workspace in the dashboard; a bare host is fine.
|
|
119
|
+
|
|
120
|
+
Each platform has its own SDK key: iOS keys start with `jry_sdk_ios_`, Android keys with `jry_sdk_android_`. A key the platform refuses makes `initialize()` log a warning and leaves the SDK off until you call it again with the right key. Two overlapping `initialize()` calls - React StrictMode runs effects twice - share one start.
|
|
121
|
+
|
|
122
|
+
`null` and `undefined` never crash your app: a `null` configuration value means the default, a call missing a required argument (an event name, a user id, a token) logs a warning and does nothing, and a `null` attribute value unsets the attribute. `NaN` and `Infinity` are never sent: an attribute value or event property holding one is left out with a warning, and the rest of the call goes ahead (`setAttribute(key, NaN)` is ignored rather than unsetting the attribute). A callback you pass to `onInAppMessage` or `onSdkAuthenticationError` that throws, or returns a promise that rejects, is caught and logged - it never crashes your app or stops other listeners.
|
|
123
|
+
|
|
124
|
+
On Android the bridge passes the current Activity to the native SDK at `initialize()`, so in-app messages and `requestPushPermissionAtLaunch` work in the very first session, not only after the app has been backgrounded once.
|
|
125
|
+
|
|
126
|
+
## Configuration options
|
|
127
|
+
|
|
128
|
+
All options are optional:
|
|
129
|
+
|
|
130
|
+
| Option | Type | Default | Description |
|
|
131
|
+
|--------|------|---------|-------------|
|
|
132
|
+
| `enableDebug` | `boolean` | `false` | Verbose native logging; also enables the testing-only APIs |
|
|
133
|
+
| `logLevel` | `'debug' \| 'info' \| 'warn' \| 'error'` | follows `enableDebug` | Override the native log level |
|
|
134
|
+
| `batchSize` | `number` | `50` | Events per batch |
|
|
135
|
+
| `flushInterval` | `number` | `5000` | Milliseconds between automatic flushes |
|
|
136
|
+
| `sendImmediately` | `boolean` | `false` | Send each event immediately |
|
|
137
|
+
| `maxQueueSize` | `number` | `1000` | Most events held while sends fail |
|
|
138
|
+
| `requestTimeout` | `number` | `10000` | Per-request timeout in ms |
|
|
139
|
+
| `sessionTimeout` | `number` | `1800000` | Milliseconds of inactivity before a new session |
|
|
140
|
+
| `trackSessionStart` | `boolean` | `true` | Track "Session Start" automatically |
|
|
141
|
+
| `userId` | `string` | - | Start with a known user ID |
|
|
142
|
+
| `anonymousId` | `string` | generated | Use your own anonymous ID |
|
|
143
|
+
| `requestPushPermissionAtLaunch` | `boolean` | `false` | Ask for notification permission at startup. Prefer asking at a moment the user understands |
|
|
144
|
+
| `allowHtmlJsInAppMessages` | `boolean` | `false` | Allow HTML in-app messages (see below) |
|
|
145
|
+
| `enableSdkAuthentication` | `boolean` | `false` | Attach a customer-minted JWT to every ingest request |
|
|
146
|
+
| `sdkAuthenticationToken` | `string` | - | Initial JWT |
|
|
147
|
+
| `trackingConsent` | `'granted' \| 'denied' \| 'pending'` | unset | Consent at startup. Leave it unset unless your consent banner has an answer - see [Privacy controls](#privacy-controls) |
|
|
148
|
+
| `optOut` | `boolean` | `false` | Start opted out of tracking |
|
|
149
|
+
| `notificationChannelName` | `string` | `"Notifications"` | Android only: the notification channel name people see in system settings (Android 8+). Pass it localized. Ignored on iOS |
|
|
150
|
+
|
|
151
|
+
## Push notifications
|
|
152
|
+
|
|
153
|
+
Upload your push credentials in the dashboard first: [APNs for iOS](https://docs.joryio.com/developers/apps/apns-credentials), [Firebase for Android](https://docs.joryio.com/developers/apps/fcm-credentials). Joryio sends iOS push through APNs, so on iOS register the **APNs** token, not the FCM one.
|
|
154
|
+
|
|
155
|
+
Ask for permission when the user has a reason to say yes - the system prompt is one-shot:
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
if ((await Joryio.getPushPermissionStatus()) === 'notDetermined') {
|
|
159
|
+
// show your own primer first, then:
|
|
160
|
+
const status = await Joryio.requestPushPermission(); // 'granted' | 'denied' | 'notDetermined' | 'unavailable'
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Registering the token (example with React Native Firebase)
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
import { Platform } from 'react-native';
|
|
168
|
+
import messaging from '@react-native-firebase/messaging';
|
|
169
|
+
|
|
170
|
+
const token = Platform.OS === 'ios'
|
|
171
|
+
? await messaging().getAPNSToken() // APNs device token (hex)
|
|
172
|
+
: await messaging().getToken(); // FCM token
|
|
173
|
+
|
|
174
|
+
if (token) {
|
|
175
|
+
const result = await Joryio.registerPushToken(token);
|
|
176
|
+
if (!result.success && result.isRetryable) {
|
|
177
|
+
// transient failure (offline, DNS, 5xx): register again when the network is back
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
messaging().onTokenRefresh((newToken) => {
|
|
182
|
+
// onTokenRefresh delivers the FCM token - on iOS too, at every app start.
|
|
183
|
+
// Register it on Android only; on iOS the APNs token above is the one.
|
|
184
|
+
if (Platform.OS === 'android') Joryio.registerPushToken(newToken);
|
|
185
|
+
});
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
On iOS register only the APNs device token. React Native Firebase's `onTokenRefresh` hands over the **FCM** token on iOS as well, at every app start, and registered it would replace the device's working APNs token with one APNs refuses - so register it on Android only, as above. On iOS `registerPushToken` refuses a token that is not an APNs token (hex) with `success: false` and `isRetryable: false`.
|
|
189
|
+
|
|
190
|
+
If the user has turned push off and you called `unregisterPush()`, do not register tokens from automatic callbacks (`onTokenRefresh`, a `getToken()` at launch) until they turn it back on: `registerPushToken()` is the explicit "push on" and lifts the unregister. Check your own push setting before registering.
|
|
191
|
+
|
|
192
|
+
`registerPushToken` resolves with `{ success, message, isUnauthorized, isRetryable }` and never rejects. `isRetryable` is `true` when `initialize()` simply has not finished yet, and `false` after it failed: register the token again once a later `initialize()` succeeds.
|
|
193
|
+
|
|
194
|
+
`requestPushPermission()` behaves the same on both platforms:
|
|
195
|
+
|
|
196
|
+
- It always settles. On Android, if the Activity is recreated while the prompt is showing (a rotation, a theme change), it resolves with the status as it stands once your app is back in front.
|
|
197
|
+
- When the user has opted out of tracking, the prompt is still shown and the result returned - whether your app may notify is the user's choice in the OS - but nothing is registered with or reported to Joryio while they stay opted out.
|
|
198
|
+
|
|
199
|
+
### Delivery receipts and clicks
|
|
200
|
+
|
|
201
|
+
When your own messaging library handles notifications, report them with the `trackingId` from the message's data payload:
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
messaging().onMessage(async (msg) => {
|
|
205
|
+
if (msg.data?.trackingId) Joryio.reportPushDelivered(String(msg.data.trackingId));
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
messaging().onNotificationOpenedApp((msg) => {
|
|
209
|
+
if (msg.data?.trackingId) Joryio.trackPushClick(String(msg.data.trackingId));
|
|
210
|
+
});
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
A Joryio push carries the same data keys on every platform: `joryio` (always `"true"`), `trackingId`, `jry_campaign_id` (campaign or journey id), `jry_deep_link` (what a tap opens) and `jry_image_url`. On Android the native SDK also records the tap itself when your app is opened from a notification; reporting it as well is harmless, a click is counted once per notification.
|
|
214
|
+
|
|
215
|
+
Also available: `isPushEnabled()` and `unregisterPush()`, which stops pushes to this device and stays in effect across restarts until you call `registerPushToken()` again.
|
|
216
|
+
|
|
217
|
+
## In-app messaging
|
|
218
|
+
|
|
219
|
+
**Messages display on their own.** Install the SDK, send a campaign, and the native SDK draws it with your app's fonts, colours and dark mode. Impressions, clicks and dismissals are tracked for you.
|
|
220
|
+
|
|
221
|
+
HTML messages are off by default, because they run inside your app; pass `allowHtmlJsInAppMessages: true` to `initialize` to opt in. Native messages display either way.
|
|
222
|
+
|
|
223
|
+
### Rendering messages in React
|
|
224
|
+
|
|
225
|
+
Subscribe to draw messages yourself. Subscribing takes over from the native rendering, so you will not get two copies, and removing your last subscription hands rendering back to the native SDK, so no message is lost after you unsubscribe:
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
const unsubscribe = Joryio.onInAppMessage(
|
|
229
|
+
(message) => {
|
|
230
|
+
if (message.kind === 'native') {
|
|
231
|
+
// message.title, message.body, message.imageUrl, message.buttons, message.style
|
|
232
|
+
// Plain text: put it in a <Text>, never in a WebView.
|
|
233
|
+
} else {
|
|
234
|
+
// message.html + message.css - render in a WebView
|
|
235
|
+
}
|
|
236
|
+
Joryio.trackInAppImpression(message.id, 'displayed');
|
|
237
|
+
// ... then 'clicked' (with the button's id) when a button is tapped, and
|
|
238
|
+
// 'dismissed' whenever the message leaves the screen - after a click too:
|
|
239
|
+
// Joryio.trackInAppImpression(message.id, 'clicked', button.id);
|
|
240
|
+
// Joryio.trackInAppImpression(message.id, 'dismissed');
|
|
241
|
+
},
|
|
242
|
+
{ capabilities: ['content.native'] }, // add 'content.html' only if you render HTML
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
// Later
|
|
246
|
+
unsubscribe();
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
You must report what happened with `trackInAppImpression(campaignId, action, elementId?)`. Report `'displayed'` once, when the message appears: that report records the impression and is what counts toward the campaign's frequency caps - handing the message to your code does neither, on both platforms. For `'clicked'`, pass the id of the tapped button or link as `elementId` so reports can say which one. A message you never report counts toward no cap and shows no displays in reporting.
|
|
250
|
+
|
|
251
|
+
Report `'dismissed'` whenever the message leaves the screen, including after a click. Until then the message holds the display slot: on iOS no other in-app message is shown, and on Android the same campaign is not shown again. If `'dismissed'` never comes, the SDK frees the slot after 5 minutes. When you render an HTML message in a WebView, escape `</` in `message.css` (`message.css.replace(/<\//g, '<\\/')`) so the campaign's CSS cannot close your `<style>` tag.
|
|
252
|
+
|
|
253
|
+
A form inside an HTML message you render yourself is submitted with `submitInAppForm(campaignId, values)`, which resolves `{ ok, message?, redirect?, errors? }` and never rejects.
|
|
254
|
+
|
|
255
|
+
Native button actions are `dismiss`, `url` (open `url` in the browser), `deep_link` (route to `url` inside your app) and `request_push_permission` - a push primer's "yes" button: close the message and call `Joryio.requestPushPermission()`.
|
|
256
|
+
|
|
257
|
+
An empty `capabilities` list means the same as leaving it out (not declared) on both platforms. `enableInAppMessages(capabilities?)` and `disableInAppMessages()` are the lower-level pair `onInAppMessage` uses; call them yourself only if you listen for the `JoryioInAppMessage` event another way.
|
|
258
|
+
|
|
259
|
+
`syncInAppCampaigns()` fetches campaigns now. `resetDisplayedCampaigns()` is a testing helper that only runs when `enableDebug` is on. When JavaScript reloads, the bridge hands rendering back to the native SDK until your code subscribes again.
|
|
260
|
+
|
|
261
|
+
## E-commerce
|
|
262
|
+
|
|
263
|
+
```typescript
|
|
264
|
+
import Joryio from '@joryio/react-native-sdk';
|
|
265
|
+
|
|
266
|
+
Joryio.ecommerce.viewProduct({ productId: 'SKU123', name: 'Blue Shirt', price: 29.99 });
|
|
267
|
+
Joryio.ecommerce.addToCart({ productId: 'SKU123', name: 'Blue Shirt', price: 29.99, quantity: 1 });
|
|
268
|
+
Joryio.ecommerce.purchase({
|
|
269
|
+
orderId: 'ORD123',
|
|
270
|
+
items: [{ productId: 'SKU123', name: 'Blue Shirt', price: 29.99, quantity: 1 }],
|
|
271
|
+
value: 29.99,
|
|
272
|
+
currency: 'USD',
|
|
273
|
+
});
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
`purchase` sends an `Order Completed` event with `orderId` and `total` (the order's `value`) - the keys revenue reporting and attribution read. A hand-typed `track('Order Completed', { order_id, value })` records a $0 order with no order id; if you track a purchase yourself, use `orderId` and `total`.
|
|
277
|
+
|
|
278
|
+
A call missing a required argument - a `purchase` without `orderId`, `items` or a numeric `value`, a product without `productId`, a cart without `items` - logs a warning and sends nothing, so it never records a $0 order.
|
|
279
|
+
|
|
280
|
+
Also available: `viewCategory`, `search`, `removeFromCart`, `cartViewed`, `updateCart`, `startCheckout`, `addPaymentInfo`, `orderFulfilled`, `orderDelivered`, `orderCancelled`, `orderRefunded`, `addToWishlist`, `removeFromWishlist`, `shareProduct`, `applyCoupon`, `removeCoupon` and `submitReview`. See [E-commerce tracking](https://docs.joryio.com/developers/sdk/ecommerce-tracking).
|
|
281
|
+
|
|
282
|
+
## Subscription preferences
|
|
283
|
+
|
|
284
|
+
Whether a person may be **messaged** on a channel - separate from `optOut()`, which is whether they may be **tracked**.
|
|
285
|
+
|
|
286
|
+
```typescript
|
|
287
|
+
Joryio.setSubscription('email', 'unsubscribed');
|
|
288
|
+
Joryio.addToSubscriptionGroup('list_abc123', 'email');
|
|
289
|
+
Joryio.removeFromSubscriptionGroup('list_abc123', 'email');
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
Channels: `email`, `sms`, `whatsapp`, `push`, `viber`. Statuses: `optedIn`, `subscribed`, `unsubscribed`.
|
|
293
|
+
|
|
294
|
+
## Privacy controls
|
|
295
|
+
|
|
296
|
+
Two separate switches decide whether the SDK collects anything, and any "no" wins:
|
|
297
|
+
|
|
298
|
+
- **Consent** - the answer from your consent banner: `'granted'`, `'denied'` or `'pending'`. Set with `trackingConsent` at `initialize()` or with `updateConsent()`.
|
|
299
|
+
- **Opt-out** - the person's own choice. Set with `optOut()` and lifted only by `optIn()`.
|
|
300
|
+
|
|
301
|
+
The SDK collects only while consent is `granted` and the person has not opted out. Granting consent never cancels an opt-out, and opting out never changes the stored consent.
|
|
302
|
+
|
|
303
|
+
```typescript
|
|
304
|
+
Joryio.optOut();
|
|
305
|
+
Joryio.optIn();
|
|
306
|
+
const optedOut = await Joryio.isUserOptedOut(); // the opt-out switch only
|
|
307
|
+
const consent = await Joryio.getTrackingConsent(); // 'granted' | 'denied' | 'pending'
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
With nothing decided and nothing stored, `getTrackingConsent()` answers `'granted'` - the state the SDK runs in - on both platforms.
|
|
311
|
+
|
|
312
|
+
`wipeData()` erases everything the device holds for the current person - identity, queued events, pending writes, in-app state, session, device id and push token - and unregisters the token on the server. The opt-out is kept. Push stays off until `registerPushToken()` or a permission grant.
|
|
313
|
+
|
|
314
|
+
An opt-out is stored on the device and survives restarts. To decide before anything is sent, pass consent to `initialize()`:
|
|
315
|
+
|
|
316
|
+
```typescript
|
|
317
|
+
await Joryio.initialize(sdkKey, apiHost, { trackingConsent: 'pending' });
|
|
318
|
+
|
|
319
|
+
// later, from your consent prompt
|
|
320
|
+
Joryio.updateConsent('granted'); // 'granted' | 'denied' | 'pending'
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
- Leave `trackingConsent` unset unless your banner has an answer for this launch. Unset, with nothing stored, behaves as `granted`; a `denied` stored by an earlier launch stays in force.
|
|
324
|
+
- `denied` is stored on the device and survives restarts. Only `updateConsent('granted')` or an explicit `trackingConsent: 'granted'` lifts it.
|
|
325
|
+
- `pending` applies to this launch only and never replaces a stored `denied`. While consent is `pending` nothing is sent: events and attribute changes wait on the device (up to `maxQueueSize`), and there is no session start, in-app sync or push registration. `updateConsent('granted')` sends what waited; `updateConsent('denied')` discards it.
|
|
326
|
+
- `optOut: true` at `initialize()` starts the SDK opted out; `optOut: false` does not undo an earlier `optOut()` - call `optIn()`.
|
|
327
|
+
- If your app already knows the answer when it starts - a banner choice it stored itself, for example - pass it as `trackingConsent` to `initialize()` instead of calling `updateConsent()` after it, so the SDK starts with that answer in force. Letter case does not matter (`'Denied'` is read as `'denied'`); an unknown value is ignored with a warning and consent stays unset.
|
|
328
|
+
|
|
329
|
+
`optOut()`, `optIn()`, `updateConsent()` and `wipeData()` can be called before `initialize()`, and they are applied first: no session start, in-app sync or push registration goes out before them. Calls made earlier that collect data - events, attributes, aliases, subscription changes - are dropped by an `optOut()`, `updateConsent('denied')` or `wipeData()` that follows them. Calls like these made while opted out or denied - before `initialize()` too, whether the decision was made in this launch or stored by an earlier one - are dropped as well, and a later `optIn()` or `updateConsent('granted')` does not send them. `identify` is dropped only by `wipeData()`. An earlier `setSdkAuthenticationToken()` is never carried over to the next visitor: on iOS and Android alike the wipe keeps it only for the erased person's own withdrawals, or drops it. After an `optOut()` or a denial the user id is kept on the device, unsent, only so that an unsubscribe reaches the right person. Withdrawals are the exception: `setSubscription(channel, 'unsubscribed')`, `removeFromSubscriptionGroup` and `unregisterPush()` made earlier are kept and sent.
|
|
330
|
+
|
|
331
|
+
`setAttributes({ nickname: null })` and `setAttribute('nickname', null)` unset the attribute, the same as `unsetAttribute('nickname')`.
|
|
332
|
+
|
|
333
|
+
## SDK authentication (optional)
|
|
334
|
+
|
|
335
|
+
```typescript
|
|
336
|
+
await Joryio.initialize(sdkKey, 'api-eu1.joryio.com', {
|
|
337
|
+
enableSdkAuthentication: true,
|
|
338
|
+
sdkAuthenticationToken: initialToken,
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
const stop = Joryio.onSdkAuthenticationError(async (error) => {
|
|
342
|
+
// error.reason: 'missing' | 'invalid' | 'expired' | 'sub_mismatch' | 'unknown'
|
|
343
|
+
// error.refreshExhausted: true once the SDK has stopped retrying this request
|
|
344
|
+
Joryio.setSdkAuthenticationToken(await fetchFreshToken());
|
|
345
|
+
});
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
With SDK authentication on, a request that carries a user id must carry a JWT your backend minted for that user; calls for anonymous visitors are accepted without one.
|
|
349
|
+
|
|
350
|
+
## API reference
|
|
351
|
+
|
|
352
|
+
| Area | Methods |
|
|
353
|
+
|------|---------|
|
|
354
|
+
| Setup | `initialize(sdkKey, apiHost, config?)` |
|
|
355
|
+
| Events | `track(eventName, properties?)`, `trackScreen(screenName, properties?)`, `flush()` |
|
|
356
|
+
| Identity | `identify(userId)`, `addAlias(aliasLabel, aliasName)`, `reset()`, `getUserId()`, `getAnonymousId()`, `getSessionId()` |
|
|
357
|
+
| Attributes | `setAttributes(attributes)`, `setAttribute(key, value)`, `incrementAttribute(key, by?)`, `unsetAttribute(key)` |
|
|
358
|
+
| Push | `registerPushToken(token)`, `unregisterPush()`, `isPushEnabled()`, `getPushPermissionStatus()`, `requestPushPermission()`, `reportPushDelivered(trackingId)`, `trackPushClick(trackingId)` |
|
|
359
|
+
| In-app | `onInAppMessage(callback, options?)`, `enableInAppMessages(capabilities?)`, `disableInAppMessages()`, `trackInAppImpression(campaignId, action, elementId?)`, `submitInAppForm(campaignId, values)`, `syncInAppCampaigns()`, `resetDisplayedCampaigns()` |
|
|
360
|
+
| Consent | `optOut()`, `optIn()`, `isUserOptedOut()`, `getTrackingConsent()`, `updateConsent(consent)`, `wipeData()` |
|
|
361
|
+
| Subscriptions | `setSubscription(channel, status)`, `addToSubscriptionGroup(groupId, channel)`, `removeFromSubscriptionGroup(groupId, channel)` |
|
|
362
|
+
| SDK authentication | `setSdkAuthenticationToken(token)`, `onSdkAuthenticationError(callback)` |
|
|
363
|
+
| E-commerce | `Joryio.ecommerce.*` (also exported as `ecommerce`) |
|
|
364
|
+
| Diagnostics | `getDiagnostics()` - the API endpoint in use and the last transport error |
|
|
365
|
+
|
|
366
|
+
The getters return promises, wait for `initialize()` (up to 5 seconds) and never reject: without a linked native module or a completed `initialize()` they resolve a default (`false`, `null`, `''` or `'unavailable'`). After a failed `initialize()` they resolve that default at once. `isUserOptedOut()` and `getTrackingConsent()` are different: while `initialize()` is running they wait for it, and otherwise they answer at once from what is stored on the device - before `initialize()` and after a failed one too - so an opt-out or a `denied` from an earlier launch reads as such straight away.
|
|
367
|
+
|
|
368
|
+
## Testing with Jest
|
|
369
|
+
|
|
370
|
+
The package's `main` is compiled JavaScript, so your Jest setup needs no `transformIgnorePatterns` entry for it. Mock the native module the way you mock any other, or leave it unmocked: without `NativeModules.JoryioModule` every call is a no-op.
|
|
371
|
+
|
|
372
|
+
## Documentation
|
|
373
|
+
|
|
374
|
+
- [React Native SDK guide](https://docs.joryio.com/developers/sdk/react-native-sdk)
|
|
375
|
+
- [iOS SDK guide](https://docs.joryio.com/developers/sdk/ios-sdk) and [Android SDK guide](https://docs.joryio.com/developers/sdk/android-sdk) for platform details
|
|
376
|
+
- [Tracking events](https://docs.joryio.com/developers/sdk/tracking-events)
|
|
377
|
+
- [Changelog](./CHANGELOG.md)
|
|
378
|
+
|
|
379
|
+
## Support
|
|
380
|
+
|
|
381
|
+
- Issues: https://github.com/joryio/joryio-react-native-sdk/issues
|
|
382
|
+
- Email: support@joryio.com
|
|
383
|
+
|
|
384
|
+
## License
|
|
385
|
+
|
|
386
|
+
Commercial - see [LICENSE](./LICENSE). Use of the SDK requires an active Joryio subscription or agreement.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
buildscript {
|
|
2
|
+
// The host app's Kotlin when it declares one (every React Native template
|
|
3
|
+
// sets rootProject.ext.kotlinVersion), so the bridge compiles with the same
|
|
4
|
+
// compiler as the React Native it links against: react-android 0.84 is
|
|
5
|
+
// built with Kotlin 2.1, whose metadata a 1.9 compiler cannot read.
|
|
6
|
+
ext.kotlin_version = rootProject.ext.has('kotlinVersion') ? rootProject.ext.get('kotlinVersion') : '1.9.24'
|
|
7
|
+
repositories {
|
|
8
|
+
google()
|
|
9
|
+
mavenCentral()
|
|
10
|
+
}
|
|
11
|
+
dependencies {
|
|
12
|
+
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
apply plugin: 'com.android.library'
|
|
17
|
+
apply plugin: 'kotlin-android'
|
|
18
|
+
|
|
19
|
+
// This package's version, which is also the version of the native SDK it wraps:
|
|
20
|
+
// the SDKs release in lockstep (sdk-version-parity.spec.ts).
|
|
21
|
+
def joryioSdkVersion = new groovy.json.JsonSlurper().parse(file('../package.json')).version
|
|
22
|
+
|
|
23
|
+
def safeExtGet(prop, fallback) {
|
|
24
|
+
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
android {
|
|
28
|
+
namespace "io.joryio.reactnative"
|
|
29
|
+
// The app's compileSdk when it sets one, as React Native libraries do.
|
|
30
|
+
compileSdkVersion safeExtGet('compileSdkVersion', 34)
|
|
31
|
+
|
|
32
|
+
defaultConfig {
|
|
33
|
+
// The native SDK this wraps (packages/sdk-android) declares minSdk 23,
|
|
34
|
+
// and manifest merging takes the MAX across libraries - so the effective
|
|
35
|
+
// floor for any consumer is 23 whatever is written here. This used to
|
|
36
|
+
// claim the native SDK was 21 (audit 2026-09-26, R24); it never was.
|
|
37
|
+
//
|
|
38
|
+
// Kept below the native floor on purpose. A thin bridge must never be
|
|
39
|
+
// the library that RAISES it: 24 here silently made every React Native
|
|
40
|
+
// consumer minSdk 24 and broke the stock RN 0.75 template (which
|
|
41
|
+
// generates 23) with
|
|
42
|
+
// uses-sdk:minSdkVersion 23 cannot be smaller than version 24
|
|
43
|
+
// declared in library [:joryio_react-native-sdk]
|
|
44
|
+
// The bridge itself uses no API above 21 - it only forwards calls.
|
|
45
|
+
minSdkVersion 21
|
|
46
|
+
targetSdkVersion safeExtGet('targetSdkVersion', 34)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
sourceSets {
|
|
50
|
+
main {
|
|
51
|
+
java.srcDirs = ['src/main/java']
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
repositories {
|
|
57
|
+
google()
|
|
58
|
+
mavenCentral()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
dependencies {
|
|
62
|
+
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
|
63
|
+
implementation "com.facebook.react:react-android:+"
|
|
64
|
+
// BASE + UI, deliberately. An RN app that installs this package and writes
|
|
65
|
+
// no JavaScript still sees in-app messages, rendered by the native views.
|
|
66
|
+
// "install the SDK, send a campaign, see it appear" is the thing that
|
|
67
|
+
// decides whether an integration succeeds.
|
|
68
|
+
//
|
|
69
|
+
// The opt-out already works and stays: calling Joryio.onInAppMessage() from
|
|
70
|
+
// JS invokes enableInAppMessages(), which sets the native callback and
|
|
71
|
+
// flips hostHandlesDisplay, so the SDK stops drawing and hands messages to
|
|
72
|
+
// React. An app that wants to render in React gets to; an app that does
|
|
73
|
+
// nothing is not left with a silently dead channel.
|
|
74
|
+
//
|
|
75
|
+
// WHERE the native SDK comes from. A published install resolves it from
|
|
76
|
+
// Maven Central at this package's own version - `-ui` pulls the base
|
|
77
|
+
// artifact at the same exact version, so one coordinate is the whole
|
|
78
|
+
// dependency. An app that builds the native SDK from source instead
|
|
79
|
+
// includes it as the `:joryio-sdk` and `:joryio-sdk-ui` Gradle projects in
|
|
80
|
+
// its settings.gradle, and those win when present. Before 2026-10-06 ONLY
|
|
81
|
+
// the source form existed, which a public npm install cannot satisfy: the
|
|
82
|
+
// package does not ship the native sources, so the build failed with
|
|
83
|
+
// "Project with path ':joryio-sdk' could not be found".
|
|
84
|
+
if (rootProject.findProject(':joryio-sdk') != null) {
|
|
85
|
+
implementation project(':joryio-sdk')
|
|
86
|
+
implementation project(':joryio-sdk-ui')
|
|
87
|
+
} else {
|
|
88
|
+
implementation "io.joryio:joryio-android-ui:${joryioSdkVersion}"
|
|
89
|
+
}
|
|
90
|
+
}
|