@sendoracloud/sdk-react-native 0.10.3 → 0.10.4
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/README.md +43 -1
- package/dist/index.cjs +35 -10
- package/dist/index.d.cts +17 -3
- package/dist/index.d.ts +17 -3
- package/dist/index.js +35 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,14 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
Official React Native + Expo SDK for [SendoraCloud](https://sendoracloud.com). Works in Expo Go, managed dev clients, and bare React Native apps.
|
|
4
4
|
|
|
5
|
+
> 📖 **First time on React Native?** Read the [5-minute quickstart](https://sendoracloud.com/docs/quickstart-react-native) — it covers the Hermes CSPRNG polyfill, anonymous-first auth, push token receipts, and the demo end-to-end push send.
|
|
6
|
+
|
|
5
7
|
## Install
|
|
6
8
|
|
|
7
9
|
```bash
|
|
8
10
|
npx expo install @sendoracloud/sdk-react-native @react-native-async-storage/async-storage
|
|
11
|
+
npm install react-native-get-random-values
|
|
9
12
|
```
|
|
10
13
|
|
|
11
14
|
`@react-native-async-storage/async-storage` is a required peer dependency — used to persist the anonymous device id across app restarts.
|
|
12
15
|
|
|
16
|
+
`react-native-get-random-values` is a required runtime polyfill — Hermes (and JavaScriptCore) doesn't expose `crypto.getRandomValues` reliably, and Sendora refuses to mint anonymous IDs from `Math.random`. Add this as the **first import** in your entry file (`index.js` or `index.tsx`):
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
// MUST be the first import in your entry file
|
|
20
|
+
import "react-native-get-random-values";
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
If you forget, `init()` throws with a paste-ready remediation block — it never fails silently.
|
|
24
|
+
|
|
13
25
|
## Quick start
|
|
14
26
|
|
|
15
27
|
```ts
|
|
@@ -36,11 +48,16 @@ SendoraCloud.screen("Pricing");
|
|
|
36
48
|
// Register the device push token (get it from expo-notifications)
|
|
37
49
|
import * as Notifications from "expo-notifications";
|
|
38
50
|
const { data } = await Notifications.getExpoPushTokenAsync();
|
|
39
|
-
await SendoraCloud.registerPushToken({
|
|
51
|
+
const receipt = await SendoraCloud.registerPushToken({
|
|
40
52
|
token: data,
|
|
41
53
|
platform: Platform.OS as "ios" | "android",
|
|
54
|
+
// bundleId is OPTIONAL but strongly recommended for multi-env apps:
|
|
55
|
+
// Sendora uses it to route to the right APNs / FCM credentials so a
|
|
56
|
+
// dev token never receives a prod push.
|
|
42
57
|
bundleId: "com.yourcompany.app",
|
|
43
58
|
});
|
|
59
|
+
// Returns { tokenId, created } — log it for debugging fan-out issues.
|
|
60
|
+
console.log("registered:", receipt.tokenId, "new?", receipt.created);
|
|
44
61
|
|
|
45
62
|
// On logout
|
|
46
63
|
SendoraCloud.reset();
|
|
@@ -72,6 +89,31 @@ SendoraCloud.reset();
|
|
|
72
89
|
}
|
|
73
90
|
```
|
|
74
91
|
|
|
92
|
+
## Stability
|
|
93
|
+
|
|
94
|
+
This SDK is on the **0.10.x line**. Patch bumps (0.10.x → 0.10.y) are backwards-compatible; minor bumps may include breaking changes — always read the [CHANGELOG](https://github.com/sendoracloud/sdk-react-native/blob/main/CHANGELOG.md) before upgrading. The next major (1.0) is targeted once the HttpOnly-cookie SSR auth + auto-trait extraction features have soaked in production for two consecutive months without a schema change. Once 1.0 ships, semver is strict.
|
|
95
|
+
|
|
96
|
+
## JWT verification (custom backend)
|
|
97
|
+
|
|
98
|
+
If your backend verifies Sendora-issued access tokens directly (rather than calling Sendora APIs that re-verify on every hit), use the OIDC-standard JWKS auto-discovery pattern — never hardcode the JWKS URL:
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
import { createRemoteJWKSet, jwtVerify, decodeJwt } from "jose";
|
|
102
|
+
|
|
103
|
+
const JWKS_BY_ISS = new Map<string, ReturnType<typeof createRemoteJWKSet>>();
|
|
104
|
+
function getJwks(iss: string) {
|
|
105
|
+
if (!JWKS_BY_ISS.has(iss)) {
|
|
106
|
+
JWKS_BY_ISS.set(iss, createRemoteJWKSet(new URL(`${iss}/.well-known/jwks.json`)));
|
|
107
|
+
}
|
|
108
|
+
return JWKS_BY_ISS.get(iss)!;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const claims = decodeJwt(token);
|
|
112
|
+
const { payload } = await jwtVerify(token, getJwks(claims.iss as string));
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
The `iss` claim is a per-org Sendora URL (e.g. `https://api.sendoracloud.com/api/v1/auth-service/<orgId>`); appending `/.well-known/jwks.json` resolves on the same host. This means a customer can rotate signing keys without your code redeploying. **Don't** copy a hardcoded JWKS URL from a tutorial — it'll break in 12 months when an org rotates.
|
|
116
|
+
|
|
75
117
|
## Security
|
|
76
118
|
|
|
77
119
|
- **Secret keys refused on the client.** The SDK only accepts `pk_(live|test)_<alphanumerics>` keys — `sk_*` (any case) or malformed values throw at init. Secret keys must stay on your backend.
|
package/dist/index.cjs
CHANGED
|
@@ -800,7 +800,7 @@ function decodeJwtPayload(token) {
|
|
|
800
800
|
}
|
|
801
801
|
|
|
802
802
|
// src/index.ts
|
|
803
|
-
var SDK_VERSION = "0.10.
|
|
803
|
+
var SDK_VERSION = "0.10.4";
|
|
804
804
|
var DEFAULT_API_URL = "https://api.sendoracloud.com";
|
|
805
805
|
var ANON_KEY = "anon_id";
|
|
806
806
|
var USER_ID_KEY = "user_id";
|
|
@@ -816,15 +816,33 @@ var MAX_PUSH_METADATA_BYTES = 4096;
|
|
|
816
816
|
var MAX_USER_ID_LEN = 256;
|
|
817
817
|
var MAX_TRAITS_BYTES = 32 * 1024;
|
|
818
818
|
var MAX_PROPERTIES_BYTES = 32 * 1024;
|
|
819
|
+
function assertCSPRNG() {
|
|
820
|
+
const g = globalThis;
|
|
821
|
+
if (g.crypto?.getRandomValues) {
|
|
822
|
+
return g.crypto;
|
|
823
|
+
}
|
|
824
|
+
const msg = [
|
|
825
|
+
"[sendoracloud] crypto.getRandomValues is unavailable in this runtime.",
|
|
826
|
+
"",
|
|
827
|
+
"Sendora refuses to mint anonymous IDs from Math.random \u2014 predictable IDs",
|
|
828
|
+
"are a security floor regression, not a tradeoff.",
|
|
829
|
+
"",
|
|
830
|
+
"Fix (React Native + Expo):",
|
|
831
|
+
" 1) npm install react-native-get-random-values",
|
|
832
|
+
" 2) Add this as the FIRST line of your app's entry file (index.js or",
|
|
833
|
+
" index.tsx \u2014 before any other import including Sendora):",
|
|
834
|
+
"",
|
|
835
|
+
" import 'react-native-get-random-values';",
|
|
836
|
+
"",
|
|
837
|
+
" 3) Reload the JS bundle (full restart, not Fast Refresh).",
|
|
838
|
+
"",
|
|
839
|
+
"Docs: https://sendoracloud.com/docs/quickstart-react-native#csprng"
|
|
840
|
+
].join("\n");
|
|
841
|
+
throw new Error(msg);
|
|
842
|
+
}
|
|
819
843
|
function uuid() {
|
|
820
844
|
const bytes = new Uint8Array(16);
|
|
821
|
-
|
|
822
|
-
if (!g.crypto?.getRandomValues) {
|
|
823
|
-
throw new Error(
|
|
824
|
-
"[sendoracloud] crypto.getRandomValues unavailable \u2014 refusing to mint IDs without a CSPRNG. Upgrade Hermes / your RN runtime."
|
|
825
|
-
);
|
|
826
|
-
}
|
|
827
|
-
g.crypto.getRandomValues(bytes);
|
|
845
|
+
assertCSPRNG().getRandomValues(bytes);
|
|
828
846
|
bytes[6] = bytes[6] & 15 | 64;
|
|
829
847
|
bytes[8] = bytes[8] & 63 | 128;
|
|
830
848
|
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
@@ -849,6 +867,7 @@ var SendoraSDK = class {
|
|
|
849
867
|
}
|
|
850
868
|
/** Initialize the SDK. Must be awaited at app startup before identify/track. */
|
|
851
869
|
async init(config) {
|
|
870
|
+
assertCSPRNG();
|
|
852
871
|
const rawKey = String(config.apiKey ?? config.publicKey ?? "").trim();
|
|
853
872
|
if (/^sk_/i.test(rawKey) || /sk_(prod|staging|dev|live|test)_/i.test(rawKey)) {
|
|
854
873
|
throw new Error(
|
|
@@ -988,7 +1007,9 @@ var SendoraSDK = class {
|
|
|
988
1007
|
*/
|
|
989
1008
|
async registerPushToken(reg) {
|
|
990
1009
|
this.assertReady();
|
|
991
|
-
if (!this.config)
|
|
1010
|
+
if (!this.config) {
|
|
1011
|
+
throw new Error("[sendoracloud] init() must complete before registerPushToken().");
|
|
1012
|
+
}
|
|
992
1013
|
if (!reg.token || byteLength(reg.token) > MAX_PUSH_TOKEN_BYTES) {
|
|
993
1014
|
throw new Error("[sendoracloud] push token is empty or exceeds 4KB.");
|
|
994
1015
|
}
|
|
@@ -998,7 +1019,7 @@ var SendoraSDK = class {
|
|
|
998
1019
|
throw new Error("[sendoracloud] push token metadata exceeds 4KB.");
|
|
999
1020
|
}
|
|
1000
1021
|
}
|
|
1001
|
-
await post(
|
|
1022
|
+
const data = await post(
|
|
1002
1023
|
{ apiUrl: this.config.apiUrl, publicKey: this.config.publicKey, debug: this.debug },
|
|
1003
1024
|
"/api/v1/push/tokens",
|
|
1004
1025
|
{
|
|
@@ -1011,6 +1032,10 @@ var SendoraSDK = class {
|
|
|
1011
1032
|
metadata: reg.metadata ?? {}
|
|
1012
1033
|
}
|
|
1013
1034
|
);
|
|
1035
|
+
if (!data?.tokenId) {
|
|
1036
|
+
throw new Error("[sendoracloud] push token registered but server returned no tokenId \u2014 upgrade backend to >= s58.16.");
|
|
1037
|
+
}
|
|
1038
|
+
return { tokenId: data.tokenId, created: data.created === true };
|
|
1014
1039
|
}
|
|
1015
1040
|
/**
|
|
1016
1041
|
* Clear identity + rotate the anonymous id. Call on logout. Awaits
|
package/dist/index.d.cts
CHANGED
|
@@ -324,11 +324,25 @@ type PushPlatform = "ios" | "android";
|
|
|
324
324
|
interface PushTokenRegistration {
|
|
325
325
|
token: string;
|
|
326
326
|
platform: PushPlatform;
|
|
327
|
-
/**
|
|
327
|
+
/**
|
|
328
|
+
* App bundle / package identifier, for fan-out to the right credentials.
|
|
329
|
+
* Optional but strongly recommended for multi-env apps so dev / staging
|
|
330
|
+
* tokens never get a prod APNs / FCM push.
|
|
331
|
+
*/
|
|
328
332
|
bundleId?: string;
|
|
329
333
|
/** Arbitrary device metadata stored alongside the token. */
|
|
330
334
|
metadata?: Record<string, string>;
|
|
331
335
|
}
|
|
336
|
+
/**
|
|
337
|
+
* Returned by `registerPushToken` so the caller can log a receipt or run
|
|
338
|
+
* a follow-up `testPush(userId)` against the freshly stored token. `created`
|
|
339
|
+
* distinguishes a brand-new token row from an idempotent re-registration of
|
|
340
|
+
* a token Sendora already had on file.
|
|
341
|
+
*/
|
|
342
|
+
interface PushTokenReceipt {
|
|
343
|
+
tokenId: string;
|
|
344
|
+
created: boolean;
|
|
345
|
+
}
|
|
332
346
|
|
|
333
347
|
/**
|
|
334
348
|
* @sendoracloud/sdk-react-native — React Native + Expo SDK.
|
|
@@ -385,7 +399,7 @@ declare class SendoraSDK {
|
|
|
385
399
|
* obtaining the token — typically via `expo-notifications` or
|
|
386
400
|
* `@react-native-firebase/messaging`.
|
|
387
401
|
*/
|
|
388
|
-
registerPushToken(reg: PushTokenRegistration): Promise<
|
|
402
|
+
registerPushToken(reg: PushTokenRegistration): Promise<PushTokenReceipt>;
|
|
389
403
|
/**
|
|
390
404
|
* Clear identity + rotate the anonymous id. Call on logout. Awaits
|
|
391
405
|
* the AsyncStorage writes so a caller who kills the app immediately
|
|
@@ -397,4 +411,4 @@ declare class SendoraSDK {
|
|
|
397
411
|
}
|
|
398
412
|
declare const SendoraCloud: SendoraSDK;
|
|
399
413
|
|
|
400
|
-
export { Auth, AuthError, type AuthTokens, type AuthUser, EmailAlreadyTakenError, type IdentifyTraits, type PushTokenRegistration, SendoraCloud, type SendoraConfig, type TrackProperties, SendoraCloud as default };
|
|
414
|
+
export { Auth, AuthError, type AuthTokens, type AuthUser, EmailAlreadyTakenError, type IdentifyTraits, type PushTokenReceipt, type PushTokenRegistration, SendoraCloud, type SendoraConfig, type TrackProperties, SendoraCloud as default };
|
package/dist/index.d.ts
CHANGED
|
@@ -324,11 +324,25 @@ type PushPlatform = "ios" | "android";
|
|
|
324
324
|
interface PushTokenRegistration {
|
|
325
325
|
token: string;
|
|
326
326
|
platform: PushPlatform;
|
|
327
|
-
/**
|
|
327
|
+
/**
|
|
328
|
+
* App bundle / package identifier, for fan-out to the right credentials.
|
|
329
|
+
* Optional but strongly recommended for multi-env apps so dev / staging
|
|
330
|
+
* tokens never get a prod APNs / FCM push.
|
|
331
|
+
*/
|
|
328
332
|
bundleId?: string;
|
|
329
333
|
/** Arbitrary device metadata stored alongside the token. */
|
|
330
334
|
metadata?: Record<string, string>;
|
|
331
335
|
}
|
|
336
|
+
/**
|
|
337
|
+
* Returned by `registerPushToken` so the caller can log a receipt or run
|
|
338
|
+
* a follow-up `testPush(userId)` against the freshly stored token. `created`
|
|
339
|
+
* distinguishes a brand-new token row from an idempotent re-registration of
|
|
340
|
+
* a token Sendora already had on file.
|
|
341
|
+
*/
|
|
342
|
+
interface PushTokenReceipt {
|
|
343
|
+
tokenId: string;
|
|
344
|
+
created: boolean;
|
|
345
|
+
}
|
|
332
346
|
|
|
333
347
|
/**
|
|
334
348
|
* @sendoracloud/sdk-react-native — React Native + Expo SDK.
|
|
@@ -385,7 +399,7 @@ declare class SendoraSDK {
|
|
|
385
399
|
* obtaining the token — typically via `expo-notifications` or
|
|
386
400
|
* `@react-native-firebase/messaging`.
|
|
387
401
|
*/
|
|
388
|
-
registerPushToken(reg: PushTokenRegistration): Promise<
|
|
402
|
+
registerPushToken(reg: PushTokenRegistration): Promise<PushTokenReceipt>;
|
|
389
403
|
/**
|
|
390
404
|
* Clear identity + rotate the anonymous id. Call on logout. Awaits
|
|
391
405
|
* the AsyncStorage writes so a caller who kills the app immediately
|
|
@@ -397,4 +411,4 @@ declare class SendoraSDK {
|
|
|
397
411
|
}
|
|
398
412
|
declare const SendoraCloud: SendoraSDK;
|
|
399
413
|
|
|
400
|
-
export { Auth, AuthError, type AuthTokens, type AuthUser, EmailAlreadyTakenError, type IdentifyTraits, type PushTokenRegistration, SendoraCloud, type SendoraConfig, type TrackProperties, SendoraCloud as default };
|
|
414
|
+
export { Auth, AuthError, type AuthTokens, type AuthUser, EmailAlreadyTakenError, type IdentifyTraits, type PushTokenReceipt, type PushTokenRegistration, SendoraCloud, type SendoraConfig, type TrackProperties, SendoraCloud as default };
|
package/dist/index.js
CHANGED
|
@@ -760,7 +760,7 @@ function decodeJwtPayload(token) {
|
|
|
760
760
|
}
|
|
761
761
|
|
|
762
762
|
// src/index.ts
|
|
763
|
-
var SDK_VERSION = "0.10.
|
|
763
|
+
var SDK_VERSION = "0.10.4";
|
|
764
764
|
var DEFAULT_API_URL = "https://api.sendoracloud.com";
|
|
765
765
|
var ANON_KEY = "anon_id";
|
|
766
766
|
var USER_ID_KEY = "user_id";
|
|
@@ -776,15 +776,33 @@ var MAX_PUSH_METADATA_BYTES = 4096;
|
|
|
776
776
|
var MAX_USER_ID_LEN = 256;
|
|
777
777
|
var MAX_TRAITS_BYTES = 32 * 1024;
|
|
778
778
|
var MAX_PROPERTIES_BYTES = 32 * 1024;
|
|
779
|
+
function assertCSPRNG() {
|
|
780
|
+
const g = globalThis;
|
|
781
|
+
if (g.crypto?.getRandomValues) {
|
|
782
|
+
return g.crypto;
|
|
783
|
+
}
|
|
784
|
+
const msg = [
|
|
785
|
+
"[sendoracloud] crypto.getRandomValues is unavailable in this runtime.",
|
|
786
|
+
"",
|
|
787
|
+
"Sendora refuses to mint anonymous IDs from Math.random \u2014 predictable IDs",
|
|
788
|
+
"are a security floor regression, not a tradeoff.",
|
|
789
|
+
"",
|
|
790
|
+
"Fix (React Native + Expo):",
|
|
791
|
+
" 1) npm install react-native-get-random-values",
|
|
792
|
+
" 2) Add this as the FIRST line of your app's entry file (index.js or",
|
|
793
|
+
" index.tsx \u2014 before any other import including Sendora):",
|
|
794
|
+
"",
|
|
795
|
+
" import 'react-native-get-random-values';",
|
|
796
|
+
"",
|
|
797
|
+
" 3) Reload the JS bundle (full restart, not Fast Refresh).",
|
|
798
|
+
"",
|
|
799
|
+
"Docs: https://sendoracloud.com/docs/quickstart-react-native#csprng"
|
|
800
|
+
].join("\n");
|
|
801
|
+
throw new Error(msg);
|
|
802
|
+
}
|
|
779
803
|
function uuid() {
|
|
780
804
|
const bytes = new Uint8Array(16);
|
|
781
|
-
|
|
782
|
-
if (!g.crypto?.getRandomValues) {
|
|
783
|
-
throw new Error(
|
|
784
|
-
"[sendoracloud] crypto.getRandomValues unavailable \u2014 refusing to mint IDs without a CSPRNG. Upgrade Hermes / your RN runtime."
|
|
785
|
-
);
|
|
786
|
-
}
|
|
787
|
-
g.crypto.getRandomValues(bytes);
|
|
805
|
+
assertCSPRNG().getRandomValues(bytes);
|
|
788
806
|
bytes[6] = bytes[6] & 15 | 64;
|
|
789
807
|
bytes[8] = bytes[8] & 63 | 128;
|
|
790
808
|
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
@@ -809,6 +827,7 @@ var SendoraSDK = class {
|
|
|
809
827
|
}
|
|
810
828
|
/** Initialize the SDK. Must be awaited at app startup before identify/track. */
|
|
811
829
|
async init(config) {
|
|
830
|
+
assertCSPRNG();
|
|
812
831
|
const rawKey = String(config.apiKey ?? config.publicKey ?? "").trim();
|
|
813
832
|
if (/^sk_/i.test(rawKey) || /sk_(prod|staging|dev|live|test)_/i.test(rawKey)) {
|
|
814
833
|
throw new Error(
|
|
@@ -948,7 +967,9 @@ var SendoraSDK = class {
|
|
|
948
967
|
*/
|
|
949
968
|
async registerPushToken(reg) {
|
|
950
969
|
this.assertReady();
|
|
951
|
-
if (!this.config)
|
|
970
|
+
if (!this.config) {
|
|
971
|
+
throw new Error("[sendoracloud] init() must complete before registerPushToken().");
|
|
972
|
+
}
|
|
952
973
|
if (!reg.token || byteLength(reg.token) > MAX_PUSH_TOKEN_BYTES) {
|
|
953
974
|
throw new Error("[sendoracloud] push token is empty or exceeds 4KB.");
|
|
954
975
|
}
|
|
@@ -958,7 +979,7 @@ var SendoraSDK = class {
|
|
|
958
979
|
throw new Error("[sendoracloud] push token metadata exceeds 4KB.");
|
|
959
980
|
}
|
|
960
981
|
}
|
|
961
|
-
await post(
|
|
982
|
+
const data = await post(
|
|
962
983
|
{ apiUrl: this.config.apiUrl, publicKey: this.config.publicKey, debug: this.debug },
|
|
963
984
|
"/api/v1/push/tokens",
|
|
964
985
|
{
|
|
@@ -971,6 +992,10 @@ var SendoraSDK = class {
|
|
|
971
992
|
metadata: reg.metadata ?? {}
|
|
972
993
|
}
|
|
973
994
|
);
|
|
995
|
+
if (!data?.tokenId) {
|
|
996
|
+
throw new Error("[sendoracloud] push token registered but server returned no tokenId \u2014 upgrade backend to >= s58.16.");
|
|
997
|
+
}
|
|
998
|
+
return { tokenId: data.tokenId, created: data.created === true };
|
|
974
999
|
}
|
|
975
1000
|
/**
|
|
976
1001
|
* Clear identity + rotate the anonymous id. Call on logout. Awaits
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sendoracloud/sdk-react-native",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.4",
|
|
4
4
|
"description": "Sendora Cloud React Native + Expo SDK — analytics, identity, push-token registration, auth. Expo Go compatible, no native modules beyond AsyncStorage.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|