@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/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) {
|
|
@@ -2371,7 +2532,7 @@ var BreadcrumbBuffer = class {
|
|
|
2371
2532
|
};
|
|
2372
2533
|
|
|
2373
2534
|
// src/_diagnostic-telemetry.ts
|
|
2374
|
-
var DIAGNOSTIC_TELEMETRY_ENDPOINT = "https://api.cross-deck.com/v1/
|
|
2535
|
+
var DIAGNOSTIC_TELEMETRY_ENDPOINT = "https://api.cross-deck.com/v1/events";
|
|
2375
2536
|
var DIAGNOSTIC_TELEMETRY_PUBLISHABLE_KEY = "cd_pub_live_9490e7aa029c432abf";
|
|
2376
2537
|
function isDiagnosticTelemetryEnabled() {
|
|
2377
2538
|
return !DIAGNOSTIC_TELEMETRY_PUBLISHABLE_KEY.startsWith(
|
|
@@ -2409,7 +2570,15 @@ function sendDiagnosticTelemetry(payload) {
|
|
|
2409
2570
|
if (!isDiagnosticTelemetryEnabled()) return;
|
|
2410
2571
|
const filtered = filterDiagnosticPayload(payload);
|
|
2411
2572
|
if (Object.keys(filtered).length === 0) return;
|
|
2412
|
-
const body = JSON.stringify(
|
|
2573
|
+
const body = JSON.stringify({
|
|
2574
|
+
events: [
|
|
2575
|
+
{
|
|
2576
|
+
name: "crossdeck.contract_failed",
|
|
2577
|
+
timestamp: Date.now(),
|
|
2578
|
+
properties: filtered
|
|
2579
|
+
}
|
|
2580
|
+
]
|
|
2581
|
+
});
|
|
2413
2582
|
const f = globalThis.fetch;
|
|
2414
2583
|
if (typeof f !== "function") return;
|
|
2415
2584
|
try {
|
|
@@ -3269,19 +3438,58 @@ var DEFAULT_ERROR_CAPTURE = {
|
|
|
3269
3438
|
"ResizeObserver loop completed with undelivered notifications",
|
|
3270
3439
|
"Non-Error promise rejection captured"
|
|
3271
3440
|
// NOTE: We deliberately do NOT drop cross-origin "Script error."
|
|
3272
|
-
// events here.
|
|
3273
|
-
//
|
|
3274
|
-
//
|
|
3275
|
-
// `
|
|
3276
|
-
//
|
|
3277
|
-
//
|
|
3278
|
-
//
|
|
3279
|
-
//
|
|
3280
|
-
// 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.
|
|
3281
3449
|
],
|
|
3282
3450
|
allowUrls: [],
|
|
3283
3451
|
denyUrls: [
|
|
3284
|
-
//
|
|
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.
|
|
3285
3493
|
/^chrome-extension:\/\//,
|
|
3286
3494
|
/^moz-extension:\/\//,
|
|
3287
3495
|
/^safari-extension:\/\//,
|
|
@@ -4074,7 +4282,12 @@ var CrossdeckClient = class {
|
|
|
4074
4282
|
if (autoTrack.sessions || autoTrack.pageViews) {
|
|
4075
4283
|
const tracker = new AutoTracker(
|
|
4076
4284
|
autoTrack,
|
|
4077
|
-
(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" }
|
|
4078
4291
|
);
|
|
4079
4292
|
this.state.autoTracker = tracker;
|
|
4080
4293
|
tracker.install();
|
|
@@ -4539,6 +4752,12 @@ var CrossdeckClient = class {
|
|
|
4539
4752
|
* true even before the session's first network round-trip. Returns
|
|
4540
4753
|
* false only for a genuinely new install that has never completed a
|
|
4541
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).
|
|
4542
4761
|
*/
|
|
4543
4762
|
isEntitled(key) {
|
|
4544
4763
|
const s = this.requireStarted();
|
|
@@ -4554,8 +4773,21 @@ var CrossdeckClient = class {
|
|
|
4554
4773
|
*
|
|
4555
4774
|
* The listener is invoked AFTER the cache mutates — once after a
|
|
4556
4775
|
* successful `getEntitlements()` warms it, again after `syncPurchases()`
|
|
4557
|
-
* delivers fresh entitlements,
|
|
4558
|
-
*
|
|
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.
|
|
4559
4791
|
*
|
|
4560
4792
|
* It is NOT invoked synchronously on subscribe. Callers that need
|
|
4561
4793
|
* the current state should read it via `isEntitled()` / `listEntitlements()`
|
|
@@ -4664,6 +4896,7 @@ var CrossdeckClient = class {
|
|
|
4664
4896
|
);
|
|
4665
4897
|
}
|
|
4666
4898
|
}
|
|
4899
|
+
s.autoTracker?.markActivity();
|
|
4667
4900
|
const enriched = { ...s.deviceInfo };
|
|
4668
4901
|
const sessionId = s.autoTracker?.currentSessionId;
|
|
4669
4902
|
if (sessionId) enriched.sessionId = sessionId;
|
|
@@ -4709,7 +4942,7 @@ var CrossdeckClient = class {
|
|
|
4709
4942
|
callerProperties: validation.properties,
|
|
4710
4943
|
superProperties: supers,
|
|
4711
4944
|
deviceProperties: s.deviceInfo,
|
|
4712
|
-
mergedProperties:
|
|
4945
|
+
mergedProperties: enriched
|
|
4713
4946
|
});
|
|
4714
4947
|
}
|
|
4715
4948
|
if (!isError && !isWebVital) {
|
|
@@ -4918,6 +5151,25 @@ var CrossdeckClient = class {
|
|
|
4918
5151
|
events: s.events.getStats()
|
|
4919
5152
|
};
|
|
4920
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
|
+
}
|
|
4921
5173
|
// ---------- private helpers ----------
|
|
4922
5174
|
requireStarted() {
|
|
4923
5175
|
if (!this.state) {
|
|
@@ -5239,8 +5491,8 @@ function getErrorCode(code) {
|
|
|
5239
5491
|
}
|
|
5240
5492
|
|
|
5241
5493
|
// src/_contracts-bundled.ts
|
|
5242
|
-
var BUNDLED_IN = "@cross-deck/web@1.
|
|
5243
|
-
var SDK_VERSION2 = "1.
|
|
5494
|
+
var BUNDLED_IN = "@cross-deck/web@1.6.1";
|
|
5495
|
+
var SDK_VERSION2 = "1.6.1";
|
|
5244
5496
|
var BUNDLED_CONTRACTS = Object.freeze([
|
|
5245
5497
|
{
|
|
5246
5498
|
"id": "contract-failed-payload-schema-lock",
|
|
@@ -5362,7 +5614,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
|
|
|
5362
5614
|
"legal/security/index.html#diagnostic",
|
|
5363
5615
|
"legal/sdk-data/index.html#b-diagnostic"
|
|
5364
5616
|
],
|
|
5365
|
-
"bundledIn": "@cross-deck/web@1.
|
|
5617
|
+
"bundledIn": "@cross-deck/web@1.6.1"
|
|
5366
5618
|
},
|
|
5367
5619
|
{
|
|
5368
5620
|
"id": "error-envelope-shape",
|
|
@@ -5401,7 +5653,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
|
|
|
5401
5653
|
],
|
|
5402
5654
|
"registeredAt": "2026-05-26",
|
|
5403
5655
|
"firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 8 (codifies existing contract)",
|
|
5404
|
-
"bundledIn": "@cross-deck/web@1.
|
|
5656
|
+
"bundledIn": "@cross-deck/web@1.6.1"
|
|
5405
5657
|
},
|
|
5406
5658
|
{
|
|
5407
5659
|
"id": "flush-interval-parity",
|
|
@@ -5446,7 +5698,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
|
|
|
5446
5698
|
],
|
|
5447
5699
|
"registeredAt": "2026-05-26",
|
|
5448
5700
|
"firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 3.3",
|
|
5449
|
-
"bundledIn": "@cross-deck/web@1.
|
|
5701
|
+
"bundledIn": "@cross-deck/web@1.6.1"
|
|
5450
5702
|
},
|
|
5451
5703
|
{
|
|
5452
5704
|
"id": "idempotency-key-deterministic",
|
|
@@ -5551,7 +5803,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
|
|
|
5551
5803
|
],
|
|
5552
5804
|
"registeredAt": "2026-05-26",
|
|
5553
5805
|
"firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 2.2.a + 2.2.b + 2.2.c",
|
|
5554
|
-
"bundledIn": "@cross-deck/web@1.
|
|
5806
|
+
"bundledIn": "@cross-deck/web@1.6.1"
|
|
5555
5807
|
},
|
|
5556
5808
|
{
|
|
5557
5809
|
"id": "init-reentry-drains-prior-queue",
|
|
@@ -5578,7 +5830,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
|
|
|
5578
5830
|
],
|
|
5579
5831
|
"registeredAt": "2026-05-26",
|
|
5580
5832
|
"firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 5.5",
|
|
5581
|
-
"bundledIn": "@cross-deck/web@1.
|
|
5833
|
+
"bundledIn": "@cross-deck/web@1.6.1"
|
|
5582
5834
|
},
|
|
5583
5835
|
{
|
|
5584
5836
|
"id": "per-user-cache-isolation",
|
|
@@ -5657,7 +5909,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
|
|
|
5657
5909
|
],
|
|
5658
5910
|
"registeredAt": "2026-05-26",
|
|
5659
5911
|
"firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 1.3 (web/RN) + dogfood-gap fix (swift + android)",
|
|
5660
|
-
"bundledIn": "@cross-deck/web@1.
|
|
5912
|
+
"bundledIn": "@cross-deck/web@1.6.1"
|
|
5661
5913
|
},
|
|
5662
5914
|
{
|
|
5663
5915
|
"id": "sdk-error-codes-catalogue",
|
|
@@ -5697,7 +5949,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
|
|
|
5697
5949
|
],
|
|
5698
5950
|
"registeredAt": "2026-05-26",
|
|
5699
5951
|
"firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 6.2",
|
|
5700
|
-
"bundledIn": "@cross-deck/web@1.
|
|
5952
|
+
"bundledIn": "@cross-deck/web@1.6.1"
|
|
5701
5953
|
},
|
|
5702
5954
|
{
|
|
5703
5955
|
"id": "sync-purchases-funnel-parity",
|
|
@@ -5730,7 +5982,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
|
|
|
5730
5982
|
],
|
|
5731
5983
|
"registeredAt": "2026-05-26",
|
|
5732
5984
|
"firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 3.5",
|
|
5733
|
-
"bundledIn": "@cross-deck/web@1.
|
|
5985
|
+
"bundledIn": "@cross-deck/web@1.6.1"
|
|
5734
5986
|
}
|
|
5735
5987
|
]);
|
|
5736
5988
|
|