@sendoracloud/sdk-react-native 0.15.0 → 0.16.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/README.md +126 -43
- package/dist/index.cjs +447 -33
- package/dist/index.d.cts +176 -56
- package/dist/index.d.ts +176 -56
- package/dist/index.js +443 -32
- package/examples/links-router.tsx +79 -0
- package/examples/links-share.tsx +99 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -22,6 +22,14 @@ import "react-native-get-random-values";
|
|
|
22
22
|
|
|
23
23
|
If you forget, `init()` throws with a paste-ready remediation block — it never fails silently.
|
|
24
24
|
|
|
25
|
+
### Optional peers (deep links)
|
|
26
|
+
|
|
27
|
+
| Peer | When to install |
|
|
28
|
+
| --- | --- |
|
|
29
|
+
| `expo-localization` | Improves device-fingerprint locale accuracy. Most Expo apps already have it. |
|
|
30
|
+
| `expo-crypto` | Hardens the SHA-256 used by `computeDeviceFingerprint()`. Auto-used when installed; SDK falls back to web crypto and then a pure-JS impl if not. |
|
|
31
|
+
| `react-native-play-install-referrer` | Android-only. Enables 100%-accurate deferred deep-link matching via Play Install Referrer. SDK auto-probes — no-op on iOS or when uninstalled. |
|
|
32
|
+
|
|
25
33
|
## Quick start
|
|
26
34
|
|
|
27
35
|
```ts
|
|
@@ -29,41 +37,31 @@ import SendoraCloud from "@sendoracloud/sdk-react-native";
|
|
|
29
37
|
|
|
30
38
|
// App.tsx or wherever you initialise app-level services
|
|
31
39
|
await SendoraCloud.init({
|
|
32
|
-
|
|
33
|
-
|
|
40
|
+
apiKey: "pk_prod_...", // from Settings → API Keys in the dashboard
|
|
41
|
+
iosBundleId: "com.yourapp", // bundle-id gate — required for SDK link mint
|
|
42
|
+
androidPackageName: "com.yourapp",
|
|
43
|
+
// linkDomain: "pulse.link", // optional — only if you use a custom share host
|
|
34
44
|
});
|
|
35
45
|
|
|
36
46
|
// Identify the signed-in user
|
|
37
|
-
SendoraCloud.identify("user-123", {
|
|
38
|
-
email: "user@example.com",
|
|
39
|
-
name: "Ada Lovelace",
|
|
40
|
-
});
|
|
47
|
+
SendoraCloud.identify("user-123", { email: "user@example.com" });
|
|
41
48
|
|
|
42
|
-
// Track
|
|
49
|
+
// Track product events + screen views
|
|
43
50
|
SendoraCloud.track("signup.completed", { plan: "growth" });
|
|
44
|
-
|
|
45
|
-
// Track screen views (RN equivalent of page views)
|
|
46
51
|
SendoraCloud.screen("Pricing");
|
|
47
52
|
|
|
48
|
-
//
|
|
49
|
-
import * as Notifications from "expo-notifications";
|
|
50
|
-
const { data } = await Notifications.getExpoPushTokenAsync();
|
|
53
|
+
// Push tokens
|
|
51
54
|
const receipt = await SendoraCloud.registerPushToken({
|
|
52
|
-
token:
|
|
55
|
+
token: expoPushToken,
|
|
53
56
|
platform: Platform.OS as "ios" | "android",
|
|
54
|
-
|
|
55
|
-
// Sendora uses it to route to the right APNs / FCM credentials so a
|
|
56
|
-
// dev token never receives a prod push.
|
|
57
|
-
bundleId: "com.yourcompany.app",
|
|
57
|
+
bundleId: "com.yourapp",
|
|
58
58
|
});
|
|
59
|
-
// Returns { tokenId, created } — log it for debugging fan-out issues.
|
|
60
|
-
console.log("registered:", receipt.tokenId, "new?", receipt.created);
|
|
61
59
|
|
|
62
60
|
// On logout
|
|
63
|
-
SendoraCloud.reset();
|
|
61
|
+
await SendoraCloud.reset();
|
|
64
62
|
```
|
|
65
63
|
|
|
66
|
-
## API
|
|
64
|
+
## API surface
|
|
67
65
|
|
|
68
66
|
| Method | Description |
|
|
69
67
|
| --- | --- |
|
|
@@ -72,38 +70,122 @@ SendoraCloud.reset();
|
|
|
72
70
|
| `track(event, props?)` | Fire a custom product event. |
|
|
73
71
|
| `screen(name, props?)` | Track a screen view. |
|
|
74
72
|
| `registerPushToken(reg)` | Register an APNs or FCM push token. |
|
|
73
|
+
| `auth.*` | Sign-in / sign-up / MFA / passkeys / OIDC / SSO. See `auth.ts`. |
|
|
74
|
+
| `links.create<T>(input, opts?)` | Mint a Sendora deep link. Generic `T` for typed `linkData`. |
|
|
75
|
+
| `links.prewarm<T>(input, opts?)` | Background-mint + cache so share-tap is instant. |
|
|
76
|
+
| `links.handleUniversalLink(url)` | Resolve a warm-path Universal Link / App Link delivery. |
|
|
77
|
+
| `links.matchDeferred(input?)` | Cold-launch deferred match. Auto-probes Play Install Referrer + fingerprint. |
|
|
78
|
+
| `links.attachLinkingApi({ onLegacyUrl })` | One-call replacement for `Linking.getInitialURL` + `addEventListener` + extract-pre-check. |
|
|
79
|
+
| `links.onLinkOpened<T>(cb)` | Register a callback fired for both warm + deferred opens. |
|
|
80
|
+
| `links.revoke(shortcode)` | Soft-delete a link (private-content unsend). Idempotent. |
|
|
81
|
+
| `links.getStats(shortcode)` | `{ totalClicks, uniqueClicks, deferredMatches, byOs, byCountry, byDevice }`. |
|
|
82
|
+
| `computeDeviceFingerprint()` | Standalone — returns canonical hex SHA-256. Identical recipe across all 3 mobile SDKs. |
|
|
75
83
|
| `reset()` | Clear identity + rotate anonymous id. Call on logout. |
|
|
76
84
|
| `getAnonymousId()` | Stable device id (persisted across restarts). |
|
|
77
85
|
| `getUserId()` | Currently identified user id, or null. |
|
|
78
86
|
|
|
87
|
+
## Deep links (Branch / Firebase Dynamic Links parity)
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
import SendoraCloud, { LinkError } from "@sendoracloud/sdk-react-native";
|
|
91
|
+
|
|
92
|
+
// 1. Typed linkData — define your app's deep-link shape once.
|
|
93
|
+
interface ArticleLink extends Record<string, unknown> {
|
|
94
|
+
type: "article";
|
|
95
|
+
articleId: string;
|
|
96
|
+
category: "tech" | "design" | "policy";
|
|
97
|
+
sharedBy: string;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 2. Prewarm on row mount → share tap completes instantly.
|
|
101
|
+
SendoraCloud.links.prewarm<ArticleLink>(
|
|
102
|
+
{
|
|
103
|
+
title: `Share: ${article.title}`,
|
|
104
|
+
iosDeepLinkPath: `/articles/${article.id}`,
|
|
105
|
+
androidDeepLinkPath: `/articles/${article.id}`,
|
|
106
|
+
ogTitle: article.title,
|
|
107
|
+
linkData: { type: "article", articleId: article.id, category: article.category, sharedBy: currentUser.id },
|
|
108
|
+
// fallbackUrl optional — backend defaults from your project's apps registry.
|
|
109
|
+
},
|
|
110
|
+
{ key: `article:${article.id}` },
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
// 3. Mint on tap — returns from prewarm cache when keys match.
|
|
114
|
+
try {
|
|
115
|
+
const link = await SendoraCloud.links.create<ArticleLink>(input, { key: `article:${article.id}` });
|
|
116
|
+
await Share.share({ message: link.url });
|
|
117
|
+
} catch (err) {
|
|
118
|
+
if (err instanceof LinkError) {
|
|
119
|
+
switch (err.code) {
|
|
120
|
+
case "BUNDLE_MISMATCH": /* register bundle in Dashboard → Apps */ break;
|
|
121
|
+
case "PLAN_LIMIT": /* upgrade plan */ break;
|
|
122
|
+
case "RATE_LIMITED": /* back off + retry */ break;
|
|
123
|
+
case "FALLBACK_REQUIRED": /* configure App Store URL in apps registry */ break;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// 4. App-root router — one call replaces the Linking boilerplate.
|
|
129
|
+
useEffect(() => {
|
|
130
|
+
const offOpen = SendoraCloud.links.onLinkOpened<ArticleLink>((event) => {
|
|
131
|
+
if (event.linkData.type === "article") navigateToArticle(event.linkData.articleId);
|
|
132
|
+
});
|
|
133
|
+
let detach: (() => void) | undefined;
|
|
134
|
+
void SendoraCloud.links
|
|
135
|
+
.attachLinkingApi({ onLegacyUrl: (url) => myExistingRouter.handle(url) })
|
|
136
|
+
.then((d) => { detach = d; });
|
|
137
|
+
// Cold-launch deferred match — auto-picks installReferrer / fingerprint.
|
|
138
|
+
void SendoraCloud.links.matchDeferred();
|
|
139
|
+
return () => { detach?.(); offOpen(); };
|
|
140
|
+
}, []);
|
|
141
|
+
|
|
142
|
+
// 5. Revoke + stats — no dashboard scraping.
|
|
143
|
+
await SendoraCloud.links.revoke("ab3xk9p");
|
|
144
|
+
const stats = await SendoraCloud.links.getStats("ab3xk9p");
|
|
145
|
+
// → { totalClicks, uniqueClicks, deferredMatches, byOs, byCountry, byDevice }
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Full paste-ready recipes live in [`examples/links-share.tsx`](./examples/links-share.tsx) and [`examples/links-router.tsx`](./examples/links-router.tsx).
|
|
149
|
+
|
|
150
|
+
### Typed errors
|
|
151
|
+
|
|
152
|
+
`LinkError` is thrown for every backend rejection. Branch on `err.code` (a typed `LinkErrorCode`) rather than string-matching `err.message`:
|
|
153
|
+
|
|
154
|
+
```
|
|
155
|
+
"BUNDLE_MISMATCH" | "DATA_TOO_LARGE" | "EXPIRED" | "NETWORK" | "RATE_LIMITED"
|
|
156
|
+
| "NOT_FOUND" | "UNAUTHORIZED" | "INVALID_INPUT" | "PLAN_LIMIT"
|
|
157
|
+
| "FALLBACK_REQUIRED" | "SERVER" | "UNKNOWN"
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Bundle-id gate
|
|
161
|
+
|
|
162
|
+
`init()` accepts `iosBundleId` / `androidPackageName`. The SDK auto-forwards them on every `links.create()` / `links.matchDeferred()` call. The backend cross-checks against your project's apps registry (Dashboard → Apps); a leaked public key plus a wrong bundle returns `LinkError(code: "BUNDLE_MISMATCH", statusCode: 422)`. Fail-closed when zero apps are registered — register your iOS bundle id + Android package name + SHA-256 fingerprint before going to prod.
|
|
163
|
+
|
|
164
|
+
## Auth
|
|
165
|
+
|
|
166
|
+
`SendoraCloud.auth` covers anonymous sign-in, email + password, magic link, email OTP, TOTP MFA, recovery codes, OIDC / SAML SSO, Sign in with Apple, Google, Microsoft, LinkedIn, Facebook, Discord. See [auth quickstart](https://sendoracloud.com/docs/quickstart-react-native#auth).
|
|
167
|
+
|
|
79
168
|
## Config
|
|
80
169
|
|
|
81
170
|
```ts
|
|
82
171
|
{
|
|
83
|
-
|
|
84
|
-
orgId?: string; // Your workspace UUID.
|
|
172
|
+
apiKey: string; // Required. pk_(prod|staging|dev)_<16+ alphanumerics>.
|
|
85
173
|
apiUrl?: string; // Defaults to https://api.sendoracloud.com
|
|
86
|
-
environment?: "prod" | "staging" | "dev";
|
|
87
|
-
projectId?: string;
|
|
174
|
+
environment?: "prod" | "staging" | "dev";
|
|
175
|
+
projectId?: string;
|
|
88
176
|
consentedByDefault?: boolean; // Defaults to true.
|
|
89
|
-
/**
|
|
90
|
-
* Auto-collect lifecycle events. Default true. Object form:
|
|
91
|
-
* { appOpen?, sessionStart?, appBackground? } for granular control.
|
|
92
|
-
* - appOpen — `app.opened` once per init.
|
|
93
|
-
* - sessionStart — `session.start` once per session (idle >sessionIdleMs).
|
|
94
|
-
* - appBackground — AppState 'change' fires `app.foregrounded` /
|
|
95
|
-
* `app.backgrounded`.
|
|
96
|
-
* Note: `screen.viewed` is NOT auto — call `SendoraCloud.screen(name)`
|
|
97
|
-
* from your navigation listener (RN has no canonical router).
|
|
98
|
-
*/
|
|
99
177
|
autoTrack?: boolean | { appOpen?: boolean; sessionStart?: boolean; appBackground?: boolean };
|
|
100
|
-
sessionIdleMs?: number;
|
|
178
|
+
sessionIdleMs?: number; // Idle threshold for session expiry. Default 30 min.
|
|
179
|
+
iosBundleId?: string; // Bundle-id gate input for links.create / matchDeferred.
|
|
180
|
+
androidPackageName?: string;
|
|
181
|
+
linkDomain?: string; // Custom share host (e.g. "pulse.link"). Defaults to Sendora's.
|
|
182
|
+
debug?: boolean;
|
|
101
183
|
}
|
|
102
184
|
```
|
|
103
185
|
|
|
104
186
|
## Stability
|
|
105
187
|
|
|
106
|
-
This SDK is on the **0.
|
|
188
|
+
This SDK is on the **0.16.x line**. Patch bumps (0.16.x → 0.16.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. 1.0 ships once the SDK has soaked 2 consecutive months in production without a schema change.
|
|
107
189
|
|
|
108
190
|
## JWT verification (custom backend)
|
|
109
191
|
|
|
@@ -124,15 +206,16 @@ const claims = decodeJwt(token);
|
|
|
124
206
|
const { payload } = await jwtVerify(token, getJwks(claims.iss as string));
|
|
125
207
|
```
|
|
126
208
|
|
|
127
|
-
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.
|
|
209
|
+
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.
|
|
128
210
|
|
|
129
211
|
## Security
|
|
130
212
|
|
|
131
|
-
- **Secret keys refused on the client.**
|
|
132
|
-
- **HTTPS enforced** outside local dev.
|
|
133
|
-
- **AsyncStorage** backs the anonymous id
|
|
134
|
-
- **Use opaque userIds, not emails.**
|
|
135
|
-
- **Push token + metadata capped** at 4 KB each
|
|
213
|
+
- **Secret keys refused on the client.** Only `pk_*` keys accepted; `sk_*` throws at init.
|
|
214
|
+
- **HTTPS enforced** outside local dev. Non-`https:` `apiUrl` values are rejected unless `localhost` / `127.0.0.1` + `environment != "prod"`.
|
|
215
|
+
- **AsyncStorage** backs the anonymous id for cross-restart identity; never cross-device. `reset()` awaits the storage writes.
|
|
216
|
+
- **Use opaque userIds, not emails.** Persisted to AsyncStorage plaintext. Put email in `traits` instead.
|
|
217
|
+
- **Push token + metadata capped** at 4 KB each.
|
|
218
|
+
- **Deep-link bundle-id gate** — a leaked public key cannot mint links pointed at someone else's app.
|
|
136
219
|
|
|
137
220
|
## License
|
|
138
221
|
|