@cross-deck/web 1.5.7 → 1.6.1
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 +49 -0
- package/README.md +6 -4
- package/dist/contracts.json +11 -11
- package/dist/crossdeck.umd.min.js +2 -2
- package/dist/crossdeck.umd.min.js.map +1 -1
- package/dist/error-codes.json +1 -1
- package/dist/index.cjs +276 -32
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +40 -5
- package/dist/index.d.ts +40 -5
- package/dist/index.mjs +276 -32
- package/dist/index.mjs.map +1 -1
- package/dist/react.cjs +266 -22
- package/dist/react.cjs.map +1 -1
- package/dist/react.mjs +266 -22
- package/dist/react.mjs.map +1 -1
- package/dist/vue.cjs +266 -22
- package/dist/vue.cjs.map +1 -1
- package/dist/vue.mjs +266 -22
- package/dist/vue.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -232,9 +232,9 @@ declare const CrossdeckContracts: {
|
|
|
232
232
|
/** Filter by lifecycle status. */
|
|
233
233
|
readonly withStatus: (status: ContractStatus) => readonly Contract[];
|
|
234
234
|
/** Semver of the SDK release these contracts were bundled with. */
|
|
235
|
-
readonly sdkVersion: "1.
|
|
235
|
+
readonly sdkVersion: "1.6.1";
|
|
236
236
|
/** Fully-qualified bundle identifier — e.g. `@cross-deck/web@1.4.2`. */
|
|
237
|
-
readonly bundledIn: "@cross-deck/web@1.
|
|
237
|
+
readonly bundledIn: "@cross-deck/web@1.6.1";
|
|
238
238
|
/**
|
|
239
239
|
* Resolve a failing test back to the contract it exercises.
|
|
240
240
|
* Used by test-framework hooks (Vitest `afterEach`, XCTest
|
|
@@ -634,6 +634,12 @@ declare class CrossdeckClient {
|
|
|
634
634
|
* true even before the session's first network round-trip. Returns
|
|
635
635
|
* false only for a genuinely new install that has never completed a
|
|
636
636
|
* getEntitlements(), or for an entitlement past its own validUntil.
|
|
637
|
+
*
|
|
638
|
+
* Throws `not_initialized` (CrossdeckError, type
|
|
639
|
+
* `configuration_error`) if called before `Crossdeck.init()`. The
|
|
640
|
+
* `useEntitlement` React hook and the Vue composable both swallow
|
|
641
|
+
* this and return `false`; bare callers must guard for it (or call
|
|
642
|
+
* after `init()` resolves).
|
|
637
643
|
*/
|
|
638
644
|
isEntitled(key: string): boolean;
|
|
639
645
|
/** Snapshot of the local entitlement cache. */
|
|
@@ -643,8 +649,21 @@ declare class CrossdeckClient {
|
|
|
643
649
|
*
|
|
644
650
|
* The listener is invoked AFTER the cache mutates — once after a
|
|
645
651
|
* successful `getEntitlements()` warms it, again after `syncPurchases()`
|
|
646
|
-
* delivers fresh entitlements,
|
|
647
|
-
*
|
|
652
|
+
* delivers fresh entitlements, once on `reset()` to fire the empty-
|
|
653
|
+
* cache state for logout flows, AND once on `identify()` after the
|
|
654
|
+
* per-user cache slot rotates and re-hydrates from device storage.
|
|
655
|
+
*
|
|
656
|
+
* IMPORTANT — the `identify()` fire is a TRAP if you treat it as
|
|
657
|
+
* authoritative network state. `identify()` does NOT fetch entitlements;
|
|
658
|
+
* it switches the per-user cache slot and rehydrates from device
|
|
659
|
+
* storage (which is empty for a brand-new install, and last-known-good
|
|
660
|
+
* — possibly stale — for a returning user). A listener that gates a
|
|
661
|
+
* paywall on the first fire after an identity switch will read
|
|
662
|
+
* `false` for a paying customer on a fresh device and let them past
|
|
663
|
+
* the gate as free. The network-truth fire is the one that follows
|
|
664
|
+
* the next `getEntitlements()` resolution. Either call
|
|
665
|
+
* `getEntitlements()` explicitly after `identify()`, or have your
|
|
666
|
+
* gating code tolerate the empty-then-populated transition.
|
|
648
667
|
*
|
|
649
668
|
* It is NOT invoked synchronously on subscribe. Callers that need
|
|
650
669
|
* the current state should read it via `isEntitled()` / `listEntitlements()`
|
|
@@ -748,6 +767,22 @@ declare class CrossdeckClient {
|
|
|
748
767
|
* `entitlements`. Pre-start values are sensible empties.
|
|
749
768
|
*/
|
|
750
769
|
diagnostics(): Diagnostics;
|
|
770
|
+
/**
|
|
771
|
+
* The stable reference to hand Stripe at checkout so the resulting
|
|
772
|
+
* purchase attributes to THIS person. Stamp it on the Checkout Session
|
|
773
|
+
* as `metadata.crossdeck_ref` (see docs/connect-stripe) — the platform
|
|
774
|
+
* webhook reads it back, validates it, and attaches the subscription to
|
|
775
|
+
* the right customer instead of stranding it on an anonymous record.
|
|
776
|
+
*
|
|
777
|
+
* Returns the strongest identifier the SDK currently holds, in the same
|
|
778
|
+
* precedence the server resolves by:
|
|
779
|
+
* crossdeckCustomerId (cdcust_…) > developerUserId > anonymousId
|
|
780
|
+
* anonymousId is always present, so this always returns a usable,
|
|
781
|
+
* non-empty reference — even before identify(). Identify the user as
|
|
782
|
+
* early as you can, though, so the reference is their stable identity
|
|
783
|
+
* and not a per-device anon id.
|
|
784
|
+
*/
|
|
785
|
+
getCheckoutReference(): string;
|
|
751
786
|
private requireStarted;
|
|
752
787
|
/**
|
|
753
788
|
* Build the identity query for /v1/entitlements. Priority:
|
|
@@ -886,7 +921,7 @@ declare class MemoryStorage implements KeyValueStorage {
|
|
|
886
921
|
*
|
|
887
922
|
* Do NOT edit by hand — `node scripts/sync-sdk-versions.mjs`.
|
|
888
923
|
*/
|
|
889
|
-
declare const SDK_VERSION = "1.
|
|
924
|
+
declare const SDK_VERSION = "1.6.1";
|
|
890
925
|
declare const SDK_NAME = "@cross-deck/web";
|
|
891
926
|
|
|
892
927
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -232,9 +232,9 @@ declare const CrossdeckContracts: {
|
|
|
232
232
|
/** Filter by lifecycle status. */
|
|
233
233
|
readonly withStatus: (status: ContractStatus) => readonly Contract[];
|
|
234
234
|
/** Semver of the SDK release these contracts were bundled with. */
|
|
235
|
-
readonly sdkVersion: "1.
|
|
235
|
+
readonly sdkVersion: "1.6.1";
|
|
236
236
|
/** Fully-qualified bundle identifier — e.g. `@cross-deck/web@1.4.2`. */
|
|
237
|
-
readonly bundledIn: "@cross-deck/web@1.
|
|
237
|
+
readonly bundledIn: "@cross-deck/web@1.6.1";
|
|
238
238
|
/**
|
|
239
239
|
* Resolve a failing test back to the contract it exercises.
|
|
240
240
|
* Used by test-framework hooks (Vitest `afterEach`, XCTest
|
|
@@ -634,6 +634,12 @@ declare class CrossdeckClient {
|
|
|
634
634
|
* true even before the session's first network round-trip. Returns
|
|
635
635
|
* false only for a genuinely new install that has never completed a
|
|
636
636
|
* getEntitlements(), or for an entitlement past its own validUntil.
|
|
637
|
+
*
|
|
638
|
+
* Throws `not_initialized` (CrossdeckError, type
|
|
639
|
+
* `configuration_error`) if called before `Crossdeck.init()`. The
|
|
640
|
+
* `useEntitlement` React hook and the Vue composable both swallow
|
|
641
|
+
* this and return `false`; bare callers must guard for it (or call
|
|
642
|
+
* after `init()` resolves).
|
|
637
643
|
*/
|
|
638
644
|
isEntitled(key: string): boolean;
|
|
639
645
|
/** Snapshot of the local entitlement cache. */
|
|
@@ -643,8 +649,21 @@ declare class CrossdeckClient {
|
|
|
643
649
|
*
|
|
644
650
|
* The listener is invoked AFTER the cache mutates — once after a
|
|
645
651
|
* successful `getEntitlements()` warms it, again after `syncPurchases()`
|
|
646
|
-
* delivers fresh entitlements,
|
|
647
|
-
*
|
|
652
|
+
* delivers fresh entitlements, once on `reset()` to fire the empty-
|
|
653
|
+
* cache state for logout flows, AND once on `identify()` after the
|
|
654
|
+
* per-user cache slot rotates and re-hydrates from device storage.
|
|
655
|
+
*
|
|
656
|
+
* IMPORTANT — the `identify()` fire is a TRAP if you treat it as
|
|
657
|
+
* authoritative network state. `identify()` does NOT fetch entitlements;
|
|
658
|
+
* it switches the per-user cache slot and rehydrates from device
|
|
659
|
+
* storage (which is empty for a brand-new install, and last-known-good
|
|
660
|
+
* — possibly stale — for a returning user). A listener that gates a
|
|
661
|
+
* paywall on the first fire after an identity switch will read
|
|
662
|
+
* `false` for a paying customer on a fresh device and let them past
|
|
663
|
+
* the gate as free. The network-truth fire is the one that follows
|
|
664
|
+
* the next `getEntitlements()` resolution. Either call
|
|
665
|
+
* `getEntitlements()` explicitly after `identify()`, or have your
|
|
666
|
+
* gating code tolerate the empty-then-populated transition.
|
|
648
667
|
*
|
|
649
668
|
* It is NOT invoked synchronously on subscribe. Callers that need
|
|
650
669
|
* the current state should read it via `isEntitled()` / `listEntitlements()`
|
|
@@ -748,6 +767,22 @@ declare class CrossdeckClient {
|
|
|
748
767
|
* `entitlements`. Pre-start values are sensible empties.
|
|
749
768
|
*/
|
|
750
769
|
diagnostics(): Diagnostics;
|
|
770
|
+
/**
|
|
771
|
+
* The stable reference to hand Stripe at checkout so the resulting
|
|
772
|
+
* purchase attributes to THIS person. Stamp it on the Checkout Session
|
|
773
|
+
* as `metadata.crossdeck_ref` (see docs/connect-stripe) — the platform
|
|
774
|
+
* webhook reads it back, validates it, and attaches the subscription to
|
|
775
|
+
* the right customer instead of stranding it on an anonymous record.
|
|
776
|
+
*
|
|
777
|
+
* Returns the strongest identifier the SDK currently holds, in the same
|
|
778
|
+
* precedence the server resolves by:
|
|
779
|
+
* crossdeckCustomerId (cdcust_…) > developerUserId > anonymousId
|
|
780
|
+
* anonymousId is always present, so this always returns a usable,
|
|
781
|
+
* non-empty reference — even before identify(). Identify the user as
|
|
782
|
+
* early as you can, though, so the reference is their stable identity
|
|
783
|
+
* and not a per-device anon id.
|
|
784
|
+
*/
|
|
785
|
+
getCheckoutReference(): string;
|
|
751
786
|
private requireStarted;
|
|
752
787
|
/**
|
|
753
788
|
* Build the identity query for /v1/entitlements. Priority:
|
|
@@ -886,7 +921,7 @@ declare class MemoryStorage implements KeyValueStorage {
|
|
|
886
921
|
*
|
|
887
922
|
* Do NOT edit by hand — `node scripts/sync-sdk-versions.mjs`.
|
|
888
923
|
*/
|
|
889
|
-
declare const SDK_VERSION = "1.
|
|
924
|
+
declare const SDK_VERSION = "1.6.1";
|
|
890
925
|
declare const SDK_NAME = "@cross-deck/web";
|
|
891
926
|
|
|
892
927
|
/**
|
package/dist/index.mjs
CHANGED
|
@@ -64,7 +64,7 @@ function typeMapForStatus(status) {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
// src/_version.ts
|
|
67
|
-
var SDK_VERSION = "1.
|
|
67
|
+
var SDK_VERSION = "1.6.1";
|
|
68
68
|
var SDK_NAME = "@cross-deck/web";
|
|
69
69
|
|
|
70
70
|
// src/http.ts
|
|
@@ -1413,6 +1413,8 @@ var DEFAULT_AUTO_TRACK = {
|
|
|
1413
1413
|
errors: true
|
|
1414
1414
|
};
|
|
1415
1415
|
var SESSION_RESUME_THRESHOLD_MS = 30 * 60 * 1e3;
|
|
1416
|
+
var SESSION_STORAGE_KEY = "crossdeck:session";
|
|
1417
|
+
var ACTIVITY_PERSIST_THROTTLE_MS = 5e3;
|
|
1416
1418
|
var EMPTY_ACQUISITION = {
|
|
1417
1419
|
utm_source: "",
|
|
1418
1420
|
utm_medium: "",
|
|
@@ -1428,11 +1430,13 @@ var EMPTY_ACQUISITION = {
|
|
|
1428
1430
|
twclid: ""
|
|
1429
1431
|
};
|
|
1430
1432
|
var AutoTracker = class {
|
|
1431
|
-
constructor(cfg, track) {
|
|
1433
|
+
constructor(cfg, track, opts) {
|
|
1432
1434
|
this.cfg = cfg;
|
|
1433
1435
|
this.track = track;
|
|
1434
1436
|
this.session = null;
|
|
1435
1437
|
this.cleanups = [];
|
|
1438
|
+
/** Last time we flushed lastActivityAt to storage (throttle gate). */
|
|
1439
|
+
this.lastPersistAt = 0;
|
|
1436
1440
|
/**
|
|
1437
1441
|
* Stable per-page-view identifier. Minted at every `page.viewed`
|
|
1438
1442
|
* emission and attached to every subsequent event until the next
|
|
@@ -1444,6 +1448,8 @@ var AutoTracker = class {
|
|
|
1444
1448
|
* install if `autoTrack.pageViews !== false`).
|
|
1445
1449
|
*/
|
|
1446
1450
|
this.pageviewId = null;
|
|
1451
|
+
this.storage = opts?.storage ?? null;
|
|
1452
|
+
this.sessionKey = opts?.storageKey ?? SESSION_STORAGE_KEY;
|
|
1447
1453
|
}
|
|
1448
1454
|
install() {
|
|
1449
1455
|
if (!isBrowserSafe()) return;
|
|
@@ -1462,6 +1468,7 @@ var AutoTracker = class {
|
|
|
1462
1468
|
if (this.session && !this.session.endedSent) {
|
|
1463
1469
|
this.emitSessionEnd();
|
|
1464
1470
|
}
|
|
1471
|
+
this.clearStoredSession();
|
|
1465
1472
|
this.session = null;
|
|
1466
1473
|
}
|
|
1467
1474
|
/** Exposed for tests + consumers that want to reset the session manually. */
|
|
@@ -1469,8 +1476,24 @@ var AutoTracker = class {
|
|
|
1469
1476
|
if (this.session && !this.session.endedSent) this.emitSessionEnd();
|
|
1470
1477
|
this.pageviewId = null;
|
|
1471
1478
|
this.session = this.startNewSession();
|
|
1479
|
+
this.persistSession();
|
|
1472
1480
|
this.emitSessionStart();
|
|
1473
1481
|
}
|
|
1482
|
+
/**
|
|
1483
|
+
* Keep the rolling session window alive. Called by the host on EVERY
|
|
1484
|
+
* tracked event (auto or custom) so any activity — not just the
|
|
1485
|
+
* pageviews/clicks AutoTracker emits itself — pushes the 30-min idle
|
|
1486
|
+
* boundary forward. In-memory time is updated exactly; the storage
|
|
1487
|
+
* flush is throttled (pagehide forces a final flush).
|
|
1488
|
+
*/
|
|
1489
|
+
markActivity() {
|
|
1490
|
+
if (!this.session) return;
|
|
1491
|
+
const now = Date.now();
|
|
1492
|
+
this.session.lastActivityAt = now;
|
|
1493
|
+
if (now - this.lastPersistAt >= ACTIVITY_PERSIST_THROTTLE_MS) {
|
|
1494
|
+
this.persistSession();
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1474
1497
|
/** Exposed for inspection/tests — returns the current sessionId (or null if not in a session). */
|
|
1475
1498
|
get currentSessionId() {
|
|
1476
1499
|
return this.session?.sessionId ?? null;
|
|
@@ -1493,26 +1516,43 @@ var AutoTracker = class {
|
|
|
1493
1516
|
}
|
|
1494
1517
|
// ---------- sessions ----------
|
|
1495
1518
|
installSessionTracking() {
|
|
1496
|
-
|
|
1497
|
-
this.
|
|
1519
|
+
const now = Date.now();
|
|
1520
|
+
const stored = this.readStoredSession();
|
|
1521
|
+
if (stored && now - stored.lastActivityAt < SESSION_RESUME_THRESHOLD_MS) {
|
|
1522
|
+
this.session = {
|
|
1523
|
+
sessionId: stored.id,
|
|
1524
|
+
startedAt: stored.startedAt,
|
|
1525
|
+
lastActivityAt: now,
|
|
1526
|
+
hiddenAt: null,
|
|
1527
|
+
endedSent: false,
|
|
1528
|
+
acquisition: stored.acquisition
|
|
1529
|
+
};
|
|
1530
|
+
this.persistSession();
|
|
1531
|
+
} else {
|
|
1532
|
+
this.session = this.startNewSession();
|
|
1533
|
+
this.persistSession();
|
|
1534
|
+
this.emitSessionStart();
|
|
1535
|
+
}
|
|
1498
1536
|
const onVisChange = () => {
|
|
1499
1537
|
if (!this.session) return;
|
|
1500
1538
|
const doc2 = globalThis.document;
|
|
1501
1539
|
if (doc2.visibilityState === "hidden") {
|
|
1502
1540
|
this.session.hiddenAt = Date.now();
|
|
1541
|
+
this.persistSession();
|
|
1503
1542
|
} else if (doc2.visibilityState === "visible") {
|
|
1504
|
-
const
|
|
1505
|
-
if (
|
|
1543
|
+
const idleFor = Date.now() - this.session.lastActivityAt;
|
|
1544
|
+
if (idleFor >= SESSION_RESUME_THRESHOLD_MS) {
|
|
1506
1545
|
this.emitSessionEnd();
|
|
1507
1546
|
this.pageviewId = null;
|
|
1508
1547
|
this.session = this.startNewSession();
|
|
1548
|
+
this.persistSession();
|
|
1509
1549
|
this.emitSessionStart();
|
|
1510
1550
|
} else {
|
|
1511
1551
|
this.session.hiddenAt = null;
|
|
1512
1552
|
}
|
|
1513
1553
|
}
|
|
1514
1554
|
};
|
|
1515
|
-
const onPageHide = () => this.
|
|
1555
|
+
const onPageHide = () => this.persistSession();
|
|
1516
1556
|
const w = globalThis.window;
|
|
1517
1557
|
const doc = globalThis.document;
|
|
1518
1558
|
doc.addEventListener("visibilitychange", onVisChange);
|
|
@@ -1525,14 +1565,63 @@ var AutoTracker = class {
|
|
|
1525
1565
|
});
|
|
1526
1566
|
}
|
|
1527
1567
|
startNewSession() {
|
|
1568
|
+
const now = Date.now();
|
|
1528
1569
|
return {
|
|
1529
1570
|
sessionId: mintSessionId(),
|
|
1530
|
-
startedAt:
|
|
1571
|
+
startedAt: now,
|
|
1572
|
+
lastActivityAt: now,
|
|
1531
1573
|
hiddenAt: null,
|
|
1532
1574
|
endedSent: false,
|
|
1533
1575
|
acquisition: captureAcquisition()
|
|
1534
1576
|
};
|
|
1535
1577
|
}
|
|
1578
|
+
/**
|
|
1579
|
+
* Read the persisted session continuity record. Returns null on no
|
|
1580
|
+
* storage, no record, malformed JSON, or a record missing its required
|
|
1581
|
+
* fields — every failure degrades to "no session to resume", which is
|
|
1582
|
+
* the safe (start-fresh) path.
|
|
1583
|
+
*/
|
|
1584
|
+
readStoredSession() {
|
|
1585
|
+
if (!this.storage) return null;
|
|
1586
|
+
try {
|
|
1587
|
+
const raw = this.storage.getItem(this.sessionKey);
|
|
1588
|
+
if (!raw) return null;
|
|
1589
|
+
const p = JSON.parse(raw);
|
|
1590
|
+
if (!p || typeof p.id !== "string" || typeof p.startedAt !== "number" || typeof p.lastActivityAt !== "number") {
|
|
1591
|
+
return null;
|
|
1592
|
+
}
|
|
1593
|
+
return {
|
|
1594
|
+
id: p.id,
|
|
1595
|
+
startedAt: p.startedAt,
|
|
1596
|
+
lastActivityAt: p.lastActivityAt,
|
|
1597
|
+
acquisition: p.acquisition && typeof p.acquisition === "object" ? p.acquisition : EMPTY_ACQUISITION
|
|
1598
|
+
};
|
|
1599
|
+
} catch {
|
|
1600
|
+
return null;
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
/** Flush the current session to storage. No-op without storage/session. */
|
|
1604
|
+
persistSession() {
|
|
1605
|
+
if (!this.storage || !this.session) return;
|
|
1606
|
+
this.lastPersistAt = Date.now();
|
|
1607
|
+
try {
|
|
1608
|
+
const rec = {
|
|
1609
|
+
id: this.session.sessionId,
|
|
1610
|
+
startedAt: this.session.startedAt,
|
|
1611
|
+
lastActivityAt: this.session.lastActivityAt,
|
|
1612
|
+
acquisition: this.session.acquisition
|
|
1613
|
+
};
|
|
1614
|
+
this.storage.setItem(this.sessionKey, JSON.stringify(rec));
|
|
1615
|
+
} catch {
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
clearStoredSession() {
|
|
1619
|
+
if (!this.storage) return;
|
|
1620
|
+
try {
|
|
1621
|
+
this.storage.removeItem(this.sessionKey);
|
|
1622
|
+
} catch {
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1536
1625
|
emitSessionStart() {
|
|
1537
1626
|
if (!this.session) return;
|
|
1538
1627
|
this.track("session.started", { sessionId: this.session.sessionId });
|
|
@@ -1704,6 +1793,78 @@ function isInsidePasswordField(el) {
|
|
|
1704
1793
|
if (el.closest('input[type="password"]')) return true;
|
|
1705
1794
|
return false;
|
|
1706
1795
|
}
|
|
1796
|
+
var INLINE_LABEL_TAGS = /* @__PURE__ */ new Set([
|
|
1797
|
+
"span",
|
|
1798
|
+
"b",
|
|
1799
|
+
"strong",
|
|
1800
|
+
"em",
|
|
1801
|
+
"i",
|
|
1802
|
+
"small",
|
|
1803
|
+
"mark",
|
|
1804
|
+
"u",
|
|
1805
|
+
"label",
|
|
1806
|
+
"abbr",
|
|
1807
|
+
"time",
|
|
1808
|
+
"bdi",
|
|
1809
|
+
"cite",
|
|
1810
|
+
"code",
|
|
1811
|
+
"kbd",
|
|
1812
|
+
"q",
|
|
1813
|
+
"sub",
|
|
1814
|
+
"sup"
|
|
1815
|
+
]);
|
|
1816
|
+
var NON_LABEL_SUBTREES = /* @__PURE__ */ new Set(["svg", "style", "script", "noscript"]);
|
|
1817
|
+
var CONTAINER_MARKERS = "a[href], button, input, select, textarea, [role='button'], [role='link'], h1, h2, h3, h4, h5, h6";
|
|
1818
|
+
var HEADING_SELECTOR = "h1, h2, h3, h4, h5, h6";
|
|
1819
|
+
function collapseWs(s) {
|
|
1820
|
+
return s.replace(/\s+/g, " ").trim();
|
|
1821
|
+
}
|
|
1822
|
+
function boundaryText(el) {
|
|
1823
|
+
let out = "";
|
|
1824
|
+
const walk = (node) => {
|
|
1825
|
+
const kids = node.childNodes;
|
|
1826
|
+
for (let i = 0; i < kids.length; i++) {
|
|
1827
|
+
const child = kids[i];
|
|
1828
|
+
if (!child) continue;
|
|
1829
|
+
if (child.nodeType === 3) {
|
|
1830
|
+
out += child.textContent || "";
|
|
1831
|
+
} else if (child.nodeType === 1) {
|
|
1832
|
+
if (NON_LABEL_SUBTREES.has(child.tagName.toLowerCase())) continue;
|
|
1833
|
+
out += " ";
|
|
1834
|
+
walk(child);
|
|
1835
|
+
out += " ";
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
};
|
|
1839
|
+
walk(el);
|
|
1840
|
+
return collapseWs(out);
|
|
1841
|
+
}
|
|
1842
|
+
function directLabel(el) {
|
|
1843
|
+
let out = "";
|
|
1844
|
+
const kids = el.childNodes;
|
|
1845
|
+
for (let i = 0; i < kids.length; i++) {
|
|
1846
|
+
const child = kids[i];
|
|
1847
|
+
if (!child) continue;
|
|
1848
|
+
if (child.nodeType === 3) {
|
|
1849
|
+
out += " " + (child.textContent || "");
|
|
1850
|
+
} else if (child.nodeType === 1 && INLINE_LABEL_TAGS.has(child.tagName.toLowerCase())) {
|
|
1851
|
+
out += " " + boundaryText(child);
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
return collapseWs(out);
|
|
1855
|
+
}
|
|
1856
|
+
function visibleLabel(el) {
|
|
1857
|
+
const isContainer = el.querySelector(CONTAINER_MARKERS) !== null;
|
|
1858
|
+
if (!isContainer) return boundaryText(el);
|
|
1859
|
+
const direct = directLabel(el);
|
|
1860
|
+
if (direct) return direct;
|
|
1861
|
+
const heading = el.querySelector(HEADING_SELECTOR);
|
|
1862
|
+
if (heading) {
|
|
1863
|
+
const h = boundaryText(heading);
|
|
1864
|
+
if (h) return h;
|
|
1865
|
+
}
|
|
1866
|
+
return "";
|
|
1867
|
+
}
|
|
1707
1868
|
function extractText(el) {
|
|
1708
1869
|
const clean = (s) => s.replace(/\s+/g, " ").trim();
|
|
1709
1870
|
const explicit = el.getAttribute("data-cd-track") || el.getAttribute("data-track") || el.getAttribute("data-testid");
|
|
@@ -1730,7 +1891,7 @@ function extractText(el) {
|
|
|
1730
1891
|
const t = clean(el.value);
|
|
1731
1892
|
if (t) return t;
|
|
1732
1893
|
}
|
|
1733
|
-
const text =
|
|
1894
|
+
const text = visibleLabel(el);
|
|
1734
1895
|
if (text) return text;
|
|
1735
1896
|
const title = el.getAttribute("title");
|
|
1736
1897
|
if (title) {
|
|
@@ -3277,19 +3438,58 @@ var DEFAULT_ERROR_CAPTURE = {
|
|
|
3277
3438
|
"ResizeObserver loop completed with undelivered notifications",
|
|
3278
3439
|
"Non-Error promise rejection captured"
|
|
3279
3440
|
// NOTE: We deliberately do NOT drop cross-origin "Script error."
|
|
3280
|
-
// events here.
|
|
3281
|
-
//
|
|
3282
|
-
//
|
|
3283
|
-
// `
|
|
3284
|
-
//
|
|
3285
|
-
//
|
|
3286
|
-
//
|
|
3287
|
-
//
|
|
3288
|
-
// init config.
|
|
3441
|
+
// events here. The actionability principle (see denyUrls
|
|
3442
|
+
// comment below) draws the line at "developer cannot act":
|
|
3443
|
+
// cross-origin CORS opacity HAS a real, code-shaped fix (add
|
|
3444
|
+
// `crossorigin="anonymous"` to the script tag + CORS headers
|
|
3445
|
+
// on the script's origin). The dashboard surfaces these with a
|
|
3446
|
+
// `cross_origin` tag pointing at that fix. Apps that genuinely
|
|
3447
|
+
// want them muted can re-add "Script error" to ignoreErrors
|
|
3448
|
+
// via init config.
|
|
3289
3449
|
],
|
|
3290
3450
|
allowUrls: [],
|
|
3291
3451
|
denyUrls: [
|
|
3292
|
-
//
|
|
3452
|
+
// The actionability principle
|
|
3453
|
+
// ---------------------------
|
|
3454
|
+
// Crossdeck's default philosophy is "classify, don't silently
|
|
3455
|
+
// drop" — surfacing errors the developer can fix is more useful
|
|
3456
|
+
// than hiding them. That's right for cross-origin "Script
|
|
3457
|
+
// error." (real, code-shaped CORS fix) and for plain
|
|
3458
|
+
// application bugs (obviously real).
|
|
3459
|
+
//
|
|
3460
|
+
// It's NOT right for events the developer cannot act on.
|
|
3461
|
+
// - A user's installed browser extension throwing inside
|
|
3462
|
+
// its own `chrome-extension://` URL: the developer can't
|
|
3463
|
+
// ship a fix for someone else's extension.
|
|
3464
|
+
// - An ad blocker preventing `googletagmanager.com` from
|
|
3465
|
+
// loading: the developer can't unblock the user's blocker.
|
|
3466
|
+
//
|
|
3467
|
+
// Capturing these creates a noise tab that's always non-empty
|
|
3468
|
+
// and never actionable, which trains the dev to ignore the
|
|
3469
|
+
// noise tab — and then the actionable noise (CORS opacity,
|
|
3470
|
+
// etc.) gets ignored along with it. Same lesson as Crossdeck's
|
|
3471
|
+
// "0-2 notifications a week" discipline: a signal that fires
|
|
3472
|
+
// constantly with nothing actionable behind it stops being a
|
|
3473
|
+
// signal.
|
|
3474
|
+
//
|
|
3475
|
+
// So we drop at the source for the unactionable category, and
|
|
3476
|
+
// keep capturing for the actionable category. Same principle
|
|
3477
|
+
// applied to two structurally different inputs — not a new
|
|
3478
|
+
// philosophy.
|
|
3479
|
+
//
|
|
3480
|
+
// The bootstrap list below is the minimum that ships in code.
|
|
3481
|
+
// The full, versioned list arrives at boot via /v1/config —
|
|
3482
|
+
// see `backend/src/lib/error-noise-deny-list.ts`. The SDK
|
|
3483
|
+
// applies the union of (bootstrap + remote), so a freshly-
|
|
3484
|
+
// installed SDK protects users immediately, and remote updates
|
|
3485
|
+
// (new ad-network domains, new pixel hosts) reach every
|
|
3486
|
+
// install without an SDK release.
|
|
3487
|
+
//
|
|
3488
|
+
// The backend Layer-2 classifier
|
|
3489
|
+
// (`backend/src/api/lib/noise-classifier.ts`) is the safety
|
|
3490
|
+
// net for events that slip past these patterns — older SDK
|
|
3491
|
+
// versions in the wild that haven't fetched the remote list
|
|
3492
|
+
// yet, or brand-new patterns the list hasn't named.
|
|
3293
3493
|
/^chrome-extension:\/\//,
|
|
3294
3494
|
/^moz-extension:\/\//,
|
|
3295
3495
|
/^safari-extension:\/\//,
|
|
@@ -4082,7 +4282,12 @@ var CrossdeckClient = class {
|
|
|
4082
4282
|
if (autoTrack.sessions || autoTrack.pageViews) {
|
|
4083
4283
|
const tracker = new AutoTracker(
|
|
4084
4284
|
autoTrack,
|
|
4085
|
-
(name, properties) => this.track(name, properties)
|
|
4285
|
+
(name, properties) => this.track(name, properties),
|
|
4286
|
+
// Persist session continuity on the SAME adapter as identity, so
|
|
4287
|
+
// a visit survives full-page navigations (multi-page sites
|
|
4288
|
+
// re-install the SDK on every page) and honours the same consent
|
|
4289
|
+
// posture — MemoryStorage when identity persistence is off.
|
|
4290
|
+
{ storage: effectiveStorage, storageKey: opts.storagePrefix + "session" }
|
|
4086
4291
|
);
|
|
4087
4292
|
this.state.autoTracker = tracker;
|
|
4088
4293
|
tracker.install();
|
|
@@ -4547,6 +4752,12 @@ var CrossdeckClient = class {
|
|
|
4547
4752
|
* true even before the session's first network round-trip. Returns
|
|
4548
4753
|
* false only for a genuinely new install that has never completed a
|
|
4549
4754
|
* getEntitlements(), or for an entitlement past its own validUntil.
|
|
4755
|
+
*
|
|
4756
|
+
* Throws `not_initialized` (CrossdeckError, type
|
|
4757
|
+
* `configuration_error`) if called before `Crossdeck.init()`. The
|
|
4758
|
+
* `useEntitlement` React hook and the Vue composable both swallow
|
|
4759
|
+
* this and return `false`; bare callers must guard for it (or call
|
|
4760
|
+
* after `init()` resolves).
|
|
4550
4761
|
*/
|
|
4551
4762
|
isEntitled(key) {
|
|
4552
4763
|
const s = this.requireStarted();
|
|
@@ -4562,8 +4773,21 @@ var CrossdeckClient = class {
|
|
|
4562
4773
|
*
|
|
4563
4774
|
* The listener is invoked AFTER the cache mutates — once after a
|
|
4564
4775
|
* successful `getEntitlements()` warms it, again after `syncPurchases()`
|
|
4565
|
-
* delivers fresh entitlements,
|
|
4566
|
-
*
|
|
4776
|
+
* delivers fresh entitlements, once on `reset()` to fire the empty-
|
|
4777
|
+
* cache state for logout flows, AND once on `identify()` after the
|
|
4778
|
+
* per-user cache slot rotates and re-hydrates from device storage.
|
|
4779
|
+
*
|
|
4780
|
+
* IMPORTANT — the `identify()` fire is a TRAP if you treat it as
|
|
4781
|
+
* authoritative network state. `identify()` does NOT fetch entitlements;
|
|
4782
|
+
* it switches the per-user cache slot and rehydrates from device
|
|
4783
|
+
* storage (which is empty for a brand-new install, and last-known-good
|
|
4784
|
+
* — possibly stale — for a returning user). A listener that gates a
|
|
4785
|
+
* paywall on the first fire after an identity switch will read
|
|
4786
|
+
* `false` for a paying customer on a fresh device and let them past
|
|
4787
|
+
* the gate as free. The network-truth fire is the one that follows
|
|
4788
|
+
* the next `getEntitlements()` resolution. Either call
|
|
4789
|
+
* `getEntitlements()` explicitly after `identify()`, or have your
|
|
4790
|
+
* gating code tolerate the empty-then-populated transition.
|
|
4567
4791
|
*
|
|
4568
4792
|
* It is NOT invoked synchronously on subscribe. Callers that need
|
|
4569
4793
|
* the current state should read it via `isEntitled()` / `listEntitlements()`
|
|
@@ -4672,6 +4896,7 @@ var CrossdeckClient = class {
|
|
|
4672
4896
|
);
|
|
4673
4897
|
}
|
|
4674
4898
|
}
|
|
4899
|
+
s.autoTracker?.markActivity();
|
|
4675
4900
|
const enriched = { ...s.deviceInfo };
|
|
4676
4901
|
const sessionId = s.autoTracker?.currentSessionId;
|
|
4677
4902
|
if (sessionId) enriched.sessionId = sessionId;
|
|
@@ -4926,6 +5151,25 @@ var CrossdeckClient = class {
|
|
|
4926
5151
|
events: s.events.getStats()
|
|
4927
5152
|
};
|
|
4928
5153
|
}
|
|
5154
|
+
/**
|
|
5155
|
+
* The stable reference to hand Stripe at checkout so the resulting
|
|
5156
|
+
* purchase attributes to THIS person. Stamp it on the Checkout Session
|
|
5157
|
+
* as `metadata.crossdeck_ref` (see docs/connect-stripe) — the platform
|
|
5158
|
+
* webhook reads it back, validates it, and attaches the subscription to
|
|
5159
|
+
* the right customer instead of stranding it on an anonymous record.
|
|
5160
|
+
*
|
|
5161
|
+
* Returns the strongest identifier the SDK currently holds, in the same
|
|
5162
|
+
* precedence the server resolves by:
|
|
5163
|
+
* crossdeckCustomerId (cdcust_…) > developerUserId > anonymousId
|
|
5164
|
+
* anonymousId is always present, so this always returns a usable,
|
|
5165
|
+
* non-empty reference — even before identify(). Identify the user as
|
|
5166
|
+
* early as you can, though, so the reference is their stable identity
|
|
5167
|
+
* and not a per-device anon id.
|
|
5168
|
+
*/
|
|
5169
|
+
getCheckoutReference() {
|
|
5170
|
+
const s = this.requireStarted();
|
|
5171
|
+
return s.identity.crossdeckCustomerId ?? s.developerUserId ?? s.identity.anonymousId;
|
|
5172
|
+
}
|
|
4929
5173
|
// ---------- private helpers ----------
|
|
4930
5174
|
requireStarted() {
|
|
4931
5175
|
if (!this.state) {
|
|
@@ -5247,8 +5491,8 @@ function getErrorCode(code) {
|
|
|
5247
5491
|
}
|
|
5248
5492
|
|
|
5249
5493
|
// src/_contracts-bundled.ts
|
|
5250
|
-
var BUNDLED_IN = "@cross-deck/web@1.
|
|
5251
|
-
var SDK_VERSION2 = "1.
|
|
5494
|
+
var BUNDLED_IN = "@cross-deck/web@1.6.1";
|
|
5495
|
+
var SDK_VERSION2 = "1.6.1";
|
|
5252
5496
|
var BUNDLED_CONTRACTS = Object.freeze([
|
|
5253
5497
|
{
|
|
5254
5498
|
"id": "contract-failed-payload-schema-lock",
|
|
@@ -5370,7 +5614,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
|
|
|
5370
5614
|
"legal/security/index.html#diagnostic",
|
|
5371
5615
|
"legal/sdk-data/index.html#b-diagnostic"
|
|
5372
5616
|
],
|
|
5373
|
-
"bundledIn": "@cross-deck/web@1.
|
|
5617
|
+
"bundledIn": "@cross-deck/web@1.6.1"
|
|
5374
5618
|
},
|
|
5375
5619
|
{
|
|
5376
5620
|
"id": "error-envelope-shape",
|
|
@@ -5409,7 +5653,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
|
|
|
5409
5653
|
],
|
|
5410
5654
|
"registeredAt": "2026-05-26",
|
|
5411
5655
|
"firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 8 (codifies existing contract)",
|
|
5412
|
-
"bundledIn": "@cross-deck/web@1.
|
|
5656
|
+
"bundledIn": "@cross-deck/web@1.6.1"
|
|
5413
5657
|
},
|
|
5414
5658
|
{
|
|
5415
5659
|
"id": "flush-interval-parity",
|
|
@@ -5454,7 +5698,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
|
|
|
5454
5698
|
],
|
|
5455
5699
|
"registeredAt": "2026-05-26",
|
|
5456
5700
|
"firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 3.3",
|
|
5457
|
-
"bundledIn": "@cross-deck/web@1.
|
|
5701
|
+
"bundledIn": "@cross-deck/web@1.6.1"
|
|
5458
5702
|
},
|
|
5459
5703
|
{
|
|
5460
5704
|
"id": "idempotency-key-deterministic",
|
|
@@ -5559,7 +5803,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
|
|
|
5559
5803
|
],
|
|
5560
5804
|
"registeredAt": "2026-05-26",
|
|
5561
5805
|
"firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 2.2.a + 2.2.b + 2.2.c",
|
|
5562
|
-
"bundledIn": "@cross-deck/web@1.
|
|
5806
|
+
"bundledIn": "@cross-deck/web@1.6.1"
|
|
5563
5807
|
},
|
|
5564
5808
|
{
|
|
5565
5809
|
"id": "init-reentry-drains-prior-queue",
|
|
@@ -5586,7 +5830,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
|
|
|
5586
5830
|
],
|
|
5587
5831
|
"registeredAt": "2026-05-26",
|
|
5588
5832
|
"firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 5.5",
|
|
5589
|
-
"bundledIn": "@cross-deck/web@1.
|
|
5833
|
+
"bundledIn": "@cross-deck/web@1.6.1"
|
|
5590
5834
|
},
|
|
5591
5835
|
{
|
|
5592
5836
|
"id": "per-user-cache-isolation",
|
|
@@ -5665,7 +5909,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
|
|
|
5665
5909
|
],
|
|
5666
5910
|
"registeredAt": "2026-05-26",
|
|
5667
5911
|
"firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 1.3 (web/RN) + dogfood-gap fix (swift + android)",
|
|
5668
|
-
"bundledIn": "@cross-deck/web@1.
|
|
5912
|
+
"bundledIn": "@cross-deck/web@1.6.1"
|
|
5669
5913
|
},
|
|
5670
5914
|
{
|
|
5671
5915
|
"id": "sdk-error-codes-catalogue",
|
|
@@ -5705,7 +5949,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
|
|
|
5705
5949
|
],
|
|
5706
5950
|
"registeredAt": "2026-05-26",
|
|
5707
5951
|
"firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 6.2",
|
|
5708
|
-
"bundledIn": "@cross-deck/web@1.
|
|
5952
|
+
"bundledIn": "@cross-deck/web@1.6.1"
|
|
5709
5953
|
},
|
|
5710
5954
|
{
|
|
5711
5955
|
"id": "sync-purchases-funnel-parity",
|
|
@@ -5738,7 +5982,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
|
|
|
5738
5982
|
],
|
|
5739
5983
|
"registeredAt": "2026-05-26",
|
|
5740
5984
|
"firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 3.5",
|
|
5741
|
-
"bundledIn": "@cross-deck/web@1.
|
|
5985
|
+
"bundledIn": "@cross-deck/web@1.6.1"
|
|
5742
5986
|
}
|
|
5743
5987
|
]);
|
|
5744
5988
|
|