@cross-deck/web 1.5.6 → 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 +287 -35
- 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 +287 -35
- package/dist/index.mjs.map +1 -1
- package/dist/react.cjs +277 -25
- package/dist/react.cjs.map +1 -1
- package/dist/react.mjs +277 -25
- package/dist/react.mjs.map +1 -1
- package/dist/vue.cjs +277 -25
- package/dist/vue.cjs.map +1 -1
- package/dist/vue.mjs +277 -25
- package/dist/vue.mjs.map +1 -1
- package/package.json +1 -1
package/dist/error-codes.json
CHANGED
package/dist/index.cjs
CHANGED
|
@@ -99,7 +99,7 @@ function typeMapForStatus(status) {
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
// src/_version.ts
|
|
102
|
-
var SDK_VERSION = "1.
|
|
102
|
+
var SDK_VERSION = "1.6.1";
|
|
103
103
|
var SDK_NAME = "@cross-deck/web";
|
|
104
104
|
|
|
105
105
|
// src/http.ts
|
|
@@ -1448,6 +1448,8 @@ var DEFAULT_AUTO_TRACK = {
|
|
|
1448
1448
|
errors: true
|
|
1449
1449
|
};
|
|
1450
1450
|
var SESSION_RESUME_THRESHOLD_MS = 30 * 60 * 1e3;
|
|
1451
|
+
var SESSION_STORAGE_KEY = "crossdeck:session";
|
|
1452
|
+
var ACTIVITY_PERSIST_THROTTLE_MS = 5e3;
|
|
1451
1453
|
var EMPTY_ACQUISITION = {
|
|
1452
1454
|
utm_source: "",
|
|
1453
1455
|
utm_medium: "",
|
|
@@ -1463,11 +1465,13 @@ var EMPTY_ACQUISITION = {
|
|
|
1463
1465
|
twclid: ""
|
|
1464
1466
|
};
|
|
1465
1467
|
var AutoTracker = class {
|
|
1466
|
-
constructor(cfg, track) {
|
|
1468
|
+
constructor(cfg, track, opts) {
|
|
1467
1469
|
this.cfg = cfg;
|
|
1468
1470
|
this.track = track;
|
|
1469
1471
|
this.session = null;
|
|
1470
1472
|
this.cleanups = [];
|
|
1473
|
+
/** Last time we flushed lastActivityAt to storage (throttle gate). */
|
|
1474
|
+
this.lastPersistAt = 0;
|
|
1471
1475
|
/**
|
|
1472
1476
|
* Stable per-page-view identifier. Minted at every `page.viewed`
|
|
1473
1477
|
* emission and attached to every subsequent event until the next
|
|
@@ -1479,6 +1483,8 @@ var AutoTracker = class {
|
|
|
1479
1483
|
* install if `autoTrack.pageViews !== false`).
|
|
1480
1484
|
*/
|
|
1481
1485
|
this.pageviewId = null;
|
|
1486
|
+
this.storage = opts?.storage ?? null;
|
|
1487
|
+
this.sessionKey = opts?.storageKey ?? SESSION_STORAGE_KEY;
|
|
1482
1488
|
}
|
|
1483
1489
|
install() {
|
|
1484
1490
|
if (!isBrowserSafe()) return;
|
|
@@ -1497,6 +1503,7 @@ var AutoTracker = class {
|
|
|
1497
1503
|
if (this.session && !this.session.endedSent) {
|
|
1498
1504
|
this.emitSessionEnd();
|
|
1499
1505
|
}
|
|
1506
|
+
this.clearStoredSession();
|
|
1500
1507
|
this.session = null;
|
|
1501
1508
|
}
|
|
1502
1509
|
/** Exposed for tests + consumers that want to reset the session manually. */
|
|
@@ -1504,8 +1511,24 @@ var AutoTracker = class {
|
|
|
1504
1511
|
if (this.session && !this.session.endedSent) this.emitSessionEnd();
|
|
1505
1512
|
this.pageviewId = null;
|
|
1506
1513
|
this.session = this.startNewSession();
|
|
1514
|
+
this.persistSession();
|
|
1507
1515
|
this.emitSessionStart();
|
|
1508
1516
|
}
|
|
1517
|
+
/**
|
|
1518
|
+
* Keep the rolling session window alive. Called by the host on EVERY
|
|
1519
|
+
* tracked event (auto or custom) so any activity — not just the
|
|
1520
|
+
* pageviews/clicks AutoTracker emits itself — pushes the 30-min idle
|
|
1521
|
+
* boundary forward. In-memory time is updated exactly; the storage
|
|
1522
|
+
* flush is throttled (pagehide forces a final flush).
|
|
1523
|
+
*/
|
|
1524
|
+
markActivity() {
|
|
1525
|
+
if (!this.session) return;
|
|
1526
|
+
const now = Date.now();
|
|
1527
|
+
this.session.lastActivityAt = now;
|
|
1528
|
+
if (now - this.lastPersistAt >= ACTIVITY_PERSIST_THROTTLE_MS) {
|
|
1529
|
+
this.persistSession();
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1509
1532
|
/** Exposed for inspection/tests — returns the current sessionId (or null if not in a session). */
|
|
1510
1533
|
get currentSessionId() {
|
|
1511
1534
|
return this.session?.sessionId ?? null;
|
|
@@ -1528,26 +1551,43 @@ var AutoTracker = class {
|
|
|
1528
1551
|
}
|
|
1529
1552
|
// ---------- sessions ----------
|
|
1530
1553
|
installSessionTracking() {
|
|
1531
|
-
|
|
1532
|
-
this.
|
|
1554
|
+
const now = Date.now();
|
|
1555
|
+
const stored = this.readStoredSession();
|
|
1556
|
+
if (stored && now - stored.lastActivityAt < SESSION_RESUME_THRESHOLD_MS) {
|
|
1557
|
+
this.session = {
|
|
1558
|
+
sessionId: stored.id,
|
|
1559
|
+
startedAt: stored.startedAt,
|
|
1560
|
+
lastActivityAt: now,
|
|
1561
|
+
hiddenAt: null,
|
|
1562
|
+
endedSent: false,
|
|
1563
|
+
acquisition: stored.acquisition
|
|
1564
|
+
};
|
|
1565
|
+
this.persistSession();
|
|
1566
|
+
} else {
|
|
1567
|
+
this.session = this.startNewSession();
|
|
1568
|
+
this.persistSession();
|
|
1569
|
+
this.emitSessionStart();
|
|
1570
|
+
}
|
|
1533
1571
|
const onVisChange = () => {
|
|
1534
1572
|
if (!this.session) return;
|
|
1535
1573
|
const doc2 = globalThis.document;
|
|
1536
1574
|
if (doc2.visibilityState === "hidden") {
|
|
1537
1575
|
this.session.hiddenAt = Date.now();
|
|
1576
|
+
this.persistSession();
|
|
1538
1577
|
} else if (doc2.visibilityState === "visible") {
|
|
1539
|
-
const
|
|
1540
|
-
if (
|
|
1578
|
+
const idleFor = Date.now() - this.session.lastActivityAt;
|
|
1579
|
+
if (idleFor >= SESSION_RESUME_THRESHOLD_MS) {
|
|
1541
1580
|
this.emitSessionEnd();
|
|
1542
1581
|
this.pageviewId = null;
|
|
1543
1582
|
this.session = this.startNewSession();
|
|
1583
|
+
this.persistSession();
|
|
1544
1584
|
this.emitSessionStart();
|
|
1545
1585
|
} else {
|
|
1546
1586
|
this.session.hiddenAt = null;
|
|
1547
1587
|
}
|
|
1548
1588
|
}
|
|
1549
1589
|
};
|
|
1550
|
-
const onPageHide = () => this.
|
|
1590
|
+
const onPageHide = () => this.persistSession();
|
|
1551
1591
|
const w = globalThis.window;
|
|
1552
1592
|
const doc = globalThis.document;
|
|
1553
1593
|
doc.addEventListener("visibilitychange", onVisChange);
|
|
@@ -1560,14 +1600,63 @@ var AutoTracker = class {
|
|
|
1560
1600
|
});
|
|
1561
1601
|
}
|
|
1562
1602
|
startNewSession() {
|
|
1603
|
+
const now = Date.now();
|
|
1563
1604
|
return {
|
|
1564
1605
|
sessionId: mintSessionId(),
|
|
1565
|
-
startedAt:
|
|
1606
|
+
startedAt: now,
|
|
1607
|
+
lastActivityAt: now,
|
|
1566
1608
|
hiddenAt: null,
|
|
1567
1609
|
endedSent: false,
|
|
1568
1610
|
acquisition: captureAcquisition()
|
|
1569
1611
|
};
|
|
1570
1612
|
}
|
|
1613
|
+
/**
|
|
1614
|
+
* Read the persisted session continuity record. Returns null on no
|
|
1615
|
+
* storage, no record, malformed JSON, or a record missing its required
|
|
1616
|
+
* fields — every failure degrades to "no session to resume", which is
|
|
1617
|
+
* the safe (start-fresh) path.
|
|
1618
|
+
*/
|
|
1619
|
+
readStoredSession() {
|
|
1620
|
+
if (!this.storage) return null;
|
|
1621
|
+
try {
|
|
1622
|
+
const raw = this.storage.getItem(this.sessionKey);
|
|
1623
|
+
if (!raw) return null;
|
|
1624
|
+
const p = JSON.parse(raw);
|
|
1625
|
+
if (!p || typeof p.id !== "string" || typeof p.startedAt !== "number" || typeof p.lastActivityAt !== "number") {
|
|
1626
|
+
return null;
|
|
1627
|
+
}
|
|
1628
|
+
return {
|
|
1629
|
+
id: p.id,
|
|
1630
|
+
startedAt: p.startedAt,
|
|
1631
|
+
lastActivityAt: p.lastActivityAt,
|
|
1632
|
+
acquisition: p.acquisition && typeof p.acquisition === "object" ? p.acquisition : EMPTY_ACQUISITION
|
|
1633
|
+
};
|
|
1634
|
+
} catch {
|
|
1635
|
+
return null;
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
/** Flush the current session to storage. No-op without storage/session. */
|
|
1639
|
+
persistSession() {
|
|
1640
|
+
if (!this.storage || !this.session) return;
|
|
1641
|
+
this.lastPersistAt = Date.now();
|
|
1642
|
+
try {
|
|
1643
|
+
const rec = {
|
|
1644
|
+
id: this.session.sessionId,
|
|
1645
|
+
startedAt: this.session.startedAt,
|
|
1646
|
+
lastActivityAt: this.session.lastActivityAt,
|
|
1647
|
+
acquisition: this.session.acquisition
|
|
1648
|
+
};
|
|
1649
|
+
this.storage.setItem(this.sessionKey, JSON.stringify(rec));
|
|
1650
|
+
} catch {
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
clearStoredSession() {
|
|
1654
|
+
if (!this.storage) return;
|
|
1655
|
+
try {
|
|
1656
|
+
this.storage.removeItem(this.sessionKey);
|
|
1657
|
+
} catch {
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1571
1660
|
emitSessionStart() {
|
|
1572
1661
|
if (!this.session) return;
|
|
1573
1662
|
this.track("session.started", { sessionId: this.session.sessionId });
|
|
@@ -1739,6 +1828,78 @@ function isInsidePasswordField(el) {
|
|
|
1739
1828
|
if (el.closest('input[type="password"]')) return true;
|
|
1740
1829
|
return false;
|
|
1741
1830
|
}
|
|
1831
|
+
var INLINE_LABEL_TAGS = /* @__PURE__ */ new Set([
|
|
1832
|
+
"span",
|
|
1833
|
+
"b",
|
|
1834
|
+
"strong",
|
|
1835
|
+
"em",
|
|
1836
|
+
"i",
|
|
1837
|
+
"small",
|
|
1838
|
+
"mark",
|
|
1839
|
+
"u",
|
|
1840
|
+
"label",
|
|
1841
|
+
"abbr",
|
|
1842
|
+
"time",
|
|
1843
|
+
"bdi",
|
|
1844
|
+
"cite",
|
|
1845
|
+
"code",
|
|
1846
|
+
"kbd",
|
|
1847
|
+
"q",
|
|
1848
|
+
"sub",
|
|
1849
|
+
"sup"
|
|
1850
|
+
]);
|
|
1851
|
+
var NON_LABEL_SUBTREES = /* @__PURE__ */ new Set(["svg", "style", "script", "noscript"]);
|
|
1852
|
+
var CONTAINER_MARKERS = "a[href], button, input, select, textarea, [role='button'], [role='link'], h1, h2, h3, h4, h5, h6";
|
|
1853
|
+
var HEADING_SELECTOR = "h1, h2, h3, h4, h5, h6";
|
|
1854
|
+
function collapseWs(s) {
|
|
1855
|
+
return s.replace(/\s+/g, " ").trim();
|
|
1856
|
+
}
|
|
1857
|
+
function boundaryText(el) {
|
|
1858
|
+
let out = "";
|
|
1859
|
+
const walk = (node) => {
|
|
1860
|
+
const kids = node.childNodes;
|
|
1861
|
+
for (let i = 0; i < kids.length; i++) {
|
|
1862
|
+
const child = kids[i];
|
|
1863
|
+
if (!child) continue;
|
|
1864
|
+
if (child.nodeType === 3) {
|
|
1865
|
+
out += child.textContent || "";
|
|
1866
|
+
} else if (child.nodeType === 1) {
|
|
1867
|
+
if (NON_LABEL_SUBTREES.has(child.tagName.toLowerCase())) continue;
|
|
1868
|
+
out += " ";
|
|
1869
|
+
walk(child);
|
|
1870
|
+
out += " ";
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
};
|
|
1874
|
+
walk(el);
|
|
1875
|
+
return collapseWs(out);
|
|
1876
|
+
}
|
|
1877
|
+
function directLabel(el) {
|
|
1878
|
+
let out = "";
|
|
1879
|
+
const kids = el.childNodes;
|
|
1880
|
+
for (let i = 0; i < kids.length; i++) {
|
|
1881
|
+
const child = kids[i];
|
|
1882
|
+
if (!child) continue;
|
|
1883
|
+
if (child.nodeType === 3) {
|
|
1884
|
+
out += " " + (child.textContent || "");
|
|
1885
|
+
} else if (child.nodeType === 1 && INLINE_LABEL_TAGS.has(child.tagName.toLowerCase())) {
|
|
1886
|
+
out += " " + boundaryText(child);
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
return collapseWs(out);
|
|
1890
|
+
}
|
|
1891
|
+
function visibleLabel(el) {
|
|
1892
|
+
const isContainer = el.querySelector(CONTAINER_MARKERS) !== null;
|
|
1893
|
+
if (!isContainer) return boundaryText(el);
|
|
1894
|
+
const direct = directLabel(el);
|
|
1895
|
+
if (direct) return direct;
|
|
1896
|
+
const heading = el.querySelector(HEADING_SELECTOR);
|
|
1897
|
+
if (heading) {
|
|
1898
|
+
const h = boundaryText(heading);
|
|
1899
|
+
if (h) return h;
|
|
1900
|
+
}
|
|
1901
|
+
return "";
|
|
1902
|
+
}
|
|
1742
1903
|
function extractText(el) {
|
|
1743
1904
|
const clean = (s) => s.replace(/\s+/g, " ").trim();
|
|
1744
1905
|
const explicit = el.getAttribute("data-cd-track") || el.getAttribute("data-track") || el.getAttribute("data-testid");
|
|
@@ -1765,7 +1926,7 @@ function extractText(el) {
|
|
|
1765
1926
|
const t = clean(el.value);
|
|
1766
1927
|
if (t) return t;
|
|
1767
1928
|
}
|
|
1768
|
-
const text =
|
|
1929
|
+
const text = visibleLabel(el);
|
|
1769
1930
|
if (text) return text;
|
|
1770
1931
|
const title = el.getAttribute("title");
|
|
1771
1932
|
if (title) {
|
|
@@ -2406,7 +2567,7 @@ var BreadcrumbBuffer = class {
|
|
|
2406
2567
|
};
|
|
2407
2568
|
|
|
2408
2569
|
// src/_diagnostic-telemetry.ts
|
|
2409
|
-
var DIAGNOSTIC_TELEMETRY_ENDPOINT = "https://api.cross-deck.com/v1/
|
|
2570
|
+
var DIAGNOSTIC_TELEMETRY_ENDPOINT = "https://api.cross-deck.com/v1/events";
|
|
2410
2571
|
var DIAGNOSTIC_TELEMETRY_PUBLISHABLE_KEY = "cd_pub_live_9490e7aa029c432abf";
|
|
2411
2572
|
function isDiagnosticTelemetryEnabled() {
|
|
2412
2573
|
return !DIAGNOSTIC_TELEMETRY_PUBLISHABLE_KEY.startsWith(
|
|
@@ -2444,7 +2605,15 @@ function sendDiagnosticTelemetry(payload) {
|
|
|
2444
2605
|
if (!isDiagnosticTelemetryEnabled()) return;
|
|
2445
2606
|
const filtered = filterDiagnosticPayload(payload);
|
|
2446
2607
|
if (Object.keys(filtered).length === 0) return;
|
|
2447
|
-
const body = JSON.stringify(
|
|
2608
|
+
const body = JSON.stringify({
|
|
2609
|
+
events: [
|
|
2610
|
+
{
|
|
2611
|
+
name: "crossdeck.contract_failed",
|
|
2612
|
+
timestamp: Date.now(),
|
|
2613
|
+
properties: filtered
|
|
2614
|
+
}
|
|
2615
|
+
]
|
|
2616
|
+
});
|
|
2448
2617
|
const f = globalThis.fetch;
|
|
2449
2618
|
if (typeof f !== "function") return;
|
|
2450
2619
|
try {
|
|
@@ -3304,19 +3473,58 @@ var DEFAULT_ERROR_CAPTURE = {
|
|
|
3304
3473
|
"ResizeObserver loop completed with undelivered notifications",
|
|
3305
3474
|
"Non-Error promise rejection captured"
|
|
3306
3475
|
// NOTE: We deliberately do NOT drop cross-origin "Script error."
|
|
3307
|
-
// events here.
|
|
3308
|
-
//
|
|
3309
|
-
//
|
|
3310
|
-
// `
|
|
3311
|
-
//
|
|
3312
|
-
//
|
|
3313
|
-
//
|
|
3314
|
-
//
|
|
3315
|
-
// init config.
|
|
3476
|
+
// events here. The actionability principle (see denyUrls
|
|
3477
|
+
// comment below) draws the line at "developer cannot act":
|
|
3478
|
+
// cross-origin CORS opacity HAS a real, code-shaped fix (add
|
|
3479
|
+
// `crossorigin="anonymous"` to the script tag + CORS headers
|
|
3480
|
+
// on the script's origin). The dashboard surfaces these with a
|
|
3481
|
+
// `cross_origin` tag pointing at that fix. Apps that genuinely
|
|
3482
|
+
// want them muted can re-add "Script error" to ignoreErrors
|
|
3483
|
+
// via init config.
|
|
3316
3484
|
],
|
|
3317
3485
|
allowUrls: [],
|
|
3318
3486
|
denyUrls: [
|
|
3319
|
-
//
|
|
3487
|
+
// The actionability principle
|
|
3488
|
+
// ---------------------------
|
|
3489
|
+
// Crossdeck's default philosophy is "classify, don't silently
|
|
3490
|
+
// drop" — surfacing errors the developer can fix is more useful
|
|
3491
|
+
// than hiding them. That's right for cross-origin "Script
|
|
3492
|
+
// error." (real, code-shaped CORS fix) and for plain
|
|
3493
|
+
// application bugs (obviously real).
|
|
3494
|
+
//
|
|
3495
|
+
// It's NOT right for events the developer cannot act on.
|
|
3496
|
+
// - A user's installed browser extension throwing inside
|
|
3497
|
+
// its own `chrome-extension://` URL: the developer can't
|
|
3498
|
+
// ship a fix for someone else's extension.
|
|
3499
|
+
// - An ad blocker preventing `googletagmanager.com` from
|
|
3500
|
+
// loading: the developer can't unblock the user's blocker.
|
|
3501
|
+
//
|
|
3502
|
+
// Capturing these creates a noise tab that's always non-empty
|
|
3503
|
+
// and never actionable, which trains the dev to ignore the
|
|
3504
|
+
// noise tab — and then the actionable noise (CORS opacity,
|
|
3505
|
+
// etc.) gets ignored along with it. Same lesson as Crossdeck's
|
|
3506
|
+
// "0-2 notifications a week" discipline: a signal that fires
|
|
3507
|
+
// constantly with nothing actionable behind it stops being a
|
|
3508
|
+
// signal.
|
|
3509
|
+
//
|
|
3510
|
+
// So we drop at the source for the unactionable category, and
|
|
3511
|
+
// keep capturing for the actionable category. Same principle
|
|
3512
|
+
// applied to two structurally different inputs — not a new
|
|
3513
|
+
// philosophy.
|
|
3514
|
+
//
|
|
3515
|
+
// The bootstrap list below is the minimum that ships in code.
|
|
3516
|
+
// The full, versioned list arrives at boot via /v1/config —
|
|
3517
|
+
// see `backend/src/lib/error-noise-deny-list.ts`. The SDK
|
|
3518
|
+
// applies the union of (bootstrap + remote), so a freshly-
|
|
3519
|
+
// installed SDK protects users immediately, and remote updates
|
|
3520
|
+
// (new ad-network domains, new pixel hosts) reach every
|
|
3521
|
+
// install without an SDK release.
|
|
3522
|
+
//
|
|
3523
|
+
// The backend Layer-2 classifier
|
|
3524
|
+
// (`backend/src/api/lib/noise-classifier.ts`) is the safety
|
|
3525
|
+
// net for events that slip past these patterns — older SDK
|
|
3526
|
+
// versions in the wild that haven't fetched the remote list
|
|
3527
|
+
// yet, or brand-new patterns the list hasn't named.
|
|
3320
3528
|
/^chrome-extension:\/\//,
|
|
3321
3529
|
/^moz-extension:\/\//,
|
|
3322
3530
|
/^safari-extension:\/\//,
|
|
@@ -4109,7 +4317,12 @@ var CrossdeckClient = class {
|
|
|
4109
4317
|
if (autoTrack.sessions || autoTrack.pageViews) {
|
|
4110
4318
|
const tracker = new AutoTracker(
|
|
4111
4319
|
autoTrack,
|
|
4112
|
-
(name, properties) => this.track(name, properties)
|
|
4320
|
+
(name, properties) => this.track(name, properties),
|
|
4321
|
+
// Persist session continuity on the SAME adapter as identity, so
|
|
4322
|
+
// a visit survives full-page navigations (multi-page sites
|
|
4323
|
+
// re-install the SDK on every page) and honours the same consent
|
|
4324
|
+
// posture — MemoryStorage when identity persistence is off.
|
|
4325
|
+
{ storage: effectiveStorage, storageKey: opts.storagePrefix + "session" }
|
|
4113
4326
|
);
|
|
4114
4327
|
this.state.autoTracker = tracker;
|
|
4115
4328
|
tracker.install();
|
|
@@ -4574,6 +4787,12 @@ var CrossdeckClient = class {
|
|
|
4574
4787
|
* true even before the session's first network round-trip. Returns
|
|
4575
4788
|
* false only for a genuinely new install that has never completed a
|
|
4576
4789
|
* getEntitlements(), or for an entitlement past its own validUntil.
|
|
4790
|
+
*
|
|
4791
|
+
* Throws `not_initialized` (CrossdeckError, type
|
|
4792
|
+
* `configuration_error`) if called before `Crossdeck.init()`. The
|
|
4793
|
+
* `useEntitlement` React hook and the Vue composable both swallow
|
|
4794
|
+
* this and return `false`; bare callers must guard for it (or call
|
|
4795
|
+
* after `init()` resolves).
|
|
4577
4796
|
*/
|
|
4578
4797
|
isEntitled(key) {
|
|
4579
4798
|
const s = this.requireStarted();
|
|
@@ -4589,8 +4808,21 @@ var CrossdeckClient = class {
|
|
|
4589
4808
|
*
|
|
4590
4809
|
* The listener is invoked AFTER the cache mutates — once after a
|
|
4591
4810
|
* successful `getEntitlements()` warms it, again after `syncPurchases()`
|
|
4592
|
-
* delivers fresh entitlements,
|
|
4593
|
-
*
|
|
4811
|
+
* delivers fresh entitlements, once on `reset()` to fire the empty-
|
|
4812
|
+
* cache state for logout flows, AND once on `identify()` after the
|
|
4813
|
+
* per-user cache slot rotates and re-hydrates from device storage.
|
|
4814
|
+
*
|
|
4815
|
+
* IMPORTANT — the `identify()` fire is a TRAP if you treat it as
|
|
4816
|
+
* authoritative network state. `identify()` does NOT fetch entitlements;
|
|
4817
|
+
* it switches the per-user cache slot and rehydrates from device
|
|
4818
|
+
* storage (which is empty for a brand-new install, and last-known-good
|
|
4819
|
+
* — possibly stale — for a returning user). A listener that gates a
|
|
4820
|
+
* paywall on the first fire after an identity switch will read
|
|
4821
|
+
* `false` for a paying customer on a fresh device and let them past
|
|
4822
|
+
* the gate as free. The network-truth fire is the one that follows
|
|
4823
|
+
* the next `getEntitlements()` resolution. Either call
|
|
4824
|
+
* `getEntitlements()` explicitly after `identify()`, or have your
|
|
4825
|
+
* gating code tolerate the empty-then-populated transition.
|
|
4594
4826
|
*
|
|
4595
4827
|
* It is NOT invoked synchronously on subscribe. Callers that need
|
|
4596
4828
|
* the current state should read it via `isEntitled()` / `listEntitlements()`
|
|
@@ -4699,6 +4931,7 @@ var CrossdeckClient = class {
|
|
|
4699
4931
|
);
|
|
4700
4932
|
}
|
|
4701
4933
|
}
|
|
4934
|
+
s.autoTracker?.markActivity();
|
|
4702
4935
|
const enriched = { ...s.deviceInfo };
|
|
4703
4936
|
const sessionId = s.autoTracker?.currentSessionId;
|
|
4704
4937
|
if (sessionId) enriched.sessionId = sessionId;
|
|
@@ -4744,7 +4977,7 @@ var CrossdeckClient = class {
|
|
|
4744
4977
|
callerProperties: validation.properties,
|
|
4745
4978
|
superProperties: supers,
|
|
4746
4979
|
deviceProperties: s.deviceInfo,
|
|
4747
|
-
mergedProperties:
|
|
4980
|
+
mergedProperties: enriched
|
|
4748
4981
|
});
|
|
4749
4982
|
}
|
|
4750
4983
|
if (!isError && !isWebVital) {
|
|
@@ -4953,6 +5186,25 @@ var CrossdeckClient = class {
|
|
|
4953
5186
|
events: s.events.getStats()
|
|
4954
5187
|
};
|
|
4955
5188
|
}
|
|
5189
|
+
/**
|
|
5190
|
+
* The stable reference to hand Stripe at checkout so the resulting
|
|
5191
|
+
* purchase attributes to THIS person. Stamp it on the Checkout Session
|
|
5192
|
+
* as `metadata.crossdeck_ref` (see docs/connect-stripe) — the platform
|
|
5193
|
+
* webhook reads it back, validates it, and attaches the subscription to
|
|
5194
|
+
* the right customer instead of stranding it on an anonymous record.
|
|
5195
|
+
*
|
|
5196
|
+
* Returns the strongest identifier the SDK currently holds, in the same
|
|
5197
|
+
* precedence the server resolves by:
|
|
5198
|
+
* crossdeckCustomerId (cdcust_…) > developerUserId > anonymousId
|
|
5199
|
+
* anonymousId is always present, so this always returns a usable,
|
|
5200
|
+
* non-empty reference — even before identify(). Identify the user as
|
|
5201
|
+
* early as you can, though, so the reference is their stable identity
|
|
5202
|
+
* and not a per-device anon id.
|
|
5203
|
+
*/
|
|
5204
|
+
getCheckoutReference() {
|
|
5205
|
+
const s = this.requireStarted();
|
|
5206
|
+
return s.identity.crossdeckCustomerId ?? s.developerUserId ?? s.identity.anonymousId;
|
|
5207
|
+
}
|
|
4956
5208
|
// ---------- private helpers ----------
|
|
4957
5209
|
requireStarted() {
|
|
4958
5210
|
if (!this.state) {
|
|
@@ -5274,8 +5526,8 @@ function getErrorCode(code) {
|
|
|
5274
5526
|
}
|
|
5275
5527
|
|
|
5276
5528
|
// src/_contracts-bundled.ts
|
|
5277
|
-
var BUNDLED_IN = "@cross-deck/web@1.
|
|
5278
|
-
var SDK_VERSION2 = "1.
|
|
5529
|
+
var BUNDLED_IN = "@cross-deck/web@1.6.1";
|
|
5530
|
+
var SDK_VERSION2 = "1.6.1";
|
|
5279
5531
|
var BUNDLED_CONTRACTS = Object.freeze([
|
|
5280
5532
|
{
|
|
5281
5533
|
"id": "contract-failed-payload-schema-lock",
|
|
@@ -5397,7 +5649,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
|
|
|
5397
5649
|
"legal/security/index.html#diagnostic",
|
|
5398
5650
|
"legal/sdk-data/index.html#b-diagnostic"
|
|
5399
5651
|
],
|
|
5400
|
-
"bundledIn": "@cross-deck/web@1.
|
|
5652
|
+
"bundledIn": "@cross-deck/web@1.6.1"
|
|
5401
5653
|
},
|
|
5402
5654
|
{
|
|
5403
5655
|
"id": "error-envelope-shape",
|
|
@@ -5436,7 +5688,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
|
|
|
5436
5688
|
],
|
|
5437
5689
|
"registeredAt": "2026-05-26",
|
|
5438
5690
|
"firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 8 (codifies existing contract)",
|
|
5439
|
-
"bundledIn": "@cross-deck/web@1.
|
|
5691
|
+
"bundledIn": "@cross-deck/web@1.6.1"
|
|
5440
5692
|
},
|
|
5441
5693
|
{
|
|
5442
5694
|
"id": "flush-interval-parity",
|
|
@@ -5481,7 +5733,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
|
|
|
5481
5733
|
],
|
|
5482
5734
|
"registeredAt": "2026-05-26",
|
|
5483
5735
|
"firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 3.3",
|
|
5484
|
-
"bundledIn": "@cross-deck/web@1.
|
|
5736
|
+
"bundledIn": "@cross-deck/web@1.6.1"
|
|
5485
5737
|
},
|
|
5486
5738
|
{
|
|
5487
5739
|
"id": "idempotency-key-deterministic",
|
|
@@ -5586,7 +5838,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
|
|
|
5586
5838
|
],
|
|
5587
5839
|
"registeredAt": "2026-05-26",
|
|
5588
5840
|
"firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 2.2.a + 2.2.b + 2.2.c",
|
|
5589
|
-
"bundledIn": "@cross-deck/web@1.
|
|
5841
|
+
"bundledIn": "@cross-deck/web@1.6.1"
|
|
5590
5842
|
},
|
|
5591
5843
|
{
|
|
5592
5844
|
"id": "init-reentry-drains-prior-queue",
|
|
@@ -5613,7 +5865,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
|
|
|
5613
5865
|
],
|
|
5614
5866
|
"registeredAt": "2026-05-26",
|
|
5615
5867
|
"firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 5.5",
|
|
5616
|
-
"bundledIn": "@cross-deck/web@1.
|
|
5868
|
+
"bundledIn": "@cross-deck/web@1.6.1"
|
|
5617
5869
|
},
|
|
5618
5870
|
{
|
|
5619
5871
|
"id": "per-user-cache-isolation",
|
|
@@ -5692,7 +5944,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
|
|
|
5692
5944
|
],
|
|
5693
5945
|
"registeredAt": "2026-05-26",
|
|
5694
5946
|
"firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 1.3 (web/RN) + dogfood-gap fix (swift + android)",
|
|
5695
|
-
"bundledIn": "@cross-deck/web@1.
|
|
5947
|
+
"bundledIn": "@cross-deck/web@1.6.1"
|
|
5696
5948
|
},
|
|
5697
5949
|
{
|
|
5698
5950
|
"id": "sdk-error-codes-catalogue",
|
|
@@ -5732,7 +5984,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
|
|
|
5732
5984
|
],
|
|
5733
5985
|
"registeredAt": "2026-05-26",
|
|
5734
5986
|
"firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 6.2",
|
|
5735
|
-
"bundledIn": "@cross-deck/web@1.
|
|
5987
|
+
"bundledIn": "@cross-deck/web@1.6.1"
|
|
5736
5988
|
},
|
|
5737
5989
|
{
|
|
5738
5990
|
"id": "sync-purchases-funnel-parity",
|
|
@@ -5765,7 +6017,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
|
|
|
5765
6017
|
],
|
|
5766
6018
|
"registeredAt": "2026-05-26",
|
|
5767
6019
|
"firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 3.5",
|
|
5768
|
-
"bundledIn": "@cross-deck/web@1.
|
|
6020
|
+
"bundledIn": "@cross-deck/web@1.6.1"
|
|
5769
6021
|
}
|
|
5770
6022
|
]);
|
|
5771
6023
|
|