@sendoracloud/sdk-react-native 0.15.1 → 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 +117 -78
- 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,82 +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. |
|
|
75
|
-
| `
|
|
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
76
|
| `links.handleUniversalLink(url)` | Resolve a warm-path Universal Link / App Link delivery. |
|
|
77
|
-
| `links.matchDeferred(
|
|
78
|
-
| `links.
|
|
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. |
|
|
79
83
|
| `reset()` | Clear identity + rotate anonymous id. Call on logout. |
|
|
80
84
|
| `getAnonymousId()` | Stable device id (persisted across restarts). |
|
|
81
85
|
| `getUserId()` | Currently identified user id, or null. |
|
|
82
86
|
|
|
83
|
-
|
|
87
|
+
## Deep links (Branch / Firebase Dynamic Links parity)
|
|
84
88
|
|
|
85
89
|
```ts
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
//
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
ogImageUrl: article.heroImage,
|
|
96
|
-
linkData: { articleId: article.id, sharedBy: currentUser.id },
|
|
97
|
-
});
|
|
98
|
-
Share.share({ message: url });
|
|
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
99
|
|
|
100
|
-
//
|
|
101
|
-
SendoraCloud.links.
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
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
|
+
}
|
|
105
127
|
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
});
|
|
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`:
|
|
111
153
|
|
|
112
|
-
// Cold path (deferred deep link after install) — Android Play Install Referrer
|
|
113
|
-
// preferred (100% accurate). iOS: ship a fingerprint hash if you have one.
|
|
114
|
-
import { PlayInstallReferrer } from "react-native-play-install-referrer";
|
|
115
|
-
const ref = await PlayInstallReferrer.getInstallReferrerInfo();
|
|
116
|
-
await SendoraCloud.links.matchDeferred({ installReferrer: ref?.installReferrer });
|
|
117
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).
|
|
118
167
|
|
|
119
168
|
## Config
|
|
120
169
|
|
|
121
170
|
```ts
|
|
122
171
|
{
|
|
123
|
-
|
|
124
|
-
orgId?: string; // Your workspace UUID.
|
|
172
|
+
apiKey: string; // Required. pk_(prod|staging|dev)_<16+ alphanumerics>.
|
|
125
173
|
apiUrl?: string; // Defaults to https://api.sendoracloud.com
|
|
126
|
-
environment?: "prod" | "staging" | "dev";
|
|
127
|
-
projectId?: string;
|
|
174
|
+
environment?: "prod" | "staging" | "dev";
|
|
175
|
+
projectId?: string;
|
|
128
176
|
consentedByDefault?: boolean; // Defaults to true.
|
|
129
|
-
/**
|
|
130
|
-
* Auto-collect lifecycle events. Default true. Object form:
|
|
131
|
-
* { appOpen?, sessionStart?, appBackground? } for granular control.
|
|
132
|
-
* - appOpen — `app.opened` once per init.
|
|
133
|
-
* - sessionStart — `session.start` once per session (idle >sessionIdleMs).
|
|
134
|
-
* - appBackground — AppState 'change' fires `app.foregrounded` /
|
|
135
|
-
* `app.backgrounded`.
|
|
136
|
-
* Note: `screen.viewed` is NOT auto — call `SendoraCloud.screen(name)`
|
|
137
|
-
* from your navigation listener (RN has no canonical router).
|
|
138
|
-
*/
|
|
139
177
|
autoTrack?: boolean | { appOpen?: boolean; sessionStart?: boolean; appBackground?: boolean };
|
|
140
|
-
sessionIdleMs?: number;
|
|
141
|
-
|
|
142
|
-
iosBundleId?: string;
|
|
143
|
-
/** Android package name — same purpose as iosBundleId. */
|
|
178
|
+
sessionIdleMs?: number; // Idle threshold for session expiry. Default 30 min.
|
|
179
|
+
iosBundleId?: string; // Bundle-id gate input for links.create / matchDeferred.
|
|
144
180
|
androidPackageName?: string;
|
|
181
|
+
linkDomain?: string; // Custom share host (e.g. "pulse.link"). Defaults to Sendora's.
|
|
182
|
+
debug?: boolean;
|
|
145
183
|
}
|
|
146
184
|
```
|
|
147
185
|
|
|
148
186
|
## Stability
|
|
149
187
|
|
|
150
|
-
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.
|
|
151
189
|
|
|
152
190
|
## JWT verification (custom backend)
|
|
153
191
|
|
|
@@ -168,15 +206,16 @@ const claims = decodeJwt(token);
|
|
|
168
206
|
const { payload } = await jwtVerify(token, getJwks(claims.iss as string));
|
|
169
207
|
```
|
|
170
208
|
|
|
171
|
-
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.
|
|
172
210
|
|
|
173
211
|
## Security
|
|
174
212
|
|
|
175
|
-
- **Secret keys refused on the client.**
|
|
176
|
-
- **HTTPS enforced** outside local dev.
|
|
177
|
-
- **AsyncStorage** backs the anonymous id
|
|
178
|
-
- **Use opaque userIds, not emails.**
|
|
179
|
-
- **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.
|
|
180
219
|
|
|
181
220
|
## License
|
|
182
221
|
|