@cross-deck/web 0.2.0 → 0.4.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 +62 -0
- package/README.md +82 -27
- package/dist/{index.js → index.cjs} +269 -17
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.mts +152 -11
- package/dist/index.d.ts +152 -11
- package/dist/index.mjs +268 -16
- package/dist/index.mjs.map +1 -1
- package/dist/react.cjs +1226 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.mts +68 -0
- package/dist/react.d.ts +68 -0
- package/dist/react.mjs +1200 -0
- package/dist/react.mjs.map +1 -0
- package/package.json +17 -1
- package/dist/index.js.map +0 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@cross-deck/web` will be documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
4
|
+
|
|
5
|
+
## [0.4.0] — 2026-05-09
|
|
6
|
+
|
|
7
|
+
Reactive entitlements. Pre-0.4.0, calling `Crossdeck.isEntitled("pro")` directly inside a React render path showed the empty-cache result forever — React had no way to know the cache had populated asynchronously after `init()`. This release closes that gap with a first-class subscribe API on the SDK and a React subpackage that uses it.
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **`Crossdeck.onEntitlementsChange(listener)`** — synchronous subscribe API. Returns an idempotent unsubscribe function. Listeners fire AFTER each cache mutation (`getEntitlements`, `syncPurchases`, `reset`). Listener errors are swallowed. NOT fired on subscribe — read state inline if you need the initial value. See `sdks/SDK_TRUTH.md` for the full contract.
|
|
12
|
+
- **`@cross-deck/web/react` subpath export** — first-class React hooks built on top of the subscribe API:
|
|
13
|
+
- `useEntitlement(key): boolean` — re-renders the component the moment the cache mutates so a JSX snippet like `useEntitlement("pro") && <ProBadge />` actually works.
|
|
14
|
+
- `useEntitlements(): readonly string[]` — reactive list of all active entitlement keys.
|
|
15
|
+
- SSR-safe: hook returns `false` / `[]` on the server and hydrates correctly on the client. Pre-init returns the empty default until `Crossdeck.init()` runs and a cache mutation lands.
|
|
16
|
+
- **`EntitlementCache.subscribe(listener)`** — internal listener API on the cache itself. Powers `onEntitlementsChange`. Iterates over a snapshot of the listener set so listeners that unsubscribe themselves during dispatch don't break the iteration.
|
|
17
|
+
- **Tests** — 7 new cases covering listener semantics: fires on `setFromList`, fires on `clear`, NOT fired on subscribe, idempotent unsubscribe, listener errors are non-fatal, self-unsubscribe-during-dispatch is safe.
|
|
18
|
+
|
|
19
|
+
### Why this exists
|
|
20
|
+
|
|
21
|
+
Without a subscribe API, every framework binding (React, SwiftUI, Compose, Vue, Solid) had to invent its own re-render trigger by polling or hooking into private SDK internals. The cache is the only place that knows precisely when `isEntitled()` would change its answer; making it the source of the notification is the correct contract. iOS and Android SDKs MUST adopt the same pattern internally before 1.0 and MUST expose framework bindings (`@Observable` / SwiftUI for iOS, `StateFlow<Boolean>` / Compose for Android) that mirror the React hook's semantics. See the SDK NorthStar Addendum §11.4.
|
|
22
|
+
|
|
23
|
+
### Build
|
|
24
|
+
|
|
25
|
+
- `tsup` now emits two entry points (`dist/index.{cjs,mjs}` and `dist/react.{cjs,mjs}`) with a custom `outExtension` matching the `package.json` exports map.
|
|
26
|
+
- React is now an optional peer dependency (`react >=18`).
|
|
27
|
+
|
|
28
|
+
### Compatibility
|
|
29
|
+
|
|
30
|
+
Source-compatible with 0.3.0. No breaking changes — `onEntitlementsChange` and the React hooks are purely additive.
|
|
31
|
+
|
|
32
|
+
## [0.3.0] — 2026-05-08
|
|
33
|
+
|
|
34
|
+
This release reconciles the web SDK with the Crossdeck SDK NorthStar Addendum (§4 Shared Contract, §11.1 Web SDK pattern, §13.1 wire envelope, §15 sensitive properties, §16 debug signal vocabulary). The public surface now matches what the iOS, Android, and Node SDKs will expose — `init`, `flush`, `syncPurchases`, `setDebugMode`.
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
|
|
38
|
+
- **`Crossdeck.init({ appId, publicKey, environment })`** — canonical lifecycle method per NorthStar §4. The trio is required and validated up-front: a publishable-key prefix that disagrees with the declared `environment` throws `CrossdeckError({ code: "environment_mismatch" })` at boot, so a typo can't silently route prod data into sandbox dashboards.
|
|
39
|
+
- **`Crossdeck.flush()`** — alias of the old `flushEvents()`, matching the standardised name.
|
|
40
|
+
- **`Crossdeck.syncPurchases(input)`** — replaces `purchaseApple`. Posts to `/v1/purchases/sync` and accepts an optional `rail` field for future Stripe/Google support.
|
|
41
|
+
- **`Crossdeck.setDebugMode(enabled)`** + `debug` init option — toggle the §16 debug signal vocabulary (`sdk.configured`, `sdk.first_event_sent`, `sdk.no_identity`, `sdk.purchase_evidence_sent`, `sdk.environment_mismatch`, `sdk.sensitive_property_warning`).
|
|
42
|
+
- **Sensitive-property warnings** — when debug mode is on, `track()` warns once per call if any property key matches `email|password|token|secret|card|phone` (NorthStar §15). The event is still sent unmodified; the warning surfaces accidental PII in the dashboard onboarding feed.
|
|
43
|
+
- **NorthStar §13.1 wire envelope** — every `/v1/events` POST now includes `appId`, `environment`, and `sdk: { name, version }` at the batch level. The backend validates these against the API-key-resolved app and rejects mismatches with `permission_error / env_mismatch`.
|
|
44
|
+
|
|
45
|
+
### Changed
|
|
46
|
+
|
|
47
|
+
- `Crossdeck.start()` is now a deprecated alias of `init()` and emits a `console.warn` once per call. The signature is unchanged, but the new `appId` and `environment` options are still required even when calling `start`.
|
|
48
|
+
- `Crossdeck.purchaseApple()` is now a deprecated alias of `syncPurchases({ rail: "apple", ... })`. The new method posts to `/v1/purchases/sync`; the legacy `/v1/purchases` route is kept on the backend for v0.2.x callers.
|
|
49
|
+
- The `not_started` configuration error code is now `not_initialized` to match the rename.
|
|
50
|
+
|
|
51
|
+
### Removed
|
|
52
|
+
|
|
53
|
+
Nothing. v0.3.0 is fully source-compatible with v0.2.x callers — the legacy method names log a deprecation but continue to work. Plan to drop them in v0.5.0.
|
|
54
|
+
|
|
55
|
+
## [0.2.0] — 2026-05-06
|
|
56
|
+
|
|
57
|
+
- Added auto-tracking: sessions, page views, and device-info enrichment are on by default in browsers. See `autoTrack` config to disable individually or wholesale.
|
|
58
|
+
- Stable `Diagnostics` shape regardless of whether `start()` has been called — pre-start values are sensible empties.
|
|
59
|
+
|
|
60
|
+
## [0.1.0] — 2026-05-05
|
|
61
|
+
|
|
62
|
+
Initial public release.
|
package/README.md
CHANGED
|
@@ -11,24 +11,66 @@ npm install @cross-deck/web
|
|
|
11
11
|
```ts
|
|
12
12
|
import { Crossdeck } from "@cross-deck/web";
|
|
13
13
|
|
|
14
|
-
// 1. Boot once at app start
|
|
15
|
-
Crossdeck.
|
|
14
|
+
// 1. Boot once at app start. Synchronous and idempotent.
|
|
15
|
+
Crossdeck.init({
|
|
16
|
+
appId: "app_web_xxx", // from the Crossdeck dashboard
|
|
17
|
+
publicKey: "cd_pub_live_…", // publishable key, safe in client code
|
|
18
|
+
environment: "production", // "production" or "sandbox"
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// 2. Telemetry — fire-and-forget, batched in the background.
|
|
22
|
+
Crossdeck.track("paywall_viewed", { variant: "v3" });
|
|
23
|
+
|
|
24
|
+
// 3. Auth + entitlements happen inside an async boot function (or a
|
|
25
|
+
// React useEffect, or any other async context). Top-level await is
|
|
26
|
+
// not portable across all bundlers.
|
|
27
|
+
async function bootCrossdeck() {
|
|
28
|
+
// Wire identify() to YOUR auth state — never hardcode a placeholder.
|
|
29
|
+
await Crossdeck.identify(currentUser.id);
|
|
30
|
+
await Crossdeck.getEntitlements(); // warm the local cache
|
|
31
|
+
|
|
32
|
+
// 4. Sync access checks (microsecond reads from cache).
|
|
33
|
+
if (Crossdeck.isEntitled("pro")) {
|
|
34
|
+
showProFeatures();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
```
|
|
16
38
|
|
|
17
|
-
|
|
18
|
-
await Crossdeck.identify("user_847");
|
|
39
|
+
### React quick start
|
|
19
40
|
|
|
20
|
-
|
|
21
|
-
|
|
41
|
+
For React apps, install Crossdeck once at the root and use the
|
|
42
|
+
`useEntitlement` hook from `@cross-deck/web/react` so components
|
|
43
|
+
re-render when entitlements arrive:
|
|
22
44
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
45
|
+
```tsx
|
|
46
|
+
"use client"
|
|
47
|
+
import { useEffect } from "react";
|
|
48
|
+
import { Crossdeck } from "@cross-deck/web";
|
|
49
|
+
import { useEntitlement } from "@cross-deck/web/react";
|
|
50
|
+
|
|
51
|
+
export function CrossdeckProvider({ children }) {
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
Crossdeck.init({
|
|
54
|
+
appId: "app_web_xxx",
|
|
55
|
+
publicKey: "cd_pub_live_…",
|
|
56
|
+
environment: "production",
|
|
57
|
+
});
|
|
58
|
+
Crossdeck.getEntitlements(); // warm the cache (fire-and-forget)
|
|
59
|
+
}, []);
|
|
60
|
+
return children;
|
|
26
61
|
}
|
|
27
62
|
|
|
28
|
-
|
|
29
|
-
|
|
63
|
+
export function ProBadge() {
|
|
64
|
+
const isPro = useEntitlement("pro");
|
|
65
|
+
return isPro ? <span className="badge">Pro</span> : null;
|
|
66
|
+
}
|
|
30
67
|
```
|
|
31
68
|
|
|
69
|
+
`useEntitlement` subscribes to the SDK's reactive cache via
|
|
70
|
+
`Crossdeck.onEntitlementsChange()`, so every component using the hook
|
|
71
|
+
re-renders the moment entitlements change. SSR-safe: returns `false`
|
|
72
|
+
on the server and hydrates correctly on the client.
|
|
73
|
+
|
|
32
74
|
That's the full happy path.
|
|
33
75
|
|
|
34
76
|
## What it does
|
|
@@ -37,7 +79,7 @@ That's the full happy path.
|
|
|
37
79
|
- **One identity for every device + user.** Pre-login events get an `anonymousId`. After login, `identify()` links them to your user ID through Crossdeck's identity graph. The SDK persists both so subsequent app launches resume where you left off.
|
|
38
80
|
- **Synchronous entitlement reads.** `getEntitlements()` populates a local cache. `isEntitled("pro")` is a Set lookup — no network call, no waiting.
|
|
39
81
|
- **Batched telemetry.** `track()` queues events in memory; the SDK flushes every 5 seconds (configurable) or when the buffer hits 20 events. Network failures re-queue the batch — events aren't lost on a flaky connection.
|
|
40
|
-
- **Boot heartbeat.** On `
|
|
82
|
+
- **Boot heartbeat.** On `init()` the SDK pings `/v1/sdk/heartbeat` so the dashboard's Apps page can show you "last seen" per install. Disable with `autoHeartbeat: false`.
|
|
41
83
|
- **Stripe-style errors.** Every async method throws `CrossdeckError` with `type`, `code`, `requestId`, and `status` — same shape as Stripe's SDKs, so generic error handlers transfer.
|
|
42
84
|
|
|
43
85
|
## Auto-tracked events
|
|
@@ -67,7 +109,7 @@ Every event's `properties` is enriched with whatever the SDK can detect:
|
|
|
67
109
|
viewportWidth: 1440,
|
|
68
110
|
viewportHeight: 900,
|
|
69
111
|
devicePixelRatio: 2,
|
|
70
|
-
appVersion: "1.2.3", // only when you set Crossdeck.
|
|
112
|
+
appVersion: "1.2.3", // only when you set Crossdeck.init({ appVersion })
|
|
71
113
|
}
|
|
72
114
|
```
|
|
73
115
|
|
|
@@ -75,13 +117,15 @@ No fingerprinting, no IP collection on the event document, no canvas hashing. Pr
|
|
|
75
117
|
|
|
76
118
|
## API
|
|
77
119
|
|
|
78
|
-
### `Crossdeck.
|
|
120
|
+
### `Crossdeck.init(options)`
|
|
79
121
|
|
|
80
|
-
Boot the client. Idempotent — calling twice with the same options is fine.
|
|
122
|
+
Boot the client. Idempotent — calling twice with the same options is fine. (`Crossdeck.start()` is kept as a deprecated alias for v0.2.x callers.)
|
|
81
123
|
|
|
82
124
|
```ts
|
|
83
|
-
Crossdeck.
|
|
125
|
+
Crossdeck.init({
|
|
126
|
+
appId: "app_web_xxx", // required — from the dashboard
|
|
84
127
|
publicKey: "cd_pub_live_…", // required
|
|
128
|
+
environment: "production", // required — "production" | "sandbox"
|
|
85
129
|
baseUrl: "https://api.cross-deck.com/v1", // override for self-host or emulator
|
|
86
130
|
appVersion: "1.2.3", // attached to every event as properties.appVersion
|
|
87
131
|
autoTrack: true, // default — sessions, page views, device info
|
|
@@ -90,9 +134,12 @@ Crossdeck.start({
|
|
|
90
134
|
eventFlushBatchSize: 20, // default
|
|
91
135
|
eventFlushIntervalMs: 5_000, // default
|
|
92
136
|
storage: customStorage, // override the persistence adapter
|
|
137
|
+
debug: false, // verbose §16 debug signals when true
|
|
93
138
|
});
|
|
94
139
|
```
|
|
95
140
|
|
|
141
|
+
Crossdeck checks the key prefix matches `environment`: `cd_pub_test_…` must declare `"sandbox"`, `cd_pub_live_…` must declare `"production"`. Mismatches throw `CrossdeckError({ code: "environment_mismatch" })` at init time so a typo can't silently route prod telemetry into sandbox dashboards.
|
|
142
|
+
|
|
96
143
|
The publishable key is safe to ship in client code. Crossdeck enforces origin allowlists (web), bundle-ID binding (mobile), and rate limits at the edge — see [docs/api-keys](https://cross-deck.com/docs/api-keys/) for the full security model.
|
|
97
144
|
|
|
98
145
|
### `await Crossdeck.identify(userId, options?)`
|
|
@@ -129,24 +176,26 @@ Synchronous read from the local cache. Returns `false` until you've called `getE
|
|
|
129
176
|
|
|
130
177
|
### `Crossdeck.track(name, properties?)`
|
|
131
178
|
|
|
132
|
-
Queue a telemetry event. Returns immediately. Events flush in batches; force a flush with `
|
|
179
|
+
Queue a telemetry event. Returns immediately. Events flush in batches; force a flush with `flush()`:
|
|
133
180
|
|
|
134
181
|
```ts
|
|
135
182
|
Crossdeck.track("checkout_started", { product: "annual_pro" });
|
|
136
183
|
// …later, e.g. before page unload:
|
|
137
184
|
window.addEventListener("beforeunload", () => {
|
|
138
|
-
void Crossdeck.
|
|
185
|
+
void Crossdeck.flush();
|
|
139
186
|
});
|
|
140
187
|
```
|
|
141
188
|
|
|
142
189
|
Event names match `[A-Za-z0-9_.\-:]+`, max 128 chars. Properties are JSON-serialisable, max 8 KB per event after JSON encoding.
|
|
143
190
|
|
|
144
|
-
|
|
191
|
+
In `debug: true` mode the SDK warns (one signal per call) when property keys look like PII — `email`, `password`, `token`, `secret`, `card`, `phone`. Crossdeck never strips fields automatically; the warning is so accidental leaks surface during development, not in prod logs.
|
|
145
192
|
|
|
146
|
-
|
|
193
|
+
### `await Crossdeck.syncPurchases(input)`
|
|
194
|
+
|
|
195
|
+
Forward purchase evidence (Apple StoreKit 2) directly to Crossdeck for verification — closes the purchase-to-entitled latency from seconds to milliseconds (faster than waiting for the App Store webhook). (`purchaseApple()` is kept as a deprecated alias.)
|
|
147
196
|
|
|
148
197
|
```ts
|
|
149
|
-
await Crossdeck.
|
|
198
|
+
await Crossdeck.syncPurchases({
|
|
150
199
|
signedTransactionInfo: transaction.jsonRepresentation, // from StoreKit 2
|
|
151
200
|
signedRenewalInfo: subscription.signedRenewalInfo, // optional
|
|
152
201
|
appAccountToken: "uuid-…", // optional
|
|
@@ -157,15 +206,19 @@ Stripe and Google purchases are verified via webhooks (Stripe Connect platform e
|
|
|
157
206
|
|
|
158
207
|
### `await Crossdeck.heartbeat()`
|
|
159
208
|
|
|
160
|
-
Manually send a heartbeat. Called automatically by `
|
|
209
|
+
Manually send a heartbeat. Called automatically by `init()` unless `autoHeartbeat: false`. Returns the readiness summary the dashboard uses to display SDK installation status.
|
|
161
210
|
|
|
162
211
|
### `Crossdeck.reset()`
|
|
163
212
|
|
|
164
213
|
Wipe persisted identity + entitlement cache + queued events. Call on logout. The next session generates a fresh `anonymousId` and starts a clean identity-graph entry.
|
|
165
214
|
|
|
166
|
-
### `Crossdeck.
|
|
215
|
+
### `Crossdeck.flush()`
|
|
216
|
+
|
|
217
|
+
Force-flush the in-memory event queue. Useful before page unload or when shutting down a script. (`flushEvents()` is kept as a deprecated alias.)
|
|
218
|
+
|
|
219
|
+
### `Crossdeck.setDebugMode(enabled)`
|
|
167
220
|
|
|
168
|
-
|
|
221
|
+
Toggle the verbose debug-signal vocabulary at runtime (NorthStar §16). When enabled, the SDK emits a fixed set of `console.info` lines tagged `[crossdeck:sdk.<signal>]` for `sdk.configured`, `sdk.first_event_sent`, `sdk.no_identity`, `sdk.purchase_evidence_sent`, `sdk.environment_mismatch`, and `sdk.sensitive_property_warning`.
|
|
169
222
|
|
|
170
223
|
### `Crossdeck.diagnostics()`
|
|
171
224
|
|
|
@@ -186,7 +239,7 @@ Diagnostic snapshot — useful for development consoles and bug reports:
|
|
|
186
239
|
|
|
187
240
|
## Errors
|
|
188
241
|
|
|
189
|
-
Every async method can throw `CrossdeckError`. Synchronous methods throw on configuration mistakes (calling before `
|
|
242
|
+
Every async method can throw `CrossdeckError`. Synchronous methods throw on configuration mistakes (calling before `init()`, invalid key prefix, env mismatch).
|
|
190
243
|
|
|
191
244
|
```ts
|
|
192
245
|
import { CrossdeckError } from "@cross-deck/web";
|
|
@@ -220,9 +273,11 @@ The SDK works the same way in Node 18+:
|
|
|
220
273
|
```ts
|
|
221
274
|
import { Crossdeck, MemoryStorage } from "@cross-deck/web";
|
|
222
275
|
|
|
223
|
-
Crossdeck.
|
|
276
|
+
Crossdeck.init({
|
|
277
|
+
appId: process.env.CROSSDECK_APP_ID!,
|
|
224
278
|
publicKey: process.env.CROSSDECK_PUBLIC_KEY!,
|
|
225
|
-
|
|
279
|
+
environment: "sandbox", // or "production"
|
|
280
|
+
storage: new MemoryStorage(), // session-only, no localStorage
|
|
226
281
|
autoHeartbeat: false, // skip the boot ping in scripts
|
|
227
282
|
});
|
|
228
283
|
```
|