@cross-deck/react-native 1.0.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/CHANGELOG.md +147 -0
- package/LICENSE +21 -0
- package/README.md +157 -0
- package/dist/index.cjs +2608 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.mts +903 -0
- package/dist/index.d.ts +903 -0
- package/dist/index.mjs +2571 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +77 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@cross-deck/react-native` will be documented
|
|
4
|
+
here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
|
|
5
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [1.0.0] — 2026-05-24
|
|
8
|
+
|
|
9
|
+
First public release. Built bank-grade from day one — every audit
|
|
10
|
+
pattern landed during the `@cross-deck/web` + `@cross-deck/node`
|
|
11
|
+
KPMG review is baked in. Three Crossdeck pillars in one SDK,
|
|
12
|
+
modelled on the shipping `@cross-deck/web` API surface so a
|
|
13
|
+
cross-platform team writes identical call-sites:
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { Crossdeck } from "@cross-deck/react-native";
|
|
17
|
+
|
|
18
|
+
Crossdeck.init({
|
|
19
|
+
appId: "app_rn_xxx",
|
|
20
|
+
publicKey: "cd_pub_live_…",
|
|
21
|
+
environment: "production",
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
await Crossdeck.identify("user_847");
|
|
25
|
+
if (Crossdeck.isEntitled("pro")) showPro();
|
|
26
|
+
Crossdeck.track("paywall_shown", { variant: "v3" });
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Subscriptions & entitlements
|
|
30
|
+
|
|
31
|
+
- **Durable last-known-good entitlement cache.** `EntitlementCache.hydrate()`
|
|
32
|
+
loads from AsyncStorage during `init()`, so `isEntitled()` is correct
|
|
33
|
+
from the first call after `init()` resolves — no cold-start window
|
|
34
|
+
where a returning Pro customer reads as free.
|
|
35
|
+
- **An outage can never fail a paying customer down to free.** A
|
|
36
|
+
failed `getEntitlements()` never clears the cache; only a successful
|
|
37
|
+
fetch replaces it. Each entitlement is still honoured against its
|
|
38
|
+
own `validUntil`, so a timed-out trial still ends.
|
|
39
|
+
- **`onEntitlementsChange(listener)`** subscriber API for reactive UI
|
|
40
|
+
binding — fires after `getEntitlements()` / `syncPurchases()` /
|
|
41
|
+
`reset()`. Listener errors are swallowed (a buggy consumer can't
|
|
42
|
+
crash the SDK or other listeners) and counted in `diagnostics()`.
|
|
43
|
+
- **`syncPurchases({ rail, signedTransactionInfo | purchaseToken })`**
|
|
44
|
+
forwards Apple StoreKit 2 or Google Billing evidence for backend
|
|
45
|
+
verification + entitlement projection.
|
|
46
|
+
- **`isEntitled(key)`** + **`listEntitlements()`** are synchronous
|
|
47
|
+
reads of the in-memory cache. Subscribe via `onEntitlementsChange`
|
|
48
|
+
for reactive bindings.
|
|
49
|
+
|
|
50
|
+
### Analytics
|
|
51
|
+
|
|
52
|
+
- **Bank-grade event queue.** `pendingBatch` slot keeps the in-flight
|
|
53
|
+
batch with the SAME `Idempotency-Key` across retries (Stripe
|
|
54
|
+
pattern) — backend dedupe on `(projectId, eventId)` handles the
|
|
55
|
+
belt-and-suspenders. Persisted blob always carries
|
|
56
|
+
`[...pendingBatch, ...buffer]` via AsyncStorage so an app crash
|
|
57
|
+
mid-flight replays the in-flight batch on the next launch.
|
|
58
|
+
- **4xx hard-stop.** 400 / 401 / 403 / 404 / 422 etc. drop the batch
|
|
59
|
+
loudly: `onPermanentFailure` callback + `console.error` regardless
|
|
60
|
+
of debug mode + `dropped` counter increments. Pre-fix (web/node
|
|
61
|
+
1.2.x and earlier) every error retried forever with the same key.
|
|
62
|
+
- **Exponential backoff with full jitter** on retryable failures
|
|
63
|
+
(5xx / network / 408 / 429). Honours server `Retry-After` when
|
|
64
|
+
bigger than the computed window, capped at 24h as a sanity guard.
|
|
65
|
+
- **Hard buffer cap (1000 events).** Past the cap we evict the
|
|
66
|
+
OLDEST events and increment `dropped` so the developer can see the
|
|
67
|
+
loss in `diagnostics()`.
|
|
68
|
+
- **Super properties** (`register` / `unregister`) and **groups**
|
|
69
|
+
(`group(type, id, traits)`) — Mixpanel pattern, attached to every
|
|
70
|
+
event automatically. Both cleared on `reset()`.
|
|
71
|
+
|
|
72
|
+
### Error capture
|
|
73
|
+
|
|
74
|
+
- **`ErrorUtils.setGlobalHandler`** chains in front of RN's default
|
|
75
|
+
handler (the red-box developer overlay) so uncaught errors AND
|
|
76
|
+
unhandled promise rejections are captured WITHOUT breaking the
|
|
77
|
+
dev experience. Stack frames parsed via the Hermes / JSC / V8
|
|
78
|
+
unified parser.
|
|
79
|
+
- **`globalThis.fetch` wrap** catches 5xx + network failures. The
|
|
80
|
+
configured `selfHostname` (derived from `init({ baseUrl })`) is
|
|
81
|
+
excluded so a Crossdeck-side outage doesn't recurse through its
|
|
82
|
+
own fetch-wrap. Strict hostname compare (no substring matches —
|
|
83
|
+
`api.cross-deck.com.attacker.example` doesn't falsely match).
|
|
84
|
+
- **Per-fingerprint rate limit** (5 per minute by default) defends
|
|
85
|
+
against runaway loops. Per-session cap (100) bounds the worst
|
|
86
|
+
case.
|
|
87
|
+
- **`captureError(err)` / `captureMessage(msg)`** manual API for
|
|
88
|
+
try/catch blocks + soft signals.
|
|
89
|
+
- **`setErrorBeforeSend(hook)`** with the bank-grade getter contract
|
|
90
|
+
— a hook installed AFTER `init()` fires on the next captured
|
|
91
|
+
error. Pre-fix on web/node 1.2.x the hook was captured by value
|
|
92
|
+
and silently inert if installed late.
|
|
93
|
+
- **Breadcrumb buffer (50 entries)** auto-populated by every
|
|
94
|
+
`track()` call + every `fetch` request (with the self-skip
|
|
95
|
+
filter). Attached to every error report.
|
|
96
|
+
|
|
97
|
+
### Privacy & compliance
|
|
98
|
+
|
|
99
|
+
- **PII scrub** — defensive regex pass over every string property
|
|
100
|
+
value before flush. Email-shaped → `<email>`, card-number-shaped
|
|
101
|
+
→ `<card>` (sentinel tokens aligned with the backend so dashboard
|
|
102
|
+
aggregation works across SDK-scrub and backend-scrub paths).
|
|
103
|
+
**Recursive walk**: nested plain objects + arrays-of-objects are
|
|
104
|
+
visited, so a `{user:{email:"x@y.com"}}` payload ships scrubbed.
|
|
105
|
+
- **`Crossdeck.consent({...})`** — three independent dimensions
|
|
106
|
+
(analytics / marketing / errors), each defaulting to `true`
|
|
107
|
+
(granted). `consent({analytics: false})` drops every subsequent
|
|
108
|
+
`track()` silently.
|
|
109
|
+
- **`Crossdeck.forget()`** — GDPR / CCPA right to be forgotten.
|
|
110
|
+
Calls `/v1/identity/forget` + wipes every local state surface.
|
|
111
|
+
|
|
112
|
+
### Diagnostics
|
|
113
|
+
|
|
114
|
+
- **`Crossdeck.diagnostics()`** — stable shape whether or not
|
|
115
|
+
`init()` has been called. Returns identity (anonymousId,
|
|
116
|
+
crossdeckCustomerId, developerUserId), clock skew (server vs
|
|
117
|
+
client `Date.now()` at last heartbeat), entitlement cache
|
|
118
|
+
freshness, queue stats (buffered, dropped, in-flight, last error,
|
|
119
|
+
consecutive failures, next retry).
|
|
120
|
+
- **Boot heartbeat** verifies the publishable key against the
|
|
121
|
+
Crossdeck API the moment the SDK is constructed. The dashboard's
|
|
122
|
+
"Verify install" check turns green within ~200ms without the
|
|
123
|
+
caller having to add an explicit call. Disable via
|
|
124
|
+
`autoHeartbeat: false` for CI / tests.
|
|
125
|
+
|
|
126
|
+
### Cross-cutting
|
|
127
|
+
|
|
128
|
+
- **`SDK_VERSION` codegen'd from `package.json`** via
|
|
129
|
+
`scripts/sync-sdk-versions.mjs` — the wire `Crossdeck-Sdk-Version`
|
|
130
|
+
header can never drift from the published bundle. CI gate via
|
|
131
|
+
`--check` mode catches drift before publish.
|
|
132
|
+
- **Identity continuity via AsyncStorage** (optional peer dep) with
|
|
133
|
+
graceful in-memory fallback when AsyncStorage isn't installed
|
|
134
|
+
(Storybook snapshots, vitest under node).
|
|
135
|
+
- **TypeScript-first** — strict mode, `noUncheckedIndexedAccess`,
|
|
136
|
+
every public type exported.
|
|
137
|
+
|
|
138
|
+
### Coverage gaps explicitly deferred
|
|
139
|
+
|
|
140
|
+
- **Auto-track sessions + deep-links** (AppState lifecycle + Linking
|
|
141
|
+
API) deferred to 1.1.0. v1.0 expects the developer to wire
|
|
142
|
+
`Crossdeck.track("screen.viewed", {...})` from their nav lib's
|
|
143
|
+
listener. Adding AppState + Linking properly is its own design
|
|
144
|
+
decision (background-foreground policy, session timeout semantics,
|
|
145
|
+
cold-start vs warm-start distinction).
|
|
146
|
+
- **Bundle-size budget gate** — RN apps don't have a per-byte CDN
|
|
147
|
+
cost the way web does; size discipline is a v1.1 add.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 VistaApps (Pty) Ltd
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# @cross-deck/react-native
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@cross-deck/react-native)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
|
|
6
|
+
Crossdeck's React Native SDK — **verified subscriptions, entitlements,
|
|
7
|
+
error capture, and product telemetry across iOS + Android JS apps in
|
|
8
|
+
one package**. Same SDK surface as [`@cross-deck/web`](https://github.com/VistaApps-za/crossdeck-web)
|
|
9
|
+
+ [`@cross-deck/node`](https://github.com/VistaApps-za/crossdeck-node)
|
|
10
|
+
— cross-platform teams write identical call-sites.
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { Crossdeck } from "@cross-deck/react-native";
|
|
14
|
+
|
|
15
|
+
Crossdeck.init({
|
|
16
|
+
appId: "app_rn_xxx",
|
|
17
|
+
publicKey: "cd_pub_live_…",
|
|
18
|
+
environment: "production",
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
await Crossdeck.identify("user_847");
|
|
22
|
+
const ents = await Crossdeck.getEntitlements();
|
|
23
|
+
if (Crossdeck.isEntitled("pro")) showPro();
|
|
24
|
+
Crossdeck.track("paywall_shown", { variant: "v3" });
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
npm install @cross-deck/react-native @react-native-async-storage/async-storage
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
# Expo
|
|
35
|
+
npx expo install @cross-deck/react-native @react-native-async-storage/async-storage
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`@react-native-async-storage/async-storage` is an **optional** peer
|
|
39
|
+
dependency. Without it the SDK falls back to in-memory storage
|
|
40
|
+
(identity + queue don't survive app restarts; events queued offline
|
|
41
|
+
are lost on cold launch). Install it for production.
|
|
42
|
+
|
|
43
|
+
## Three pillars in one SDK
|
|
44
|
+
|
|
45
|
+
| Pillar | Surface |
|
|
46
|
+
|---|---|
|
|
47
|
+
| Subscriptions & entitlements | `getEntitlements()`, `isEntitled(key)`, `listEntitlements()`, `onEntitlementsChange(listener)`, `syncPurchases({rail, signedTransactionInfo, purchaseToken})` |
|
|
48
|
+
| Behavioural analytics | `track(name, properties)`, `identify(userId, options)`, `register(props)`, `group(type, id, traits)`, `consent({...})` |
|
|
49
|
+
| Error capture | Auto: `ErrorUtils.setGlobalHandler` + `globalThis.fetch` wrap. Manual: `captureError(err)`, `captureMessage(msg)`, `setTag`, `setContext`, `addBreadcrumb`, `setErrorBeforeSend` |
|
|
50
|
+
|
|
51
|
+
## Bank-grade defaults
|
|
52
|
+
|
|
53
|
+
Every Crossdeck SDK ships these patterns by default:
|
|
54
|
+
|
|
55
|
+
- **Durable last-known-good entitlement cache.** A returning Pro
|
|
56
|
+
customer reads as Pro on the FIRST `isEntitled()` after `init()`,
|
|
57
|
+
even on a cold launch with no network. A Crossdeck outage can
|
|
58
|
+
never fail a paying customer down to free.
|
|
59
|
+
- **Queue durability + Stripe-style Idempotency-Key reuse.** Events
|
|
60
|
+
spliced for a flush persist to AsyncStorage with the in-flight
|
|
61
|
+
batch attached, so an app crash mid-flight replays the batch on
|
|
62
|
+
the next launch. Backend dedupes on `(projectId, eventId)`.
|
|
63
|
+
- **4xx hard-stop.** Permanent failures (401 key revoked, 400/422
|
|
64
|
+
schema, 403 permission, 404 endpoint) drop the batch + fire
|
|
65
|
+
`onPermanentFailure` + `console.error` regardless of debug mode.
|
|
66
|
+
No silent infinite-retry-with-growing-backlog.
|
|
67
|
+
- **PII scrub default-on.** Email-shaped and card-number-shaped
|
|
68
|
+
substrings rewritten to `<email>` / `<card>` (sentinel tokens
|
|
69
|
+
match the backend's defence-in-depth scrubber). Recursive — nested
|
|
70
|
+
`{user:{email:...}}` payloads ship scrubbed.
|
|
71
|
+
- **Error self-skip from baseUrl.** Requests to the SDK's own
|
|
72
|
+
Crossdeck endpoint never trigger captureHttp — otherwise a
|
|
73
|
+
Crossdeck outage would recurse forever.
|
|
74
|
+
- **Boot heartbeat.** Verifies the publishable key against the
|
|
75
|
+
Crossdeck API the moment the SDK is constructed. The dashboard's
|
|
76
|
+
"Verify install" check turns green within ~200ms.
|
|
77
|
+
|
|
78
|
+
## Init options
|
|
79
|
+
|
|
80
|
+
| Option | Default | Notes |
|
|
81
|
+
|---|---|---|
|
|
82
|
+
| `appId` | — | **Required.** From the Crossdeck dashboard. |
|
|
83
|
+
| `publicKey` | — | **Required.** `cd_pub_live_…` or `cd_pub_test_…`. |
|
|
84
|
+
| `environment` | — | **Required.** `"production"` or `"sandbox"`. Must match key prefix. |
|
|
85
|
+
| `baseUrl` | `https://api.cross-deck.com/v1` | Override for self-hosted setups. |
|
|
86
|
+
| `persistIdentity` | `true` | Set false to defer AsyncStorage writes until after a consent gate. |
|
|
87
|
+
| `storage` | AsyncStorage (auto-detected) | Pass a SecureStore / MMKV adapter for higher-security app shells. |
|
|
88
|
+
| `storagePrefix` | `"crossdeck:"` | Key namespace inside the storage adapter. |
|
|
89
|
+
| `autoHeartbeat` | `true` | Disable for CI / tests. |
|
|
90
|
+
| `eventFlushBatchSize` | `20` | Flush when buffer reaches this size. |
|
|
91
|
+
| `eventFlushIntervalMs` | `5000` | Idle interval before flushing a partial batch. |
|
|
92
|
+
| `appVersion` | — | Your app's version (e.g. `"1.2.3"`). Auto-attached to every event as `properties.appVersion`. |
|
|
93
|
+
| `platform` | auto-detected | Override the `Platform.OS` detection. |
|
|
94
|
+
| `timeoutMs` | `15000` | Per-request HTTP timeout. |
|
|
95
|
+
| `debug` | `false` | Verbose diagnostic logging via the §16 debug-signal vocabulary. |
|
|
96
|
+
| `scrubPii` | `true` | Disable only if your pipeline does its own PII redaction downstream. |
|
|
97
|
+
| `errorCapture` | `true` | Disable if you have a separate error tracker (Sentry, Bugsnag) and don't want duplicates. |
|
|
98
|
+
|
|
99
|
+
## Lifecycle
|
|
100
|
+
|
|
101
|
+
`init()` returns void but kicks off async hydration (identity, super-
|
|
102
|
+
props, entitlement cache, persisted event queue). Every async method
|
|
103
|
+
(`identify`, `track`, `flush`, `getEntitlements`, etc.) awaits the
|
|
104
|
+
internal `ready` promise — callers can fire methods immediately after
|
|
105
|
+
`init()` without manual sequencing.
|
|
106
|
+
|
|
107
|
+
Sync methods (`isEntitled`, `getSuperProperties`, `diagnostics`)
|
|
108
|
+
read in-memory state. Until `init()` has fired they return sensible
|
|
109
|
+
empties.
|
|
110
|
+
|
|
111
|
+
## Foreground/background lifecycle (v1.0)
|
|
112
|
+
|
|
113
|
+
v1.0 ships WITHOUT auto-session tracking. Wire your nav library's
|
|
114
|
+
listener into:
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
import { AppState } from "react-native";
|
|
118
|
+
|
|
119
|
+
AppState.addEventListener("change", (state) => {
|
|
120
|
+
if (state === "background") {
|
|
121
|
+
void Crossdeck.flush();
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Auto sessions + deep-link tracking land in v1.1 as opt-in
|
|
127
|
+
`autoTrack: { sessions, deepLinks }`.
|
|
128
|
+
|
|
129
|
+
## Diagnostics
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
Crossdeck.diagnostics();
|
|
133
|
+
// {
|
|
134
|
+
// started: true,
|
|
135
|
+
// anonymousId: "anon_1mqz3…",
|
|
136
|
+
// crossdeckCustomerId: "cdcust_abc",
|
|
137
|
+
// developerUserId: "user_847",
|
|
138
|
+
// sdkVersion: "1.0.0",
|
|
139
|
+
// baseUrl: "https://api.cross-deck.com/v1",
|
|
140
|
+
// platform: "ios",
|
|
141
|
+
// clock: { lastServerTime: 1779…, lastClientTime: 1779…, skewMs: 12 },
|
|
142
|
+
// entitlements: { count: 2, lastUpdated: 1779…, stale: false, listenerErrors: 0 },
|
|
143
|
+
// events: { buffered: 0, dropped: 0, inFlight: 0, lastFlushAt: 1779…, lastError: null, consecutiveFailures: 0, nextRetryAt: null }
|
|
144
|
+
// }
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Documentation
|
|
148
|
+
|
|
149
|
+
- [Full SDK reference](https://cross-deck.com/docs/react-native-sdk)
|
|
150
|
+
- [Identify users](https://cross-deck.com/docs/identify-users)
|
|
151
|
+
- [Track events](https://cross-deck.com/docs/track-events)
|
|
152
|
+
- [Entitlements gating](https://cross-deck.com/docs/entitlements)
|
|
153
|
+
- [Error capture](https://cross-deck.com/docs/errors)
|
|
154
|
+
|
|
155
|
+
## License
|
|
156
|
+
|
|
157
|
+
MIT © VistaApps (Pty) Ltd
|