@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 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
+ [![npm](https://img.shields.io/npm/v/@cross-deck/react-native.svg)](https://www.npmjs.com/package/@cross-deck/react-native)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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