@cross-deck/react-native 1.0.0 → 1.5.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 +46 -0
- package/README.md +13 -0
- package/dist/contracts.json +371 -0
- package/dist/index.cjs +877 -35
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +156 -2
- package/dist/index.d.ts +156 -2
- package/dist/index.mjs +883 -35
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,52 @@ All notable changes to `@cross-deck/react-native` will be documented
|
|
|
4
4
|
here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
|
|
5
5
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [1.4.2] — 2026-05-26
|
|
8
|
+
|
|
9
|
+
Patch — wire `bundleId` + `packageName` (per-platform identity-
|
|
10
|
+
lock fields declared on `CrossdeckOptions` since v1.3.0) into
|
|
11
|
+
the `InternalState` opts merge. tsc accepted the missing fields
|
|
12
|
+
in monorepo CI because the monorepo test workflow doesn't lint
|
|
13
|
+
the RN SDK — only the Web SDK gets type-checked. The public
|
|
14
|
+
crossdeck-react-native publish workflow DOES run `npm run lint`
|
|
15
|
+
and aborted with TS2322. Fix: default both to empty string in
|
|
16
|
+
the opts initialiser (HTTP layer skips the header when empty;
|
|
17
|
+
backend rejects with bundle_id_not_allowed /
|
|
18
|
+
package_name_not_allowed at first request if the project
|
|
19
|
+
requires the lock — intentional fail-closed). v1.4.1 was
|
|
20
|
+
tagged on crossdeck-react-native but never reached npm.
|
|
21
|
+
**No SDK code changes vs v1.4.0 / v1.4.1**.
|
|
22
|
+
|
|
23
|
+
## [1.4.1] — 2026-05-26
|
|
24
|
+
|
|
25
|
+
Patch — add automated npm publish workflow to the public
|
|
26
|
+
`crossdeck-react-native` repo so future `vX.Y.Z` tag pushes
|
|
27
|
+
auto-publish to npm via OIDC Trusted Publishing (matches the
|
|
28
|
+
existing `crossdeck-web` pattern). No SDK code changes vs v1.4.0.
|
|
29
|
+
|
|
30
|
+
**Operator note:** npmjs.com Trusted Publisher rule must be
|
|
31
|
+
configured for `crossdeck-react-native` (owner: VistaApps-za,
|
|
32
|
+
workflow: publish.yml) before the OIDC publish succeeds. First
|
|
33
|
+
publish after this lands will fail with an auth error if the
|
|
34
|
+
rule is missing — that's the prompt to configure it.
|
|
35
|
+
|
|
36
|
+
## [1.4.0] — 2026-05-26
|
|
37
|
+
|
|
38
|
+
**Bank-grade reconciliation release.** Joined the v1.4.0 release line with the rest of the Crossdeck SDK suite. 6-pillar KPMG-style audit closed; every behavioural guarantee registered in the monorepo's `contracts/` directory with a CI-enforced audit job.
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
|
|
42
|
+
- **Per-user entitlement cache isolation.** Storage key is now `crossdeck:entitlements:<sha256(userId)>` — a user-switch on a shared device cannot physically read prior user's cached entitlements even if the in-memory clear is somehow skipped. `reset()` wipes EVERY per-user slot via the persisted index. New pure-JS SHA-256 helper.
|
|
43
|
+
- **Deterministic `Idempotency-Key` on `syncPurchases()`** — same JWS/purchaseToken → same key. Cross-SDK parity oracle CI-pinned.
|
|
44
|
+
- **`PurchaseResult.idempotent_replay?: boolean`** — true when the backend replayed a cached response.
|
|
45
|
+
- **`purchase.completed` event on every successful `syncPurchases()`** — funnel parity with native auto-track.
|
|
46
|
+
- **`setSessionId(sessionId: string | null)`** — host-driven session lifecycle. Call from your AppState change listener so every `track()` event carries the `sessionId` property — funnel parity with the web SDK.
|
|
47
|
+
|
|
48
|
+
### Changed
|
|
49
|
+
|
|
50
|
+
- **`init()` re-entry now drains the prior `EventQueue`'s pending timer** before swapping `this.state`. Pre-1.4.0 the timer fired AFTER the state swap, sending old-init events under new-init identity.
|
|
51
|
+
- **Default event-queue flush interval is now 2000ms** (was 5000ms) — cross-SDK parity.
|
|
52
|
+
|
|
7
53
|
## [1.0.0] — 2026-05-24
|
|
8
54
|
|
|
9
55
|
First public release. Built bank-grade from day one — every audit
|
package/README.md
CHANGED
|
@@ -56,10 +56,23 @@ Every Crossdeck SDK ships these patterns by default:
|
|
|
56
56
|
customer reads as Pro on the FIRST `isEntitled()` after `init()`,
|
|
57
57
|
even on a cold launch with no network. A Crossdeck outage can
|
|
58
58
|
never fail a paying customer down to free.
|
|
59
|
+
- **Per-user cache isolation (v1.4.0).** Every `identify(userId)`
|
|
60
|
+
switches the entitlement cache to a per-user AsyncStorage slot
|
|
61
|
+
(`crossdeck:entitlements:<sha256(userId)>`) and unconditionally
|
|
62
|
+
wipes the in-memory snapshot. A user-switch on a shared device
|
|
63
|
+
CANNOT cross-read a prior user's cache, even if the in-memory
|
|
64
|
+
clear is somehow skipped — the storage keys are physically
|
|
65
|
+
separate. `reset()` then wipes every per-user slot on the device
|
|
66
|
+
(logout-grade).
|
|
59
67
|
- **Queue durability + Stripe-style Idempotency-Key reuse.** Events
|
|
60
68
|
spliced for a flush persist to AsyncStorage with the in-flight
|
|
61
69
|
batch attached, so an app crash mid-flight replays the batch on
|
|
62
70
|
the next launch. Backend dedupes on `(projectId, eventId)`.
|
|
71
|
+
- **Deterministic Idempotency-Key on `syncPurchases` (v1.4.0).**
|
|
72
|
+
Same signed transaction → same key → backend short-circuits
|
|
73
|
+
with `idempotent_replay: true` on retry. A network blip or app
|
|
74
|
+
crash mid-flight that re-fires the same purchase never
|
|
75
|
+
double-processes.
|
|
63
76
|
- **4xx hard-stop.** Permanent failures (401 key revoked, 400/422
|
|
64
77
|
schema, 403 permission, 404 endpoint) drop the batch + fire
|
|
65
78
|
`onPermanentFailure` + `console.error` regardless of debug mode.
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"generatedAt": "2026-05-26T13:09:42.134Z",
|
|
4
|
+
"sdk": "@cross-deck/react-native",
|
|
5
|
+
"sdkVersion": "1.5.0",
|
|
6
|
+
"bundledIn": "@cross-deck/react-native@1.5.0",
|
|
7
|
+
"count": 7,
|
|
8
|
+
"contracts": [
|
|
9
|
+
{
|
|
10
|
+
"id": "error-envelope-shape",
|
|
11
|
+
"pillar": "errors",
|
|
12
|
+
"status": "enforced",
|
|
13
|
+
"claim": "Every v1 REST endpoint returns errors in a Stripe-shape envelope: `{ error: { type, code, message, request_id } }` where `type` is one of authentication_error / permission_error / invalid_request_error / rate_limit_error / internal_error (the wire vocabulary in backend/src/api/v1-errors.ts ApiErrorType). HTTP status parity: invalid_request_error → 400, authentication_error → 401, permission_error → 403, rate_limit_error → 429, internal_error → 500. SDK-side clients parse this shape via `crossdeckErrorFromResponse` (Web/Node/RN) / `crossdeckErrorFrom(response:)` (Swift) / `crossdeckErrorFromResponse` (Android) and surface the request_id verbatim so support traces are end-to-end joinable. Firebase callable endpoints (managed-keys / dashboard auth) use the Firebase HttpsError envelope instead — this contract applies to REST /v1/* only.",
|
|
14
|
+
"appliesTo": [
|
|
15
|
+
"web",
|
|
16
|
+
"node",
|
|
17
|
+
"react-native",
|
|
18
|
+
"swift",
|
|
19
|
+
"android",
|
|
20
|
+
"backend"
|
|
21
|
+
],
|
|
22
|
+
"codeRef": [
|
|
23
|
+
"backend/src/api/v1-errors.ts",
|
|
24
|
+
"sdks/web/src/errors.ts",
|
|
25
|
+
"sdks/node/src/errors.ts",
|
|
26
|
+
"sdks/react-native/src/errors.ts",
|
|
27
|
+
"sdks/swift/Sources/Crossdeck/Errors.swift",
|
|
28
|
+
"sdks/android/crossdeck/src/main/kotlin/com/crossdeck/Errors.kt"
|
|
29
|
+
],
|
|
30
|
+
"testRef": [
|
|
31
|
+
{
|
|
32
|
+
"file": "sdks/swift/Tests/CrossdeckTests/ErrorsTests.swift",
|
|
33
|
+
"name": "test_errorEnvelope_fallsBackOnGarbageBody"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"file": "sdks/swift/Tests/CrossdeckTests/ErrorsTests.swift",
|
|
37
|
+
"name": "test_errorEnvelope_reads_XRequestId_fallback"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"file": "sdks/android/crossdeck/src/test/kotlin/com/crossdeck/ErrorTypeWireVocabTest.kt",
|
|
41
|
+
"name": "backend 500 response parses to INTERNAL_ERROR"
|
|
42
|
+
}
|
|
43
|
+
],
|
|
44
|
+
"registeredAt": "2026-05-26",
|
|
45
|
+
"firstRegisteredIn": "bank-grade reconciliation v1.4.0 — phase 8 (codifies existing contract)",
|
|
46
|
+
"bundledIn": "@cross-deck/react-native@1.5.0"
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"id": "flush-interval-parity",
|
|
50
|
+
"pillar": "analytics",
|
|
51
|
+
"status": "enforced",
|
|
52
|
+
"claim": "Every Crossdeck SDK defaults its event-queue flush interval to 2000ms — the Stripe-adjacent industry norm. Pre-v1.4.0 the defaults disagreed (Web/Node 1500ms; RN/Swift/Android 5000ms), so cross-platform funnels saw events landing at different cadences. Per-instance override stays — call sites can still tune it freely.",
|
|
53
|
+
"appliesTo": [
|
|
54
|
+
"web",
|
|
55
|
+
"node",
|
|
56
|
+
"react-native",
|
|
57
|
+
"swift",
|
|
58
|
+
"android"
|
|
59
|
+
],
|
|
60
|
+
"codeRef": [
|
|
61
|
+
"sdks/web/src/crossdeck.ts",
|
|
62
|
+
"sdks/node/src/crossdeck-server.ts",
|
|
63
|
+
"sdks/react-native/src/crossdeck.ts",
|
|
64
|
+
"sdks/swift/Sources/Crossdeck/EventQueue.swift",
|
|
65
|
+
"sdks/android/crossdeck/src/main/kotlin/com/crossdeck/EventQueue.kt"
|
|
66
|
+
],
|
|
67
|
+
"testRef": [
|
|
68
|
+
{
|
|
69
|
+
"file": "sdks/swift/Sources/Crossdeck/EventQueue.swift",
|
|
70
|
+
"name": "flushIntervalMs: Int = 2_000"
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"file": "sdks/android/crossdeck/src/main/kotlin/com/crossdeck/EventQueue.kt",
|
|
74
|
+
"name": "flushIntervalMs: Long = 2_000L"
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"file": "sdks/web/src/crossdeck.ts",
|
|
78
|
+
"name": "options.eventFlushIntervalMs ?? 2000"
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"file": "sdks/node/src/crossdeck-server.ts",
|
|
82
|
+
"name": "options.eventFlushIntervalMs ?? 2000"
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"file": "sdks/react-native/src/crossdeck.ts",
|
|
86
|
+
"name": "options.eventFlushIntervalMs ?? 2000"
|
|
87
|
+
}
|
|
88
|
+
],
|
|
89
|
+
"registeredAt": "2026-05-26",
|
|
90
|
+
"firstRegisteredIn": "bank-grade reconciliation v1.4.0 — phase 3.3",
|
|
91
|
+
"bundledIn": "@cross-deck/react-native@1.5.0"
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"id": "idempotency-key-deterministic",
|
|
95
|
+
"pillar": "revenue",
|
|
96
|
+
"status": "enforced",
|
|
97
|
+
"claim": "syncPurchases() on every SDK derives a deterministic Idempotency-Key from the request body (UUID-shaped SHA-256 of `crossdeck:purchases/sync:<rail>:<jws|token>`). Same input -> same key. Backend short-circuits same-key-same-body retries by returning the cached response (status + body) with `idempotent_replay: true` flag in the body AND `Idempotent-Replayed: true` response header. Same-key-different-body returns 400 `idempotency_key_in_use`. 24-hour TTL matches Stripe. Cache only stores 2xx responses — 4xx/5xx pass through so callers can fix bugs and retry. Helper returns nil/throws on missing identifier (no silent random fallback). Cross-SDK parity is CI-pinned: deriveForPurchase('apple', 'eyJ.jws.sig') MUST equal 'a66b1640-efaf-bb4d-1261-6650033bf111' on every SDK.",
|
|
98
|
+
"appliesTo": [
|
|
99
|
+
"web",
|
|
100
|
+
"node",
|
|
101
|
+
"react-native",
|
|
102
|
+
"swift",
|
|
103
|
+
"android",
|
|
104
|
+
"backend"
|
|
105
|
+
],
|
|
106
|
+
"codeRef": [
|
|
107
|
+
"sdks/web/src/idempotency-key.ts",
|
|
108
|
+
"sdks/web/src/crossdeck.ts",
|
|
109
|
+
"sdks/react-native/src/idempotency-key.ts",
|
|
110
|
+
"sdks/react-native/src/crossdeck.ts",
|
|
111
|
+
"sdks/node/src/idempotency-key.ts",
|
|
112
|
+
"sdks/node/src/crossdeck-server.ts",
|
|
113
|
+
"sdks/swift/Sources/Crossdeck/IdempotencyKey.swift",
|
|
114
|
+
"sdks/swift/Sources/Crossdeck/Crossdeck.swift",
|
|
115
|
+
"sdks/android/crossdeck/src/main/kotlin/com/crossdeck/IdempotencyKey.kt",
|
|
116
|
+
"sdks/android/crossdeck/src/main/kotlin/com/crossdeck/Crossdeck.kt",
|
|
117
|
+
"backend/src/lib/idempotency-response-cache.ts",
|
|
118
|
+
"backend/src/api/v1-purchases.ts"
|
|
119
|
+
],
|
|
120
|
+
"testRef": [
|
|
121
|
+
{
|
|
122
|
+
"file": "sdks/web/tests/idempotency-key.test.ts",
|
|
123
|
+
"name": "cross-SDK oracle — apple JWS pins canonical vector"
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
"file": "sdks/web/tests/idempotency-key.test.ts",
|
|
127
|
+
"name": "is deterministic: same body twice -> identical key"
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
"file": "sdks/web/tests/idempotency-key.test.ts",
|
|
131
|
+
"name": "same identifier under different rails -> different keys"
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
"file": "sdks/web/tests/idempotency-key.test.ts",
|
|
135
|
+
"name": "never silently falls back to a random key on missing identifier"
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
"file": "sdks/react-native/tests/idempotency-key.test.ts",
|
|
139
|
+
"name": "is deterministic"
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
"file": "sdks/react-native/tests/idempotency-key.test.ts",
|
|
143
|
+
"name": "cross-SDK oracle — apple JWS pins canonical vector"
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
"file": "sdks/node/tests/idempotency-key.test.ts",
|
|
147
|
+
"name": "is deterministic"
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
"file": "sdks/node/tests/idempotency-key.test.ts",
|
|
151
|
+
"name": "rail namespacing prevents cross-rail collisions"
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
"file": "sdks/node/tests/idempotency-key.test.ts",
|
|
155
|
+
"name": "apple JWS produces the canonical pinned UUID across all 5 SDKs"
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
"file": "backend/tests/unit/idempotency-response-cache.test.ts",
|
|
159
|
+
"name": "is deterministic for the same input"
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
"file": "backend/tests/unit/idempotency-response-cache.test.ts",
|
|
163
|
+
"name": "injects idempotent_replay: true into a JSON object body"
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
"file": "backend/tests/unit/idempotency-response-cache.test.ts",
|
|
167
|
+
"name": "matches Stripe's 24-hour idempotency window"
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
"file": "sdks/swift/Tests/CrossdeckTests/IdempotencyKeyTests.swift",
|
|
171
|
+
"name": "test_crossSdkOracle_appleJWS"
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
"file": "sdks/swift/Tests/CrossdeckTests/IdempotencyKeyTests.swift",
|
|
175
|
+
"name": "test_railNamespacing_preventsCrossRailCollisions"
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
"file": "sdks/swift/Tests/CrossdeckTests/IdempotencyKeyTests.swift",
|
|
179
|
+
"name": "test_missingIdentifier_returnsNil"
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
"file": "sdks/android/crossdeck/src/test/kotlin/com/crossdeck/IdempotencyKeyTest.kt",
|
|
183
|
+
"name": "cross-SDK oracle for apple JWS"
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
"file": "sdks/android/crossdeck/src/test/kotlin/com/crossdeck/IdempotencyKeyTest.kt",
|
|
187
|
+
"name": "rail namespacing prevents cross-rail collisions"
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
"file": "sdks/android/crossdeck/src/test/kotlin/com/crossdeck/IdempotencyKeyTest.kt",
|
|
191
|
+
"name": "missing identifier returns null - never silent random fallback"
|
|
192
|
+
}
|
|
193
|
+
],
|
|
194
|
+
"registeredAt": "2026-05-26",
|
|
195
|
+
"firstRegisteredIn": "bank-grade reconciliation v1.4.0 — phase 2.2.a + 2.2.b + 2.2.c",
|
|
196
|
+
"bundledIn": "@cross-deck/react-native@1.5.0"
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
"id": "init-reentry-drains-prior-queue",
|
|
200
|
+
"pillar": "lifecycle",
|
|
201
|
+
"status": "enforced",
|
|
202
|
+
"claim": "Web + RN init() re-entry drains the prior EventQueue's pending setTimeout BEFORE replacing this.state. Pre-v1.4.0 the teardown handled autoTracker/webVitals/errors/unloadFlush but NOT events, so the prior queue's timer would fire AFTER the state swap — sending old-init events against new-init http + identity references (cross-identity leak during HMR / config swap / multi-tenant SDK shells). The teardown CANNOT call persistent.clear() — the durable queue belongs to the SDK lifetime, not the init() lifetime, and a survived crash mid-flush re-hydrates on the next init.",
|
|
203
|
+
"appliesTo": [
|
|
204
|
+
"web",
|
|
205
|
+
"react-native"
|
|
206
|
+
],
|
|
207
|
+
"codeRef": [
|
|
208
|
+
"sdks/web/src/crossdeck.ts",
|
|
209
|
+
"sdks/react-native/src/crossdeck.ts"
|
|
210
|
+
],
|
|
211
|
+
"testRef": [
|
|
212
|
+
{
|
|
213
|
+
"file": "sdks/web/tests/init-reentry.test.ts",
|
|
214
|
+
"name": "re-init drains the prior queue's pending timer before swapping state"
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
"file": "sdks/web/tests/init-reentry.test.ts",
|
|
218
|
+
"name": "re-init does NOT wipe the durable event store"
|
|
219
|
+
}
|
|
220
|
+
],
|
|
221
|
+
"registeredAt": "2026-05-26",
|
|
222
|
+
"firstRegisteredIn": "bank-grade reconciliation v1.4.0 — phase 5.5",
|
|
223
|
+
"bundledIn": "@cross-deck/react-native@1.5.0"
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
"id": "per-user-cache-isolation",
|
|
227
|
+
"pillar": "entitlements",
|
|
228
|
+
"status": "enforced",
|
|
229
|
+
"claim": "Every identify(userId) switches the entitlement cache to a physically separate per-user storage slot — `crossdeck:entitlements:<sha256(userId)>` — and unconditionally wipes the in-memory snapshot. A user-switch on a shared device CANNOT cross-read a prior user's cached entitlements, even if the in-memory clear is somehow skipped, because the storage keys are physically separate. reset() wipes every per-user slot via the persisted index.",
|
|
230
|
+
"appliesTo": [
|
|
231
|
+
"web",
|
|
232
|
+
"react-native",
|
|
233
|
+
"swift",
|
|
234
|
+
"android"
|
|
235
|
+
],
|
|
236
|
+
"codeRef": [
|
|
237
|
+
"sdks/web/src/entitlement-cache.ts",
|
|
238
|
+
"sdks/web/src/hash.ts",
|
|
239
|
+
"sdks/web/src/crossdeck.ts",
|
|
240
|
+
"sdks/react-native/src/entitlement-cache.ts",
|
|
241
|
+
"sdks/react-native/src/hash.ts",
|
|
242
|
+
"sdks/react-native/src/crossdeck.ts",
|
|
243
|
+
"sdks/swift/Sources/Crossdeck/EntitlementCache.swift",
|
|
244
|
+
"sdks/swift/Sources/Crossdeck/IdempotencyKey.swift",
|
|
245
|
+
"sdks/swift/Sources/Crossdeck/Crossdeck.swift",
|
|
246
|
+
"sdks/android/crossdeck/src/main/kotlin/com/crossdeck/EntitlementCache.kt",
|
|
247
|
+
"sdks/android/crossdeck/src/main/kotlin/com/crossdeck/IdempotencyKey.kt",
|
|
248
|
+
"sdks/android/crossdeck/src/main/kotlin/com/crossdeck/Crossdeck.kt"
|
|
249
|
+
],
|
|
250
|
+
"testRef": [
|
|
251
|
+
{
|
|
252
|
+
"file": "sdks/web/tests/entitlement-cache-isolation.test.ts",
|
|
253
|
+
"name": "identify(B) makes A's entitlements unreachable from in-memory"
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
"file": "sdks/web/tests/entitlement-cache-isolation.test.ts",
|
|
257
|
+
"name": "clearAll() removes every per-user storage key plus the index"
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
"file": "sdks/web/tests/entitlement-cache-isolation.test.ts",
|
|
261
|
+
"name": "a second cache instance reading A's storage suffix CANNOT see B's data"
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
"file": "sdks/react-native/tests/entitlement-cache-isolation.test.ts",
|
|
265
|
+
"name": "identify(B) makes A's entitlements unreachable from in-memory"
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
"file": "sdks/react-native/tests/entitlement-cache-isolation.test.ts",
|
|
269
|
+
"name": "removes every per-user storage key plus the index"
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
"file": "sdks/swift/Tests/CrossdeckTests/EntitlementCacheIsolationTests.swift",
|
|
273
|
+
"name": "test_identifyB_makesAEntitlementsUnreachable"
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
"file": "sdks/swift/Tests/CrossdeckTests/EntitlementCacheIsolationTests.swift",
|
|
277
|
+
"name": "test_identifiedWritesLandUnderPerUserSha256Key"
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
"file": "sdks/swift/Tests/CrossdeckTests/EntitlementCacheIsolationTests.swift",
|
|
281
|
+
"name": "test_clearAll_removesEveryPerUserStorageKeyPlusIndex"
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
"file": "sdks/android/crossdeck/src/test/kotlin/com/crossdeck/EntitlementCacheIsolationTest.kt",
|
|
285
|
+
"name": "identified writes land under per-user sha256 key"
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
"file": "sdks/android/crossdeck/src/test/kotlin/com/crossdeck/EntitlementCacheIsolationTest.kt",
|
|
289
|
+
"name": "identify B makes A entitlements unreachable from in-memory"
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
"file": "sdks/android/crossdeck/src/test/kotlin/com/crossdeck/EntitlementCacheIsolationTest.kt",
|
|
293
|
+
"name": "clearAll removes every per-user storage key plus the index"
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
"file": "sdks/android/crossdeck/src/test/kotlin/com/crossdeck/EntitlementCacheIsolationTest.kt",
|
|
297
|
+
"name": "a fresh cache bound to A's key CANNOT read B's blob"
|
|
298
|
+
}
|
|
299
|
+
],
|
|
300
|
+
"registeredAt": "2026-05-26",
|
|
301
|
+
"firstRegisteredIn": "bank-grade reconciliation v1.4.0 — phase 1.3 (web/RN) + dogfood-gap fix (swift + android)",
|
|
302
|
+
"bundledIn": "@cross-deck/react-native@1.5.0"
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
"id": "rn-session-id-enrichment",
|
|
306
|
+
"pillar": "analytics",
|
|
307
|
+
"status": "enforced",
|
|
308
|
+
"claim": "RN SDK's track() pipeline attaches a `sessionId` property to every event when the host has called `setSessionId(...)` — parity with the web SDK's session-anchored funnel queries. Pre-v1.4.0 the enrichment merged device + super + groups + caller but never carried sessionId, so cross-platform funnels on session anchors returned zero RN rows. The host owns session lifecycle (AppState + nav library); the SDK exposes setSessionId() / setSessionId(null) for the host to drive. Caller-supplied sessionId in properties still wins on conflict (matches the Phase 3.2 caller > super > device precedence chain).",
|
|
309
|
+
"appliesTo": [
|
|
310
|
+
"react-native"
|
|
311
|
+
],
|
|
312
|
+
"codeRef": [
|
|
313
|
+
"sdks/react-native/src/crossdeck.ts"
|
|
314
|
+
],
|
|
315
|
+
"testRef": [
|
|
316
|
+
{
|
|
317
|
+
"file": "sdks/react-native/tests/session-id-enrichment.test.ts",
|
|
318
|
+
"name": "track() events carry sessionId after setSessionId() is called"
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
"file": "sdks/react-native/tests/session-id-enrichment.test.ts",
|
|
322
|
+
"name": "track() events do NOT carry sessionId before setSessionId() is called"
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
"file": "sdks/react-native/tests/session-id-enrichment.test.ts",
|
|
326
|
+
"name": "setSessionId(null) clears the active session"
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
"file": "sdks/react-native/tests/session-id-enrichment.test.ts",
|
|
330
|
+
"name": "caller-supplied sessionId property overrides setSessionId() value (Phase 3.2 precedence)"
|
|
331
|
+
}
|
|
332
|
+
],
|
|
333
|
+
"registeredAt": "2026-05-26",
|
|
334
|
+
"firstRegisteredIn": "bank-grade reconciliation v1.4.0 — phase 3.4",
|
|
335
|
+
"bundledIn": "@cross-deck/react-native@1.5.0"
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
"id": "sync-purchases-funnel-parity",
|
|
339
|
+
"pillar": "analytics",
|
|
340
|
+
"status": "enforced",
|
|
341
|
+
"claim": "Manual syncPurchases() emits a `purchase.completed` analytics event on success across ALL SDKs (Web / Node / RN / Swift / Android). Pre-v1.4.0 only Swift/Android auto-track emitted it — Web/Node/RN manual calls + Swift/Android manual calls fired ZERO analytics. Schema mirrors the auto-track event name + rail/productId/subscriptionId so cross-platform funnels reconcile on every payment path. When the backend short-circuits via the v1.4.0 idempotency cache, the event also carries `idempotent_replay: true`.",
|
|
342
|
+
"appliesTo": [
|
|
343
|
+
"web",
|
|
344
|
+
"node",
|
|
345
|
+
"react-native",
|
|
346
|
+
"swift",
|
|
347
|
+
"android"
|
|
348
|
+
],
|
|
349
|
+
"codeRef": [
|
|
350
|
+
"sdks/web/src/crossdeck.ts",
|
|
351
|
+
"sdks/node/src/crossdeck-server.ts",
|
|
352
|
+
"sdks/react-native/src/crossdeck.ts",
|
|
353
|
+
"sdks/swift/Sources/Crossdeck/Crossdeck.swift",
|
|
354
|
+
"sdks/android/crossdeck/src/main/kotlin/com/crossdeck/Crossdeck.kt"
|
|
355
|
+
],
|
|
356
|
+
"testRef": [
|
|
357
|
+
{
|
|
358
|
+
"file": "sdks/web/tests/sync-purchases-funnel.test.ts",
|
|
359
|
+
"name": "emits purchase.completed after a successful sync"
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
"file": "sdks/web/tests/sync-purchases-funnel.test.ts",
|
|
363
|
+
"name": "carries idempotent_replay=true when backend replied from cache"
|
|
364
|
+
}
|
|
365
|
+
],
|
|
366
|
+
"registeredAt": "2026-05-26",
|
|
367
|
+
"firstRegisteredIn": "bank-grade reconciliation v1.4.0 — phase 3.5",
|
|
368
|
+
"bundledIn": "@cross-deck/react-native@1.5.0"
|
|
369
|
+
}
|
|
370
|
+
]
|
|
371
|
+
}
|