@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/vue.cjs
CHANGED
|
@@ -92,7 +92,7 @@ function typeMapForStatus(status) {
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
// src/_version.ts
|
|
95
|
-
var SDK_VERSION = "1.
|
|
95
|
+
var SDK_VERSION = "1.6.1";
|
|
96
96
|
var SDK_NAME = "@cross-deck/web";
|
|
97
97
|
|
|
98
98
|
// src/http.ts
|
|
@@ -1441,6 +1441,8 @@ var DEFAULT_AUTO_TRACK = {
|
|
|
1441
1441
|
errors: true
|
|
1442
1442
|
};
|
|
1443
1443
|
var SESSION_RESUME_THRESHOLD_MS = 30 * 60 * 1e3;
|
|
1444
|
+
var SESSION_STORAGE_KEY = "crossdeck:session";
|
|
1445
|
+
var ACTIVITY_PERSIST_THROTTLE_MS = 5e3;
|
|
1444
1446
|
var EMPTY_ACQUISITION = {
|
|
1445
1447
|
utm_source: "",
|
|
1446
1448
|
utm_medium: "",
|
|
@@ -1456,11 +1458,13 @@ var EMPTY_ACQUISITION = {
|
|
|
1456
1458
|
twclid: ""
|
|
1457
1459
|
};
|
|
1458
1460
|
var AutoTracker = class {
|
|
1459
|
-
constructor(cfg, track) {
|
|
1461
|
+
constructor(cfg, track, opts) {
|
|
1460
1462
|
this.cfg = cfg;
|
|
1461
1463
|
this.track = track;
|
|
1462
1464
|
this.session = null;
|
|
1463
1465
|
this.cleanups = [];
|
|
1466
|
+
/** Last time we flushed lastActivityAt to storage (throttle gate). */
|
|
1467
|
+
this.lastPersistAt = 0;
|
|
1464
1468
|
/**
|
|
1465
1469
|
* Stable per-page-view identifier. Minted at every `page.viewed`
|
|
1466
1470
|
* emission and attached to every subsequent event until the next
|
|
@@ -1472,6 +1476,8 @@ var AutoTracker = class {
|
|
|
1472
1476
|
* install if `autoTrack.pageViews !== false`).
|
|
1473
1477
|
*/
|
|
1474
1478
|
this.pageviewId = null;
|
|
1479
|
+
this.storage = opts?.storage ?? null;
|
|
1480
|
+
this.sessionKey = opts?.storageKey ?? SESSION_STORAGE_KEY;
|
|
1475
1481
|
}
|
|
1476
1482
|
install() {
|
|
1477
1483
|
if (!isBrowserSafe()) return;
|
|
@@ -1490,6 +1496,7 @@ var AutoTracker = class {
|
|
|
1490
1496
|
if (this.session && !this.session.endedSent) {
|
|
1491
1497
|
this.emitSessionEnd();
|
|
1492
1498
|
}
|
|
1499
|
+
this.clearStoredSession();
|
|
1493
1500
|
this.session = null;
|
|
1494
1501
|
}
|
|
1495
1502
|
/** Exposed for tests + consumers that want to reset the session manually. */
|
|
@@ -1497,8 +1504,24 @@ var AutoTracker = class {
|
|
|
1497
1504
|
if (this.session && !this.session.endedSent) this.emitSessionEnd();
|
|
1498
1505
|
this.pageviewId = null;
|
|
1499
1506
|
this.session = this.startNewSession();
|
|
1507
|
+
this.persistSession();
|
|
1500
1508
|
this.emitSessionStart();
|
|
1501
1509
|
}
|
|
1510
|
+
/**
|
|
1511
|
+
* Keep the rolling session window alive. Called by the host on EVERY
|
|
1512
|
+
* tracked event (auto or custom) so any activity — not just the
|
|
1513
|
+
* pageviews/clicks AutoTracker emits itself — pushes the 30-min idle
|
|
1514
|
+
* boundary forward. In-memory time is updated exactly; the storage
|
|
1515
|
+
* flush is throttled (pagehide forces a final flush).
|
|
1516
|
+
*/
|
|
1517
|
+
markActivity() {
|
|
1518
|
+
if (!this.session) return;
|
|
1519
|
+
const now = Date.now();
|
|
1520
|
+
this.session.lastActivityAt = now;
|
|
1521
|
+
if (now - this.lastPersistAt >= ACTIVITY_PERSIST_THROTTLE_MS) {
|
|
1522
|
+
this.persistSession();
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1502
1525
|
/** Exposed for inspection/tests — returns the current sessionId (or null if not in a session). */
|
|
1503
1526
|
get currentSessionId() {
|
|
1504
1527
|
return this.session?.sessionId ?? null;
|
|
@@ -1521,26 +1544,43 @@ var AutoTracker = class {
|
|
|
1521
1544
|
}
|
|
1522
1545
|
// ---------- sessions ----------
|
|
1523
1546
|
installSessionTracking() {
|
|
1524
|
-
|
|
1525
|
-
this.
|
|
1547
|
+
const now = Date.now();
|
|
1548
|
+
const stored = this.readStoredSession();
|
|
1549
|
+
if (stored && now - stored.lastActivityAt < SESSION_RESUME_THRESHOLD_MS) {
|
|
1550
|
+
this.session = {
|
|
1551
|
+
sessionId: stored.id,
|
|
1552
|
+
startedAt: stored.startedAt,
|
|
1553
|
+
lastActivityAt: now,
|
|
1554
|
+
hiddenAt: null,
|
|
1555
|
+
endedSent: false,
|
|
1556
|
+
acquisition: stored.acquisition
|
|
1557
|
+
};
|
|
1558
|
+
this.persistSession();
|
|
1559
|
+
} else {
|
|
1560
|
+
this.session = this.startNewSession();
|
|
1561
|
+
this.persistSession();
|
|
1562
|
+
this.emitSessionStart();
|
|
1563
|
+
}
|
|
1526
1564
|
const onVisChange = () => {
|
|
1527
1565
|
if (!this.session) return;
|
|
1528
1566
|
const doc2 = globalThis.document;
|
|
1529
1567
|
if (doc2.visibilityState === "hidden") {
|
|
1530
1568
|
this.session.hiddenAt = Date.now();
|
|
1569
|
+
this.persistSession();
|
|
1531
1570
|
} else if (doc2.visibilityState === "visible") {
|
|
1532
|
-
const
|
|
1533
|
-
if (
|
|
1571
|
+
const idleFor = Date.now() - this.session.lastActivityAt;
|
|
1572
|
+
if (idleFor >= SESSION_RESUME_THRESHOLD_MS) {
|
|
1534
1573
|
this.emitSessionEnd();
|
|
1535
1574
|
this.pageviewId = null;
|
|
1536
1575
|
this.session = this.startNewSession();
|
|
1576
|
+
this.persistSession();
|
|
1537
1577
|
this.emitSessionStart();
|
|
1538
1578
|
} else {
|
|
1539
1579
|
this.session.hiddenAt = null;
|
|
1540
1580
|
}
|
|
1541
1581
|
}
|
|
1542
1582
|
};
|
|
1543
|
-
const onPageHide = () => this.
|
|
1583
|
+
const onPageHide = () => this.persistSession();
|
|
1544
1584
|
const w = globalThis.window;
|
|
1545
1585
|
const doc = globalThis.document;
|
|
1546
1586
|
doc.addEventListener("visibilitychange", onVisChange);
|
|
@@ -1553,14 +1593,63 @@ var AutoTracker = class {
|
|
|
1553
1593
|
});
|
|
1554
1594
|
}
|
|
1555
1595
|
startNewSession() {
|
|
1596
|
+
const now = Date.now();
|
|
1556
1597
|
return {
|
|
1557
1598
|
sessionId: mintSessionId(),
|
|
1558
|
-
startedAt:
|
|
1599
|
+
startedAt: now,
|
|
1600
|
+
lastActivityAt: now,
|
|
1559
1601
|
hiddenAt: null,
|
|
1560
1602
|
endedSent: false,
|
|
1561
1603
|
acquisition: captureAcquisition()
|
|
1562
1604
|
};
|
|
1563
1605
|
}
|
|
1606
|
+
/**
|
|
1607
|
+
* Read the persisted session continuity record. Returns null on no
|
|
1608
|
+
* storage, no record, malformed JSON, or a record missing its required
|
|
1609
|
+
* fields — every failure degrades to "no session to resume", which is
|
|
1610
|
+
* the safe (start-fresh) path.
|
|
1611
|
+
*/
|
|
1612
|
+
readStoredSession() {
|
|
1613
|
+
if (!this.storage) return null;
|
|
1614
|
+
try {
|
|
1615
|
+
const raw = this.storage.getItem(this.sessionKey);
|
|
1616
|
+
if (!raw) return null;
|
|
1617
|
+
const p = JSON.parse(raw);
|
|
1618
|
+
if (!p || typeof p.id !== "string" || typeof p.startedAt !== "number" || typeof p.lastActivityAt !== "number") {
|
|
1619
|
+
return null;
|
|
1620
|
+
}
|
|
1621
|
+
return {
|
|
1622
|
+
id: p.id,
|
|
1623
|
+
startedAt: p.startedAt,
|
|
1624
|
+
lastActivityAt: p.lastActivityAt,
|
|
1625
|
+
acquisition: p.acquisition && typeof p.acquisition === "object" ? p.acquisition : EMPTY_ACQUISITION
|
|
1626
|
+
};
|
|
1627
|
+
} catch {
|
|
1628
|
+
return null;
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
/** Flush the current session to storage. No-op without storage/session. */
|
|
1632
|
+
persistSession() {
|
|
1633
|
+
if (!this.storage || !this.session) return;
|
|
1634
|
+
this.lastPersistAt = Date.now();
|
|
1635
|
+
try {
|
|
1636
|
+
const rec = {
|
|
1637
|
+
id: this.session.sessionId,
|
|
1638
|
+
startedAt: this.session.startedAt,
|
|
1639
|
+
lastActivityAt: this.session.lastActivityAt,
|
|
1640
|
+
acquisition: this.session.acquisition
|
|
1641
|
+
};
|
|
1642
|
+
this.storage.setItem(this.sessionKey, JSON.stringify(rec));
|
|
1643
|
+
} catch {
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
clearStoredSession() {
|
|
1647
|
+
if (!this.storage) return;
|
|
1648
|
+
try {
|
|
1649
|
+
this.storage.removeItem(this.sessionKey);
|
|
1650
|
+
} catch {
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1564
1653
|
emitSessionStart() {
|
|
1565
1654
|
if (!this.session) return;
|
|
1566
1655
|
this.track("session.started", { sessionId: this.session.sessionId });
|
|
@@ -1732,6 +1821,78 @@ function isInsidePasswordField(el) {
|
|
|
1732
1821
|
if (el.closest('input[type="password"]')) return true;
|
|
1733
1822
|
return false;
|
|
1734
1823
|
}
|
|
1824
|
+
var INLINE_LABEL_TAGS = /* @__PURE__ */ new Set([
|
|
1825
|
+
"span",
|
|
1826
|
+
"b",
|
|
1827
|
+
"strong",
|
|
1828
|
+
"em",
|
|
1829
|
+
"i",
|
|
1830
|
+
"small",
|
|
1831
|
+
"mark",
|
|
1832
|
+
"u",
|
|
1833
|
+
"label",
|
|
1834
|
+
"abbr",
|
|
1835
|
+
"time",
|
|
1836
|
+
"bdi",
|
|
1837
|
+
"cite",
|
|
1838
|
+
"code",
|
|
1839
|
+
"kbd",
|
|
1840
|
+
"q",
|
|
1841
|
+
"sub",
|
|
1842
|
+
"sup"
|
|
1843
|
+
]);
|
|
1844
|
+
var NON_LABEL_SUBTREES = /* @__PURE__ */ new Set(["svg", "style", "script", "noscript"]);
|
|
1845
|
+
var CONTAINER_MARKERS = "a[href], button, input, select, textarea, [role='button'], [role='link'], h1, h2, h3, h4, h5, h6";
|
|
1846
|
+
var HEADING_SELECTOR = "h1, h2, h3, h4, h5, h6";
|
|
1847
|
+
function collapseWs(s) {
|
|
1848
|
+
return s.replace(/\s+/g, " ").trim();
|
|
1849
|
+
}
|
|
1850
|
+
function boundaryText(el) {
|
|
1851
|
+
let out = "";
|
|
1852
|
+
const walk = (node) => {
|
|
1853
|
+
const kids = node.childNodes;
|
|
1854
|
+
for (let i = 0; i < kids.length; i++) {
|
|
1855
|
+
const child = kids[i];
|
|
1856
|
+
if (!child) continue;
|
|
1857
|
+
if (child.nodeType === 3) {
|
|
1858
|
+
out += child.textContent || "";
|
|
1859
|
+
} else if (child.nodeType === 1) {
|
|
1860
|
+
if (NON_LABEL_SUBTREES.has(child.tagName.toLowerCase())) continue;
|
|
1861
|
+
out += " ";
|
|
1862
|
+
walk(child);
|
|
1863
|
+
out += " ";
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
};
|
|
1867
|
+
walk(el);
|
|
1868
|
+
return collapseWs(out);
|
|
1869
|
+
}
|
|
1870
|
+
function directLabel(el) {
|
|
1871
|
+
let out = "";
|
|
1872
|
+
const kids = el.childNodes;
|
|
1873
|
+
for (let i = 0; i < kids.length; i++) {
|
|
1874
|
+
const child = kids[i];
|
|
1875
|
+
if (!child) continue;
|
|
1876
|
+
if (child.nodeType === 3) {
|
|
1877
|
+
out += " " + (child.textContent || "");
|
|
1878
|
+
} else if (child.nodeType === 1 && INLINE_LABEL_TAGS.has(child.tagName.toLowerCase())) {
|
|
1879
|
+
out += " " + boundaryText(child);
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
return collapseWs(out);
|
|
1883
|
+
}
|
|
1884
|
+
function visibleLabel(el) {
|
|
1885
|
+
const isContainer = el.querySelector(CONTAINER_MARKERS) !== null;
|
|
1886
|
+
if (!isContainer) return boundaryText(el);
|
|
1887
|
+
const direct = directLabel(el);
|
|
1888
|
+
if (direct) return direct;
|
|
1889
|
+
const heading = el.querySelector(HEADING_SELECTOR);
|
|
1890
|
+
if (heading) {
|
|
1891
|
+
const h = boundaryText(heading);
|
|
1892
|
+
if (h) return h;
|
|
1893
|
+
}
|
|
1894
|
+
return "";
|
|
1895
|
+
}
|
|
1735
1896
|
function extractText(el) {
|
|
1736
1897
|
const clean = (s) => s.replace(/\s+/g, " ").trim();
|
|
1737
1898
|
const explicit = el.getAttribute("data-cd-track") || el.getAttribute("data-track") || el.getAttribute("data-testid");
|
|
@@ -1758,7 +1919,7 @@ function extractText(el) {
|
|
|
1758
1919
|
const t = clean(el.value);
|
|
1759
1920
|
if (t) return t;
|
|
1760
1921
|
}
|
|
1761
|
-
const text =
|
|
1922
|
+
const text = visibleLabel(el);
|
|
1762
1923
|
if (text) return text;
|
|
1763
1924
|
const title = el.getAttribute("title");
|
|
1764
1925
|
if (title) {
|
|
@@ -2399,7 +2560,7 @@ var BreadcrumbBuffer = class {
|
|
|
2399
2560
|
};
|
|
2400
2561
|
|
|
2401
2562
|
// src/_diagnostic-telemetry.ts
|
|
2402
|
-
var DIAGNOSTIC_TELEMETRY_ENDPOINT = "https://api.cross-deck.com/v1/
|
|
2563
|
+
var DIAGNOSTIC_TELEMETRY_ENDPOINT = "https://api.cross-deck.com/v1/events";
|
|
2403
2564
|
var DIAGNOSTIC_TELEMETRY_PUBLISHABLE_KEY = "cd_pub_live_9490e7aa029c432abf";
|
|
2404
2565
|
function isDiagnosticTelemetryEnabled() {
|
|
2405
2566
|
return !DIAGNOSTIC_TELEMETRY_PUBLISHABLE_KEY.startsWith(
|
|
@@ -2437,7 +2598,15 @@ function sendDiagnosticTelemetry(payload) {
|
|
|
2437
2598
|
if (!isDiagnosticTelemetryEnabled()) return;
|
|
2438
2599
|
const filtered = filterDiagnosticPayload(payload);
|
|
2439
2600
|
if (Object.keys(filtered).length === 0) return;
|
|
2440
|
-
const body = JSON.stringify(
|
|
2601
|
+
const body = JSON.stringify({
|
|
2602
|
+
events: [
|
|
2603
|
+
{
|
|
2604
|
+
name: "crossdeck.contract_failed",
|
|
2605
|
+
timestamp: Date.now(),
|
|
2606
|
+
properties: filtered
|
|
2607
|
+
}
|
|
2608
|
+
]
|
|
2609
|
+
});
|
|
2441
2610
|
const f = globalThis.fetch;
|
|
2442
2611
|
if (typeof f !== "function") return;
|
|
2443
2612
|
try {
|
|
@@ -3297,19 +3466,58 @@ var DEFAULT_ERROR_CAPTURE = {
|
|
|
3297
3466
|
"ResizeObserver loop completed with undelivered notifications",
|
|
3298
3467
|
"Non-Error promise rejection captured"
|
|
3299
3468
|
// NOTE: We deliberately do NOT drop cross-origin "Script error."
|
|
3300
|
-
// events here.
|
|
3301
|
-
//
|
|
3302
|
-
//
|
|
3303
|
-
// `
|
|
3304
|
-
//
|
|
3305
|
-
//
|
|
3306
|
-
//
|
|
3307
|
-
//
|
|
3308
|
-
// init config.
|
|
3469
|
+
// events here. The actionability principle (see denyUrls
|
|
3470
|
+
// comment below) draws the line at "developer cannot act":
|
|
3471
|
+
// cross-origin CORS opacity HAS a real, code-shaped fix (add
|
|
3472
|
+
// `crossorigin="anonymous"` to the script tag + CORS headers
|
|
3473
|
+
// on the script's origin). The dashboard surfaces these with a
|
|
3474
|
+
// `cross_origin` tag pointing at that fix. Apps that genuinely
|
|
3475
|
+
// want them muted can re-add "Script error" to ignoreErrors
|
|
3476
|
+
// via init config.
|
|
3309
3477
|
],
|
|
3310
3478
|
allowUrls: [],
|
|
3311
3479
|
denyUrls: [
|
|
3312
|
-
//
|
|
3480
|
+
// The actionability principle
|
|
3481
|
+
// ---------------------------
|
|
3482
|
+
// Crossdeck's default philosophy is "classify, don't silently
|
|
3483
|
+
// drop" — surfacing errors the developer can fix is more useful
|
|
3484
|
+
// than hiding them. That's right for cross-origin "Script
|
|
3485
|
+
// error." (real, code-shaped CORS fix) and for plain
|
|
3486
|
+
// application bugs (obviously real).
|
|
3487
|
+
//
|
|
3488
|
+
// It's NOT right for events the developer cannot act on.
|
|
3489
|
+
// - A user's installed browser extension throwing inside
|
|
3490
|
+
// its own `chrome-extension://` URL: the developer can't
|
|
3491
|
+
// ship a fix for someone else's extension.
|
|
3492
|
+
// - An ad blocker preventing `googletagmanager.com` from
|
|
3493
|
+
// loading: the developer can't unblock the user's blocker.
|
|
3494
|
+
//
|
|
3495
|
+
// Capturing these creates a noise tab that's always non-empty
|
|
3496
|
+
// and never actionable, which trains the dev to ignore the
|
|
3497
|
+
// noise tab — and then the actionable noise (CORS opacity,
|
|
3498
|
+
// etc.) gets ignored along with it. Same lesson as Crossdeck's
|
|
3499
|
+
// "0-2 notifications a week" discipline: a signal that fires
|
|
3500
|
+
// constantly with nothing actionable behind it stops being a
|
|
3501
|
+
// signal.
|
|
3502
|
+
//
|
|
3503
|
+
// So we drop at the source for the unactionable category, and
|
|
3504
|
+
// keep capturing for the actionable category. Same principle
|
|
3505
|
+
// applied to two structurally different inputs — not a new
|
|
3506
|
+
// philosophy.
|
|
3507
|
+
//
|
|
3508
|
+
// The bootstrap list below is the minimum that ships in code.
|
|
3509
|
+
// The full, versioned list arrives at boot via /v1/config —
|
|
3510
|
+
// see `backend/src/lib/error-noise-deny-list.ts`. The SDK
|
|
3511
|
+
// applies the union of (bootstrap + remote), so a freshly-
|
|
3512
|
+
// installed SDK protects users immediately, and remote updates
|
|
3513
|
+
// (new ad-network domains, new pixel hosts) reach every
|
|
3514
|
+
// install without an SDK release.
|
|
3515
|
+
//
|
|
3516
|
+
// The backend Layer-2 classifier
|
|
3517
|
+
// (`backend/src/api/lib/noise-classifier.ts`) is the safety
|
|
3518
|
+
// net for events that slip past these patterns — older SDK
|
|
3519
|
+
// versions in the wild that haven't fetched the remote list
|
|
3520
|
+
// yet, or brand-new patterns the list hasn't named.
|
|
3313
3521
|
/^chrome-extension:\/\//,
|
|
3314
3522
|
/^moz-extension:\/\//,
|
|
3315
3523
|
/^safari-extension:\/\//,
|
|
@@ -4102,7 +4310,12 @@ var CrossdeckClient = class {
|
|
|
4102
4310
|
if (autoTrack.sessions || autoTrack.pageViews) {
|
|
4103
4311
|
const tracker = new AutoTracker(
|
|
4104
4312
|
autoTrack,
|
|
4105
|
-
(name, properties) => this.track(name, properties)
|
|
4313
|
+
(name, properties) => this.track(name, properties),
|
|
4314
|
+
// Persist session continuity on the SAME adapter as identity, so
|
|
4315
|
+
// a visit survives full-page navigations (multi-page sites
|
|
4316
|
+
// re-install the SDK on every page) and honours the same consent
|
|
4317
|
+
// posture — MemoryStorage when identity persistence is off.
|
|
4318
|
+
{ storage: effectiveStorage, storageKey: opts.storagePrefix + "session" }
|
|
4106
4319
|
);
|
|
4107
4320
|
this.state.autoTracker = tracker;
|
|
4108
4321
|
tracker.install();
|
|
@@ -4567,6 +4780,12 @@ var CrossdeckClient = class {
|
|
|
4567
4780
|
* true even before the session's first network round-trip. Returns
|
|
4568
4781
|
* false only for a genuinely new install that has never completed a
|
|
4569
4782
|
* getEntitlements(), or for an entitlement past its own validUntil.
|
|
4783
|
+
*
|
|
4784
|
+
* Throws `not_initialized` (CrossdeckError, type
|
|
4785
|
+
* `configuration_error`) if called before `Crossdeck.init()`. The
|
|
4786
|
+
* `useEntitlement` React hook and the Vue composable both swallow
|
|
4787
|
+
* this and return `false`; bare callers must guard for it (or call
|
|
4788
|
+
* after `init()` resolves).
|
|
4570
4789
|
*/
|
|
4571
4790
|
isEntitled(key) {
|
|
4572
4791
|
const s = this.requireStarted();
|
|
@@ -4582,8 +4801,21 @@ var CrossdeckClient = class {
|
|
|
4582
4801
|
*
|
|
4583
4802
|
* The listener is invoked AFTER the cache mutates — once after a
|
|
4584
4803
|
* successful `getEntitlements()` warms it, again after `syncPurchases()`
|
|
4585
|
-
* delivers fresh entitlements,
|
|
4586
|
-
*
|
|
4804
|
+
* delivers fresh entitlements, once on `reset()` to fire the empty-
|
|
4805
|
+
* cache state for logout flows, AND once on `identify()` after the
|
|
4806
|
+
* per-user cache slot rotates and re-hydrates from device storage.
|
|
4807
|
+
*
|
|
4808
|
+
* IMPORTANT — the `identify()` fire is a TRAP if you treat it as
|
|
4809
|
+
* authoritative network state. `identify()` does NOT fetch entitlements;
|
|
4810
|
+
* it switches the per-user cache slot and rehydrates from device
|
|
4811
|
+
* storage (which is empty for a brand-new install, and last-known-good
|
|
4812
|
+
* — possibly stale — for a returning user). A listener that gates a
|
|
4813
|
+
* paywall on the first fire after an identity switch will read
|
|
4814
|
+
* `false` for a paying customer on a fresh device and let them past
|
|
4815
|
+
* the gate as free. The network-truth fire is the one that follows
|
|
4816
|
+
* the next `getEntitlements()` resolution. Either call
|
|
4817
|
+
* `getEntitlements()` explicitly after `identify()`, or have your
|
|
4818
|
+
* gating code tolerate the empty-then-populated transition.
|
|
4587
4819
|
*
|
|
4588
4820
|
* It is NOT invoked synchronously on subscribe. Callers that need
|
|
4589
4821
|
* the current state should read it via `isEntitled()` / `listEntitlements()`
|
|
@@ -4692,6 +4924,7 @@ var CrossdeckClient = class {
|
|
|
4692
4924
|
);
|
|
4693
4925
|
}
|
|
4694
4926
|
}
|
|
4927
|
+
s.autoTracker?.markActivity();
|
|
4695
4928
|
const enriched = { ...s.deviceInfo };
|
|
4696
4929
|
const sessionId = s.autoTracker?.currentSessionId;
|
|
4697
4930
|
if (sessionId) enriched.sessionId = sessionId;
|
|
@@ -4737,7 +4970,7 @@ var CrossdeckClient = class {
|
|
|
4737
4970
|
callerProperties: validation.properties,
|
|
4738
4971
|
superProperties: supers,
|
|
4739
4972
|
deviceProperties: s.deviceInfo,
|
|
4740
|
-
mergedProperties:
|
|
4973
|
+
mergedProperties: enriched
|
|
4741
4974
|
});
|
|
4742
4975
|
}
|
|
4743
4976
|
if (!isError && !isWebVital) {
|
|
@@ -4946,6 +5179,25 @@ var CrossdeckClient = class {
|
|
|
4946
5179
|
events: s.events.getStats()
|
|
4947
5180
|
};
|
|
4948
5181
|
}
|
|
5182
|
+
/**
|
|
5183
|
+
* The stable reference to hand Stripe at checkout so the resulting
|
|
5184
|
+
* purchase attributes to THIS person. Stamp it on the Checkout Session
|
|
5185
|
+
* as `metadata.crossdeck_ref` (see docs/connect-stripe) — the platform
|
|
5186
|
+
* webhook reads it back, validates it, and attaches the subscription to
|
|
5187
|
+
* the right customer instead of stranding it on an anonymous record.
|
|
5188
|
+
*
|
|
5189
|
+
* Returns the strongest identifier the SDK currently holds, in the same
|
|
5190
|
+
* precedence the server resolves by:
|
|
5191
|
+
* crossdeckCustomerId (cdcust_…) > developerUserId > anonymousId
|
|
5192
|
+
* anonymousId is always present, so this always returns a usable,
|
|
5193
|
+
* non-empty reference — even before identify(). Identify the user as
|
|
5194
|
+
* early as you can, though, so the reference is their stable identity
|
|
5195
|
+
* and not a per-device anon id.
|
|
5196
|
+
*/
|
|
5197
|
+
getCheckoutReference() {
|
|
5198
|
+
const s = this.requireStarted();
|
|
5199
|
+
return s.identity.crossdeckCustomerId ?? s.developerUserId ?? s.identity.anonymousId;
|
|
5200
|
+
}
|
|
4949
5201
|
// ---------- private helpers ----------
|
|
4950
5202
|
requireStarted() {
|
|
4951
5203
|
if (!this.state) {
|