@vio-live/web-sdk 0.19.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +144 -0
- package/dist/chunk-25QECQDS.js +3 -0
- package/dist/{chunk-PZUOMOLF.js.map → chunk-25QECQDS.js.map} +1 -1
- package/dist/{chunk-CMNTHF66.js → chunk-AMDI7KH3.js} +918 -83
- package/dist/chunk-AMDI7KH3.js.map +1 -0
- package/dist/{chunk-NUNGU6LL.js → chunk-COCHGJ2K.js} +699 -69
- package/dist/chunk-COCHGJ2K.js.map +1 -0
- package/dist/core/index.d.ts +467 -11
- package/dist/core/index.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -2
- package/dist/react/index.d.ts +2 -2
- package/dist/react/index.js +3 -3
- package/dist/react/index.js.map +1 -1
- package/dist/{tokens-IrVSMVcA.d.ts → tokens-DSYvrBsB.d.ts} +117 -0
- package/dist/ui/index.d.ts +1 -1
- package/dist/ui/index.js +3 -3
- package/package.json +1 -1
- package/dist/chunk-CMNTHF66.js.map +0 -1
- package/dist/chunk-NUNGU6LL.js.map +0 -1
- package/dist/chunk-PZUOMOLF.js +0 -3
|
@@ -46,6 +46,9 @@ var ConfigurationSingleton = class {
|
|
|
46
46
|
this.resolved = {
|
|
47
47
|
apiKey: input.apiKey,
|
|
48
48
|
environment: env,
|
|
49
|
+
platform: input.platform ?? "web",
|
|
50
|
+
contentId: input.contentId,
|
|
51
|
+
contentTitle: input.contentTitle,
|
|
49
52
|
apiBase: input.apiBase ?? defaults.apiBase,
|
|
50
53
|
graphQLBase: input.graphQLBase ?? defaults.graphQLBase,
|
|
51
54
|
eventsBase: input.eventsBase ?? defaults.eventsBase,
|
|
@@ -58,6 +61,7 @@ var ConfigurationSingleton = class {
|
|
|
58
61
|
const masked = input.apiKey.length > 4 ? "*".repeat(Math.max(0, input.apiKey.length - 4)) + input.apiKey.slice(-4) : "****";
|
|
59
62
|
console.info("[Vio] Initialized", {
|
|
60
63
|
environment: env,
|
|
64
|
+
platform: this.resolved.platform,
|
|
61
65
|
apiBase: this.resolved.apiBase,
|
|
62
66
|
apiKey: masked,
|
|
63
67
|
stripeConfigured: !!input.stripePublishableKey,
|
|
@@ -118,9 +122,21 @@ var VioApi = class {
|
|
|
118
122
|
/**
|
|
119
123
|
* Bootstrap — fetches client app config (sponsors with commerce keys,
|
|
120
124
|
* default market, campaign id). Called once at SDK init.
|
|
125
|
+
*
|
|
126
|
+
* With `platform`, `campaigns[].slots[]` only lists the assignments of that
|
|
127
|
+
* platform (plus the ones with no platform) — see ADR-0027.
|
|
121
128
|
*/
|
|
122
|
-
async bootstrap() {
|
|
123
|
-
|
|
129
|
+
async bootstrap(platform) {
|
|
130
|
+
const query = platform ? `?platform=${encodeURIComponent(platform)}` : "";
|
|
131
|
+
return this.get(`/v2/mobile/config${query}`);
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Declare the slots of this page to the campaigns service
|
|
135
|
+
* (`POST /v2/mobile/components/manifest`, ADR-0034). The web SDK always
|
|
136
|
+
* sends `mode: 'merge'`; see `Vio.declareSlots()` for the dedupe on top.
|
|
137
|
+
*/
|
|
138
|
+
async declareManifest(body) {
|
|
139
|
+
return this.post("/v2/mobile/components/manifest", body);
|
|
124
140
|
}
|
|
125
141
|
/**
|
|
126
142
|
* Get placement components for a campaign. Used by the carousel auto-fetch
|
|
@@ -146,6 +162,22 @@ var VioApi = class {
|
|
|
146
162
|
}
|
|
147
163
|
return await res.json();
|
|
148
164
|
}
|
|
165
|
+
async post(path, body) {
|
|
166
|
+
const url = `${this.apiBase}${path}`;
|
|
167
|
+
const res = await fetch(url, {
|
|
168
|
+
method: "POST",
|
|
169
|
+
headers: {
|
|
170
|
+
"X-API-Key": this.apiKey,
|
|
171
|
+
Accept: "application/json",
|
|
172
|
+
"Content-Type": "application/json"
|
|
173
|
+
},
|
|
174
|
+
body: JSON.stringify(body)
|
|
175
|
+
});
|
|
176
|
+
if (!res.ok) {
|
|
177
|
+
throw new VioApiError(res.status, await safeText(res), `POST ${path}`);
|
|
178
|
+
}
|
|
179
|
+
return await res.json();
|
|
180
|
+
}
|
|
149
181
|
};
|
|
150
182
|
var VioApiError = class extends Error {
|
|
151
183
|
constructor(status, body, endpoint) {
|
|
@@ -1208,6 +1240,7 @@ query GetCheckout($checkoutId: String!) {
|
|
|
1208
1240
|
status
|
|
1209
1241
|
checkout_url
|
|
1210
1242
|
origin_payment_id
|
|
1243
|
+
duplicate_of
|
|
1211
1244
|
billing_address {
|
|
1212
1245
|
id
|
|
1213
1246
|
first_name
|
|
@@ -1384,23 +1417,40 @@ function getCustomerSessionId() {
|
|
|
1384
1417
|
}
|
|
1385
1418
|
return id;
|
|
1386
1419
|
}
|
|
1420
|
+
var CART_GRAPHQL_TIMEOUT_MS = 15e3;
|
|
1387
1421
|
async function executeCartGraphQL(query, variables, options = {}) {
|
|
1388
1422
|
const resolvedOptions = typeof options === "number" ? await getCartGraphQLOptions(options) : !options.apiKey && !options.commerceKey ? { ...await getCartGraphQLOptions(), ...options } : options;
|
|
1389
1423
|
const { endpoint = "https://graph-ql-dev.vio.live", apiKey, commerceKey } = resolvedOptions;
|
|
1390
1424
|
const headers = { "Content-Type": "application/json" };
|
|
1391
1425
|
if (apiKey) headers["X-API-Key"] = apiKey;
|
|
1392
1426
|
if (commerceKey || apiKey) headers["Authorization"] = commerceKey || apiKey;
|
|
1393
|
-
const
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1427
|
+
const controller = typeof AbortController !== "undefined" ? new AbortController() : void 0;
|
|
1428
|
+
let timer;
|
|
1429
|
+
const timedOut = new Promise((_, reject) => {
|
|
1430
|
+
timer = setTimeout(() => {
|
|
1431
|
+
controller?.abort();
|
|
1432
|
+
reject(new Error(`GraphQL request timed out after ${CART_GRAPHQL_TIMEOUT_MS}ms`));
|
|
1433
|
+
}, CART_GRAPHQL_TIMEOUT_MS);
|
|
1397
1434
|
});
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1435
|
+
try {
|
|
1436
|
+
const res = await Promise.race([
|
|
1437
|
+
fetch(endpoint, {
|
|
1438
|
+
method: "POST",
|
|
1439
|
+
headers,
|
|
1440
|
+
body: JSON.stringify({ query, variables }),
|
|
1441
|
+
signal: controller?.signal
|
|
1442
|
+
}),
|
|
1443
|
+
timedOut
|
|
1444
|
+
]);
|
|
1445
|
+
const json = await Promise.race([res.json().catch(() => ({})), timedOut]);
|
|
1446
|
+
if (!res.ok || json.errors) {
|
|
1447
|
+
const errMsg = json.errors?.[0]?.message || `GraphQL request failed (HTTP ${res.status})`;
|
|
1448
|
+
throw new Error(errMsg);
|
|
1449
|
+
}
|
|
1450
|
+
return json;
|
|
1451
|
+
} finally {
|
|
1452
|
+
clearTimeout(timer);
|
|
1402
1453
|
}
|
|
1403
|
-
return json;
|
|
1404
1454
|
}
|
|
1405
1455
|
async function createCart(customerSessionId, currency, shippingCountry, options) {
|
|
1406
1456
|
const json = await executeCartGraphQL(
|
|
@@ -1538,6 +1588,7 @@ query GetVippsStatus($checkoutId: String!) {
|
|
|
1538
1588
|
Payment {
|
|
1539
1589
|
GetVippsStatus(checkout_id: $checkoutId) {
|
|
1540
1590
|
state
|
|
1591
|
+
duplicate_of
|
|
1541
1592
|
}
|
|
1542
1593
|
}
|
|
1543
1594
|
}
|
|
@@ -1579,6 +1630,14 @@ var CartManager = class extends EventTarget {
|
|
|
1579
1630
|
* landed (Alan, 2026-08-18: shippings queried before AddItem settled,
|
|
1580
1631
|
* came back empty). */
|
|
1581
1632
|
this.inFlightMutations = /* @__PURE__ */ new Map();
|
|
1633
|
+
/** Per-line quantity sync with the backend: at most ONE UpdateItem /
|
|
1634
|
+
* DeleteItem in flight per line. Targets that arrive while one is in flight
|
|
1635
|
+
* are coalesced into `pending` (only the latest survives) and sent when the
|
|
1636
|
+
* current request settles. Keyed by `${sponsorId}:${cartItemId}`.
|
|
1637
|
+
* (Alan, 2026-10-15: a burst of +/− clicks fired one UpdateItem per click
|
|
1638
|
+
* in parallel; each response re-applied the whole backend cart, so an
|
|
1639
|
+
* older one landing after a newer optimistic change rolled the line back.) */
|
|
1640
|
+
this.qtySyncs = /* @__PURE__ */ new Map();
|
|
1582
1641
|
/** Quantity intents for lines whose backend id hasn't arrived yet — replayed
|
|
1583
1642
|
* when the AddItem response reconciles (keyed by product::variant). */
|
|
1584
1643
|
this.pendingQtyIntents = /* @__PURE__ */ new Map();
|
|
@@ -1589,21 +1648,75 @@ var CartManager = class extends EventTarget {
|
|
|
1589
1648
|
this.mutationSeq.set(sponsorId, n);
|
|
1590
1649
|
return n;
|
|
1591
1650
|
}
|
|
1592
|
-
/** Resolves once
|
|
1593
|
-
* sponsor has settled (success or failure)
|
|
1651
|
+
/** Resolves once every in-flight AddItem/UpdateItem/DeleteItem for this
|
|
1652
|
+
* sponsor has settled (success or failure), including ones started while
|
|
1653
|
+
* waiting (a quantity intent replayed by an AddItem reconcile). No-op if
|
|
1654
|
+
* nothing is pending. */
|
|
1594
1655
|
async waitForCartMutations(sponsorId) {
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
await
|
|
1599
|
-
}
|
|
1600
|
-
|
|
1656
|
+
for (; ; ) {
|
|
1657
|
+
const set = this.inFlightMutations.get(sponsorId);
|
|
1658
|
+
if (!set || set.size === 0) return;
|
|
1659
|
+
await Promise.allSettled([...set]);
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
/** Register a backend mutation so `waitForCartMutations` can await it. */
|
|
1663
|
+
trackMutation(sponsorId, mutation) {
|
|
1664
|
+
let set = this.inFlightMutations.get(sponsorId);
|
|
1665
|
+
if (!set) {
|
|
1666
|
+
set = /* @__PURE__ */ new Set();
|
|
1667
|
+
this.inFlightMutations.set(sponsorId, set);
|
|
1668
|
+
}
|
|
1669
|
+
const tracked = set;
|
|
1670
|
+
tracked.add(mutation);
|
|
1671
|
+
const untrack = () => {
|
|
1672
|
+
tracked.delete(mutation);
|
|
1673
|
+
if (tracked.size === 0 && this.inFlightMutations.get(sponsorId) === tracked) {
|
|
1674
|
+
this.inFlightMutations.delete(sponsorId);
|
|
1675
|
+
}
|
|
1676
|
+
};
|
|
1677
|
+
mutation.then(untrack, untrack);
|
|
1678
|
+
}
|
|
1679
|
+
qtySyncKey(sponsorId, cartItemId) {
|
|
1680
|
+
return `${sponsorId}:${cartItemId}`;
|
|
1601
1681
|
}
|
|
1602
|
-
/**
|
|
1682
|
+
/** The quantity a line is heading to (pending beats in flight), or
|
|
1683
|
+
* undefined when no quantity sync is active for it. */
|
|
1684
|
+
intendedQuantity(sponsorId, cartItemId) {
|
|
1685
|
+
const sync = this.qtySyncs.get(this.qtySyncKey(sponsorId, cartItemId));
|
|
1686
|
+
return sync ? sync.pending ?? sync.sent : void 0;
|
|
1687
|
+
}
|
|
1688
|
+
/** Apply a backend response only if no newer response was applied already.
|
|
1689
|
+
* Returns false when it was discarded as stale. */
|
|
1603
1690
|
applyBackendCart(sponsorId, backendCart, seq) {
|
|
1604
|
-
if (seq < (this.appliedSeq.get(sponsorId) ?? 0)) return;
|
|
1691
|
+
if (seq < (this.appliedSeq.get(sponsorId) ?? 0)) return false;
|
|
1605
1692
|
this.appliedSeq.set(sponsorId, seq);
|
|
1606
1693
|
this.updateFromBackendCart(sponsorId, backendCart);
|
|
1694
|
+
return true;
|
|
1695
|
+
}
|
|
1696
|
+
/** The final answer of a line's sync lost the per-sponsor seq race (another
|
|
1697
|
+
* line answered later): the cart as a whole is stale, but for THIS line it
|
|
1698
|
+
* is the latest word. Copy its quantity (and price) so a server-side clamp
|
|
1699
|
+
* — stock — doesn't leave the optimistic value behind for good, with the
|
|
1700
|
+
* checkout / Apple Pay total built on it. */
|
|
1701
|
+
reconcileLineFromResponse(sponsorId, cartItemId, resCart) {
|
|
1702
|
+
const cart = this.cartsBySponsor.get(sponsorId);
|
|
1703
|
+
const line = cart?.items.find((i) => i.cartItemId === cartItemId || i.id === cartItemId);
|
|
1704
|
+
const server = resCart.line_items?.find((li) => li.id === cartItemId);
|
|
1705
|
+
if (!cart || !line || !server) return;
|
|
1706
|
+
const price = server.price?.amount_incl_taxes ?? server.price?.amount;
|
|
1707
|
+
let changed = false;
|
|
1708
|
+
if (server.quantity !== line.quantity) {
|
|
1709
|
+
line.quantity = server.quantity;
|
|
1710
|
+
line.origins = reconciledOrigins(line.origins, server.quantity);
|
|
1711
|
+
changed = true;
|
|
1712
|
+
}
|
|
1713
|
+
if (price != null && price !== line.unitPrice) {
|
|
1714
|
+
line.unitPrice = price;
|
|
1715
|
+
changed = true;
|
|
1716
|
+
}
|
|
1717
|
+
if (!changed) return;
|
|
1718
|
+
this.persist();
|
|
1719
|
+
this.emit();
|
|
1607
1720
|
}
|
|
1608
1721
|
/** Surface a cart sync problem to hosts/UI (and the console). */
|
|
1609
1722
|
emitError(message, err) {
|
|
@@ -1682,6 +1795,7 @@ var CartManager = class extends EventTarget {
|
|
|
1682
1795
|
if (backendCart.available_shipping_countries) {
|
|
1683
1796
|
cart.availableShippingCountries = backendCart.available_shipping_countries;
|
|
1684
1797
|
}
|
|
1798
|
+
const removedBeforeSync = [];
|
|
1685
1799
|
if (Array.isArray(backendCart.line_items)) {
|
|
1686
1800
|
const existingItems = cart.items || [];
|
|
1687
1801
|
if (backendCart.line_items.length === 0 && existingItems.length > 0) {
|
|
@@ -1689,7 +1803,16 @@ var CartManager = class extends EventTarget {
|
|
|
1689
1803
|
this.emit();
|
|
1690
1804
|
return;
|
|
1691
1805
|
}
|
|
1692
|
-
cart.items = backendCart.line_items.
|
|
1806
|
+
cart.items = backendCart.line_items.flatMap((li) => {
|
|
1807
|
+
const intended = this.intendedQuantity(sponsorId, li.id);
|
|
1808
|
+
if (intended !== void 0 && intended <= 0) return [];
|
|
1809
|
+
const earlyKey = this.intentKey(li.product_id, li.variant_id);
|
|
1810
|
+
const earlyIntent = this.pendingQtyIntents.get(earlyKey);
|
|
1811
|
+
if (earlyIntent !== void 0 && earlyIntent <= 0) {
|
|
1812
|
+
this.pendingQtyIntents.delete(earlyKey);
|
|
1813
|
+
removedBeforeSync.push(li.id);
|
|
1814
|
+
return [];
|
|
1815
|
+
}
|
|
1693
1816
|
const urlFromBackend = (typeof li.image === "string" ? li.image : li.image?.url) || li.imageUrl || li.image_url || "";
|
|
1694
1817
|
const matchedLocal = existingItems.find((existing) => {
|
|
1695
1818
|
const p1 = normId(existing.productId);
|
|
@@ -1705,7 +1828,7 @@ var CartManager = class extends EventTarget {
|
|
|
1705
1828
|
return false;
|
|
1706
1829
|
});
|
|
1707
1830
|
const imageUrl = urlFromBackend || matchedLocal?.imageUrl || "";
|
|
1708
|
-
|
|
1831
|
+
const line = {
|
|
1709
1832
|
id: li.id,
|
|
1710
1833
|
cartItemId: li.id,
|
|
1711
1834
|
productId: li.product_id,
|
|
@@ -1716,14 +1839,22 @@ var CartManager = class extends EventTarget {
|
|
|
1716
1839
|
unitPrice: li.price?.amount_incl_taxes ?? li.price?.amount ?? matchedLocal?.unitPrice ?? 0,
|
|
1717
1840
|
currency: li.price?.currency_code || backendCart.currency || matchedLocal?.currency || "NOK",
|
|
1718
1841
|
imageUrl,
|
|
1719
|
-
quantity: li.quantity,
|
|
1842
|
+
quantity: intended ?? li.quantity,
|
|
1843
|
+
// The server knows nothing about origins: keep the local ones,
|
|
1844
|
+
// squared with the quantity it answered.
|
|
1845
|
+
origins: matchedLocal ? reconciledOrigins(matchedLocal.origins, intended ?? li.quantity) : void 0,
|
|
1720
1846
|
shipping: li.shipping,
|
|
1721
1847
|
availableShippings: li.available_shippings
|
|
1722
1848
|
};
|
|
1849
|
+
return [line];
|
|
1723
1850
|
});
|
|
1724
1851
|
}
|
|
1725
1852
|
this.persist();
|
|
1726
1853
|
this.emit();
|
|
1854
|
+
const syncedCartId = cart.cartId;
|
|
1855
|
+
if (syncedCartId) {
|
|
1856
|
+
for (const id of removedBeforeSync) this.syncQuantity(sponsorId, syncedCartId, id, 0);
|
|
1857
|
+
}
|
|
1727
1858
|
if (this.pendingQtyIntents.size > 0) {
|
|
1728
1859
|
const currentCart = this.cartsBySponsor.get(sponsorId);
|
|
1729
1860
|
for (const [key, qty] of [...this.pendingQtyIntents]) {
|
|
@@ -1759,7 +1890,8 @@ var CartManager = class extends EventTarget {
|
|
|
1759
1890
|
currency: price.currency_code,
|
|
1760
1891
|
imageUrl,
|
|
1761
1892
|
quantity: opts.quantity,
|
|
1762
|
-
variantId: opts.variantId
|
|
1893
|
+
variantId: opts.variantId,
|
|
1894
|
+
origin: opts.origin
|
|
1763
1895
|
});
|
|
1764
1896
|
}
|
|
1765
1897
|
/** Add a manually-specified line item (no Product DTO). */
|
|
@@ -1792,6 +1924,7 @@ var CartManager = class extends EventTarget {
|
|
|
1792
1924
|
});
|
|
1793
1925
|
if (existing) {
|
|
1794
1926
|
existing.quantity += qty;
|
|
1927
|
+
addOriginQuantity(existing, opts.origin, qty);
|
|
1795
1928
|
} else {
|
|
1796
1929
|
const item = {
|
|
1797
1930
|
id: lineId(),
|
|
@@ -1803,7 +1936,8 @@ var CartManager = class extends EventTarget {
|
|
|
1803
1936
|
unitPrice: opts.unitPrice,
|
|
1804
1937
|
currency: opts.currency,
|
|
1805
1938
|
imageUrl: opts.imageUrl,
|
|
1806
|
-
quantity: qty
|
|
1939
|
+
quantity: qty,
|
|
1940
|
+
origins: [{ ...normalizeOrigin(opts.origin), quantity: qty }]
|
|
1807
1941
|
};
|
|
1808
1942
|
cart.items.push(item);
|
|
1809
1943
|
}
|
|
@@ -1834,6 +1968,7 @@ var CartManager = class extends EventTarget {
|
|
|
1834
1968
|
const line = c?.items.find((i) => i.id === addedLineId);
|
|
1835
1969
|
if (c && line) {
|
|
1836
1970
|
line.quantity -= qty;
|
|
1971
|
+
removeOriginQuantity(line, qty);
|
|
1837
1972
|
if (line.quantity <= 0) c.items = c.items.filter((i) => i !== line);
|
|
1838
1973
|
if (c.items.length === 0) this.cartsBySponsor.delete(opts.sponsorId);
|
|
1839
1974
|
this.persist();
|
|
@@ -1842,24 +1977,39 @@ var CartManager = class extends EventTarget {
|
|
|
1842
1977
|
this.emitError("AddItem rejected by the backend \u2014 item removed from the cart", err);
|
|
1843
1978
|
}
|
|
1844
1979
|
})();
|
|
1845
|
-
this.
|
|
1846
|
-
void mutation.finally(() => {
|
|
1847
|
-
if (this.inFlightMutations.get(opts.sponsorId) === mutation) {
|
|
1848
|
-
this.inFlightMutations.delete(opts.sponsorId);
|
|
1849
|
-
}
|
|
1850
|
-
});
|
|
1980
|
+
this.trackMutation(opts.sponsorId, mutation);
|
|
1851
1981
|
return addedItem;
|
|
1852
1982
|
}
|
|
1853
1983
|
intentKey(productId, variantId) {
|
|
1854
1984
|
return `${normId(productId)}::${normId(variantId)}`;
|
|
1855
1985
|
}
|
|
1856
|
-
|
|
1986
|
+
findLine(sponsorId, lineItemId) {
|
|
1987
|
+
return this.cartsBySponsor.get(sponsorId)?.items.find((i) => i.id === lineItemId || i.cartItemId === lineItemId);
|
|
1988
|
+
}
|
|
1989
|
+
/** Change a line's quantity by `delta`, computed on the manager's CURRENT
|
|
1990
|
+
* quantity — not on whatever a UI rendered a moment ago. A burst of +/−
|
|
1991
|
+
* clicks before a re-render therefore asks for 2, 3, 4… and not for the
|
|
1992
|
+
* same target several times. Reaching 0 removes the line. */
|
|
1993
|
+
adjustQuantity(lineItemId, sponsorId, delta, opts = {}) {
|
|
1994
|
+
const item = this.findLine(sponsorId, lineItemId);
|
|
1995
|
+
if (!item) return;
|
|
1996
|
+
this.updateQuantity(lineItemId, sponsorId, item.quantity + delta, opts);
|
|
1997
|
+
}
|
|
1998
|
+
/** Set a line's quantity (0 or less removes it). Optimistic: the local
|
|
1999
|
+
* state and `change` update at once; the backend is synced through
|
|
2000
|
+
* `syncQuantity`, one request in flight per line, latest target wins. */
|
|
2001
|
+
updateQuantity(lineItemId, sponsorId, quantity, opts = {}) {
|
|
1857
2002
|
const cart = this.cartsBySponsor.get(sponsorId);
|
|
1858
2003
|
if (!cart) return;
|
|
1859
2004
|
const item = cart.items.find((i) => i.id === lineItemId || i.cartItemId === lineItemId);
|
|
1860
2005
|
if (!item) return;
|
|
2006
|
+
const target = quantity > 0 ? quantity : 0;
|
|
2007
|
+
if (target === item.quantity) return;
|
|
1861
2008
|
const targetItemId = item.cartItemId;
|
|
1862
|
-
|
|
2009
|
+
const removedOrigin = latestOrigin(item);
|
|
2010
|
+
if (target > item.quantity) addOriginQuantity(item, opts.origin, target - item.quantity);
|
|
2011
|
+
else removeOriginQuantity(item, item.quantity - target);
|
|
2012
|
+
if (target <= 0) {
|
|
1863
2013
|
cart.items = cart.items.filter((i) => i !== item);
|
|
1864
2014
|
if (typeof document !== "undefined") {
|
|
1865
2015
|
document.dispatchEvent(
|
|
@@ -1873,42 +2023,84 @@ var CartManager = class extends EventTarget {
|
|
|
1873
2023
|
price: item.unitPrice,
|
|
1874
2024
|
quantity: item.quantity,
|
|
1875
2025
|
currency: item.currency,
|
|
1876
|
-
sponsorId
|
|
2026
|
+
sponsorId,
|
|
2027
|
+
// Attribution of the removed units: their most recent origin.
|
|
2028
|
+
slot: removedOrigin?.slot,
|
|
2029
|
+
campaignId: removedOrigin?.campaignId,
|
|
2030
|
+
contentId: removedOrigin?.contentId
|
|
1877
2031
|
}
|
|
1878
2032
|
})
|
|
1879
2033
|
);
|
|
1880
2034
|
}
|
|
1881
2035
|
} else {
|
|
1882
|
-
item.quantity =
|
|
2036
|
+
item.quantity = target;
|
|
1883
2037
|
}
|
|
1884
2038
|
if (cart.items.length === 0) this.cartsBySponsor.delete(sponsorId);
|
|
1885
2039
|
this.persist();
|
|
1886
2040
|
this.emit();
|
|
1887
2041
|
if (!cart.cartId) return;
|
|
1888
2042
|
if (!targetItemId) {
|
|
1889
|
-
this.pendingQtyIntents.set(this.intentKey(item.productId, item.variantId),
|
|
2043
|
+
this.pendingQtyIntents.set(this.intentKey(item.productId, item.variantId), target);
|
|
1890
2044
|
return;
|
|
1891
2045
|
}
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
2046
|
+
this.syncQuantity(sponsorId, cart.cartId, targetItemId, target);
|
|
2047
|
+
}
|
|
2048
|
+
/** Send a line's target quantity to the backend — ONE request in flight per
|
|
2049
|
+
* line. A target that arrives while one is in flight replaces any earlier
|
|
2050
|
+
* waiting one and goes out when the current request settles, so the server
|
|
2051
|
+
* ends on the last intent and never sees two updates racing each other.
|
|
2052
|
+
* While the sync is active the line keeps its optimistic quantity through
|
|
2053
|
+
* any backend response (see `updateFromBackendCart`); a rejection drops
|
|
2054
|
+
* what was queued and reconciles the cart from the server. */
|
|
2055
|
+
syncQuantity(sponsorId, backendCartId, cartItemId, target) {
|
|
2056
|
+
const key = this.qtySyncKey(sponsorId, cartItemId);
|
|
2057
|
+
const active = this.qtySyncs.get(key);
|
|
2058
|
+
if (active) {
|
|
2059
|
+
active.pending = target;
|
|
2060
|
+
return;
|
|
2061
|
+
}
|
|
2062
|
+
const sync = { sent: target };
|
|
2063
|
+
this.qtySyncs.set(key, sync);
|
|
2064
|
+
const run = (async () => {
|
|
2065
|
+
let next = target;
|
|
2066
|
+
let removalRetried = false;
|
|
2067
|
+
while (next !== void 0) {
|
|
2068
|
+
sync.sent = next;
|
|
2069
|
+
sync.pending = void 0;
|
|
2070
|
+
const seq = this.nextSeq(sponsorId);
|
|
2071
|
+
try {
|
|
2072
|
+
const gqlOpts = await getCartGraphQLOptions(sponsorId);
|
|
2073
|
+
const resCart = next <= 0 ? await deleteItem(backendCartId, cartItemId, gqlOpts) : await updateItem(backendCartId, cartItemId, null, next, gqlOpts);
|
|
2074
|
+
next = sync.pending;
|
|
2075
|
+
if (next === sync.sent) next = void 0;
|
|
2076
|
+
if (next === void 0) this.qtySyncs.delete(key);
|
|
2077
|
+
if (resCart && Array.isArray(resCart.line_items)) {
|
|
2078
|
+
const applied = this.applyBackendCart(sponsorId, resCart, seq);
|
|
2079
|
+
if (!applied && next === void 0) {
|
|
2080
|
+
this.reconcileLineFromResponse(sponsorId, cartItemId, resCart);
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
} catch (err) {
|
|
2084
|
+
const retryRemoval = !removalRetried && sync.pending !== void 0 && sync.pending <= 0;
|
|
2085
|
+
next = void 0;
|
|
2086
|
+
if (retryRemoval) {
|
|
2087
|
+
removalRetried = true;
|
|
2088
|
+
sync.sent = 0;
|
|
2089
|
+
sync.pending = void 0;
|
|
2090
|
+
next = 0;
|
|
2091
|
+
} else {
|
|
2092
|
+
this.qtySyncs.delete(key);
|
|
2093
|
+
}
|
|
2094
|
+
this.emitError("Update/DeleteItem failed \u2014 cart re-synced from backend", err);
|
|
2095
|
+
try {
|
|
2096
|
+
await this.fetchCartFromBackend(sponsorId);
|
|
2097
|
+
} catch (fetchErr) {
|
|
2098
|
+
this.emitError("cart re-sync after a rejected Update/DeleteItem failed", fetchErr);
|
|
2099
|
+
}
|
|
1900
2100
|
}
|
|
1901
|
-
} catch (err) {
|
|
1902
|
-
this.emitError("Update/DeleteItem failed \u2014 cart re-synced from backend", err);
|
|
1903
|
-
void this.fetchCartFromBackend(sponsorId);
|
|
1904
2101
|
}
|
|
1905
2102
|
})();
|
|
1906
|
-
this.
|
|
1907
|
-
void mutation.finally(() => {
|
|
1908
|
-
if (this.inFlightMutations.get(sponsorId) === mutation) {
|
|
1909
|
-
this.inFlightMutations.delete(sponsorId);
|
|
1910
|
-
}
|
|
1911
|
-
});
|
|
2103
|
+
this.trackMutation(sponsorId, run);
|
|
1912
2104
|
}
|
|
1913
2105
|
removeItem(lineItemId, sponsorId) {
|
|
1914
2106
|
this.updateQuantity(lineItemId, sponsorId, 0);
|
|
@@ -2019,6 +2211,68 @@ var CartManager = class extends EventTarget {
|
|
|
2019
2211
|
this.dispatchEvent(new CustomEvent("change", { detail }));
|
|
2020
2212
|
}
|
|
2021
2213
|
};
|
|
2214
|
+
function normalizeOrigin(origin) {
|
|
2215
|
+
const out = {};
|
|
2216
|
+
if (!origin || typeof origin !== "object") return out;
|
|
2217
|
+
const campaignId = Number(origin.campaignId);
|
|
2218
|
+
if (origin.campaignId !== void 0 && origin.campaignId !== null && Number.isFinite(campaignId)) {
|
|
2219
|
+
out.campaignId = campaignId;
|
|
2220
|
+
}
|
|
2221
|
+
if (typeof origin.slot === "string" && origin.slot.trim()) out.slot = origin.slot.trim();
|
|
2222
|
+
if (origin.contentId !== void 0 && origin.contentId !== null && String(origin.contentId).trim()) {
|
|
2223
|
+
out.contentId = String(origin.contentId).trim();
|
|
2224
|
+
}
|
|
2225
|
+
return out;
|
|
2226
|
+
}
|
|
2227
|
+
function sameOrigin(a, b) {
|
|
2228
|
+
return a.campaignId === b.campaignId && a.slot === b.slot && a.contentId === b.contentId;
|
|
2229
|
+
}
|
|
2230
|
+
function latestOrigin(line) {
|
|
2231
|
+
const origins = Array.isArray(line.origins) ? line.origins : [];
|
|
2232
|
+
return origins[origins.length - 1];
|
|
2233
|
+
}
|
|
2234
|
+
function addOriginQuantity(line, origin, qty) {
|
|
2235
|
+
if (!(qty > 0)) return;
|
|
2236
|
+
const origins = Array.isArray(line.origins) ? line.origins : [];
|
|
2237
|
+
const target = origin ? normalizeOrigin(origin) : latestOrigin({ origins });
|
|
2238
|
+
const index = target ? origins.findIndex((o) => sameOrigin(o, target)) : -1;
|
|
2239
|
+
if (index >= 0) {
|
|
2240
|
+
const [found] = origins.splice(index, 1);
|
|
2241
|
+
found.quantity += qty;
|
|
2242
|
+
origins.push(found);
|
|
2243
|
+
} else {
|
|
2244
|
+
origins.push({ ...target ?? {}, quantity: qty });
|
|
2245
|
+
}
|
|
2246
|
+
line.origins = origins;
|
|
2247
|
+
}
|
|
2248
|
+
function removeOriginQuantity(line, qty) {
|
|
2249
|
+
if (!(qty > 0) || !Array.isArray(line.origins)) return;
|
|
2250
|
+
let left = qty;
|
|
2251
|
+
const origins = line.origins;
|
|
2252
|
+
while (left > 0 && origins.length > 0) {
|
|
2253
|
+
const last = origins[origins.length - 1];
|
|
2254
|
+
const take = Math.min(last.quantity, left);
|
|
2255
|
+
last.quantity -= take;
|
|
2256
|
+
left -= take;
|
|
2257
|
+
if (last.quantity <= 0) origins.pop();
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
function reconciledOrigins(origins, quantity) {
|
|
2261
|
+
if (!Array.isArray(origins)) return void 0;
|
|
2262
|
+
const copy = origins.map((o) => ({ ...o }));
|
|
2263
|
+
const total = copy.reduce((sum, o) => sum + (o.quantity > 0 ? o.quantity : 0), 0);
|
|
2264
|
+
const line = { origins: copy };
|
|
2265
|
+
if (quantity > total) addOriginQuantity(line, void 0, quantity - total);
|
|
2266
|
+
else if (quantity < total) removeOriginQuantity(line, total - quantity);
|
|
2267
|
+
return line.origins;
|
|
2268
|
+
}
|
|
2269
|
+
function lineOrigins(line) {
|
|
2270
|
+
const known = (Array.isArray(line.origins) ? line.origins : []).filter((o) => o && o.quantity > 0);
|
|
2271
|
+
const total = known.reduce((sum, o) => sum + o.quantity, 0);
|
|
2272
|
+
const quantity = line.quantity > 0 ? line.quantity : 0;
|
|
2273
|
+
if (total >= quantity) return known.map((o) => ({ ...o }));
|
|
2274
|
+
return [...known.map((o) => ({ ...o })), { quantity: quantity - total }];
|
|
2275
|
+
}
|
|
2022
2276
|
|
|
2023
2277
|
// src/core/checkout/payments/apple-pay.ts
|
|
2024
2278
|
var STRIPE_JS_URL = "https://js.stripe.com/v3";
|
|
@@ -2216,7 +2470,7 @@ async function prepareApplePay(config) {
|
|
|
2216
2470
|
}
|
|
2217
2471
|
|
|
2218
2472
|
// src/core/version.ts
|
|
2219
|
-
var SDK_VERSION = "0.
|
|
2473
|
+
var SDK_VERSION = "0.23.0";
|
|
2220
2474
|
|
|
2221
2475
|
// src/core/checkout/payments/stripe-embedded.ts
|
|
2222
2476
|
var CREATE_PAYMENT_INTENT_STRIPE_MUTATION = `
|
|
@@ -2724,8 +2978,8 @@ function renderKustomSnippet(container, htmlSnippet) {
|
|
|
2724
2978
|
const scripts = Array.from(container.querySelectorAll("script"));
|
|
2725
2979
|
for (const oldScript of scripts) {
|
|
2726
2980
|
const newScript = document.createElement("script");
|
|
2727
|
-
for (const
|
|
2728
|
-
newScript.setAttribute(
|
|
2981
|
+
for (const attr2 of Array.from(oldScript.attributes)) {
|
|
2982
|
+
newScript.setAttribute(attr2.name, attr2.value);
|
|
2729
2983
|
}
|
|
2730
2984
|
newScript.text = oldScript.text;
|
|
2731
2985
|
oldScript.parentNode?.replaceChild(newScript, oldScript);
|
|
@@ -3783,6 +4037,7 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
|
|
|
3783
4037
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
3784
4038
|
/** Create the backend checkout for the sponsor's cart (auto-accepts terms). */
|
|
3785
4039
|
async createCheckout(sponsorId) {
|
|
4040
|
+
await this.cartManager.waitForCartMutations(sponsorId);
|
|
3786
4041
|
const cart = this.cartManager.getCart(sponsorId);
|
|
3787
4042
|
let cartId = cart?.cartId;
|
|
3788
4043
|
if (!cartId) {
|
|
@@ -3975,6 +4230,7 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
|
|
|
3975
4230
|
async prepareRedirectPayment(sponsorId, formData, extraUpdateVars = {}) {
|
|
3976
4231
|
const spId = sponsorId ?? this.state?.sponsorId;
|
|
3977
4232
|
if (!spId) throw new Error("[CheckoutManager] no sponsor for payment");
|
|
4233
|
+
await this.cartManager.waitForCartMutations(spId);
|
|
3978
4234
|
const cart = this.cartManager.getCart(spId);
|
|
3979
4235
|
let cartId = cart?.cartId;
|
|
3980
4236
|
if (!cartId) {
|
|
@@ -4089,6 +4345,8 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
|
|
|
4089
4345
|
* plain payment once the shopper chose one in our form.
|
|
4090
4346
|
*/
|
|
4091
4347
|
async startVippsPayment(sponsorId, formData, options = {}) {
|
|
4348
|
+
const waitFor = sponsorId ?? this.state?.sponsorId;
|
|
4349
|
+
if (waitFor) await this.cartManager.waitForCartMutations(waitFor);
|
|
4092
4350
|
const { spId, checkoutId, emailVal, opts } = await this.prepareRedirectPayment(
|
|
4093
4351
|
sponsorId,
|
|
4094
4352
|
formData,
|
|
@@ -5586,6 +5844,194 @@ function toSmallestUnit(amount, currency) {
|
|
|
5586
5844
|
return Math.round(amount * 100);
|
|
5587
5845
|
}
|
|
5588
5846
|
|
|
5847
|
+
// src/core/content.ts
|
|
5848
|
+
var CONTENT_ID_QUERY = "vio_content";
|
|
5849
|
+
var CONTENT_TITLE_QUERY = "vio_content_title";
|
|
5850
|
+
function resolveContent(init, doc = typeof document !== "undefined" ? document : null, loc = typeof location !== "undefined" ? location : null) {
|
|
5851
|
+
try {
|
|
5852
|
+
const query = parseQuery(loc?.search);
|
|
5853
|
+
const id = text(init?.contentId) ?? text(query.get(CONTENT_ID_QUERY)) ?? text(meta(doc, "name", "vio:content-id")) ?? stripQueryAndHash(text(attr(doc, 'link[rel="canonical"]', "href")) ?? text(meta(doc, "property", "og:url"))) ?? text(loc?.pathname);
|
|
5854
|
+
const title = text(init?.contentTitle) ?? text(query.get(CONTENT_TITLE_QUERY)) ?? text(meta(doc, "name", "vio:content-title")) ?? text(meta(doc, "property", "og:title")) ?? text(doc?.title);
|
|
5855
|
+
const out = {};
|
|
5856
|
+
if (id !== void 0) out.id = id;
|
|
5857
|
+
if (title !== void 0) out.title = title.slice(0, 255);
|
|
5858
|
+
return out;
|
|
5859
|
+
} catch {
|
|
5860
|
+
return {};
|
|
5861
|
+
}
|
|
5862
|
+
}
|
|
5863
|
+
function text(value) {
|
|
5864
|
+
if (value === void 0 || value === null) return void 0;
|
|
5865
|
+
const s = String(value).trim();
|
|
5866
|
+
return s ? s : void 0;
|
|
5867
|
+
}
|
|
5868
|
+
function parseQuery(search) {
|
|
5869
|
+
try {
|
|
5870
|
+
return new URLSearchParams(typeof search === "string" ? search : "");
|
|
5871
|
+
} catch {
|
|
5872
|
+
return new URLSearchParams();
|
|
5873
|
+
}
|
|
5874
|
+
}
|
|
5875
|
+
function meta(doc, key, value) {
|
|
5876
|
+
return attr(doc, `meta[${key}="${value}"]`, "content");
|
|
5877
|
+
}
|
|
5878
|
+
function attr(doc, selector, name) {
|
|
5879
|
+
try {
|
|
5880
|
+
if (!doc || typeof doc.querySelector !== "function") return void 0;
|
|
5881
|
+
const el = doc.querySelector(selector);
|
|
5882
|
+
const v = el?.getAttribute?.(name);
|
|
5883
|
+
return typeof v === "string" ? v : void 0;
|
|
5884
|
+
} catch {
|
|
5885
|
+
return void 0;
|
|
5886
|
+
}
|
|
5887
|
+
}
|
|
5888
|
+
function stripQueryAndHash(url) {
|
|
5889
|
+
if (!url) return void 0;
|
|
5890
|
+
const cut = url.search(/[?#]/);
|
|
5891
|
+
const clean = cut >= 0 ? url.slice(0, cut) : url;
|
|
5892
|
+
return clean.trim() || void 0;
|
|
5893
|
+
}
|
|
5894
|
+
|
|
5895
|
+
// src/core/slots.ts
|
|
5896
|
+
var SLOT_ID_MAX_LENGTH = 100;
|
|
5897
|
+
var SLOT_DECLARATION_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
5898
|
+
function resolveSlotCampaign(boot, slot, now = Date.now(), platform) {
|
|
5899
|
+
if (!boot) return null;
|
|
5900
|
+
return findSlotAssignment(boot, slot, now, platform) ?? defaultCampaignEntry(boot);
|
|
5901
|
+
}
|
|
5902
|
+
function findSlotAssignment(boot, slot, now = Date.now(), platform) {
|
|
5903
|
+
const name = typeof slot === "string" ? slot.trim() : "";
|
|
5904
|
+
if (!boot || !name) return null;
|
|
5905
|
+
try {
|
|
5906
|
+
const matches = activeCampaigns(boot).filter(
|
|
5907
|
+
(c) => slotsOf(c).some((s) => assignmentMatches(s, name, now, platform))
|
|
5908
|
+
);
|
|
5909
|
+
if (matches.length === 0) return null;
|
|
5910
|
+
const defaultId = toNumber(boot.campaign?.id);
|
|
5911
|
+
return matches.find((c) => toNumber(c.id) === defaultId) ?? matches[0] ?? null;
|
|
5912
|
+
} catch {
|
|
5913
|
+
return null;
|
|
5914
|
+
}
|
|
5915
|
+
}
|
|
5916
|
+
function defaultCampaignEntry(boot) {
|
|
5917
|
+
if (!boot?.campaign || typeof boot.campaign !== "object") return null;
|
|
5918
|
+
const defaultId = toNumber(boot.campaign.id);
|
|
5919
|
+
if (defaultId === void 0) return null;
|
|
5920
|
+
try {
|
|
5921
|
+
const listed = wellFormedCampaigns(boot).find((c) => toNumber(c.id) === defaultId);
|
|
5922
|
+
if (listed) return listed;
|
|
5923
|
+
} catch {
|
|
5924
|
+
}
|
|
5925
|
+
return {
|
|
5926
|
+
...boot.campaign,
|
|
5927
|
+
isDefault: true,
|
|
5928
|
+
primarySponsor: boot.primarySponsor ?? null,
|
|
5929
|
+
secondarySponsors: Array.isArray(boot.secondarySponsors) ? boot.secondarySponsors : [],
|
|
5930
|
+
slots: []
|
|
5931
|
+
};
|
|
5932
|
+
}
|
|
5933
|
+
function wellFormedCampaigns(boot) {
|
|
5934
|
+
const list = Array.isArray(boot.campaigns) ? boot.campaigns : [];
|
|
5935
|
+
return list.filter((c) => !!c && typeof c === "object");
|
|
5936
|
+
}
|
|
5937
|
+
function activeCampaigns(boot) {
|
|
5938
|
+
return wellFormedCampaigns(boot).filter((c) => c.status === "active");
|
|
5939
|
+
}
|
|
5940
|
+
function slotsOf(campaign) {
|
|
5941
|
+
const slots = Array.isArray(campaign.slots) ? campaign.slots : [];
|
|
5942
|
+
return slots.filter((s) => !!s && typeof s === "object");
|
|
5943
|
+
}
|
|
5944
|
+
function assignmentMatches(s, name, now, platform) {
|
|
5945
|
+
if (typeof s.name !== "string" || s.name.trim() !== name) return false;
|
|
5946
|
+
if (platform && s.platform != null && s.platform !== platform) return false;
|
|
5947
|
+
const start = parseTime(s.startDate);
|
|
5948
|
+
if (start !== void 0 && start > now) return false;
|
|
5949
|
+
const end = parseTime(s.endDate);
|
|
5950
|
+
if (end !== void 0 && now >= end) return false;
|
|
5951
|
+
return true;
|
|
5952
|
+
}
|
|
5953
|
+
function parseTime(value) {
|
|
5954
|
+
if (value == null || value === "") return void 0;
|
|
5955
|
+
const t = Date.parse(String(value));
|
|
5956
|
+
return Number.isFinite(t) ? t : void 0;
|
|
5957
|
+
}
|
|
5958
|
+
function toNumber(value) {
|
|
5959
|
+
if (value === void 0 || value === null || value === "") return void 0;
|
|
5960
|
+
const n = Number(value);
|
|
5961
|
+
return Number.isFinite(n) ? n : void 0;
|
|
5962
|
+
}
|
|
5963
|
+
function normalizeSlotId(value) {
|
|
5964
|
+
if (typeof value !== "string") return "";
|
|
5965
|
+
return value.trim().slice(0, SLOT_ID_MAX_LENGTH);
|
|
5966
|
+
}
|
|
5967
|
+
function normalizeSlotLocations(slots) {
|
|
5968
|
+
const out = [];
|
|
5969
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5970
|
+
if (!Array.isArray(slots)) return out;
|
|
5971
|
+
for (const raw of slots) {
|
|
5972
|
+
const id = normalizeSlotId(typeof raw === "string" ? raw : raw?.id);
|
|
5973
|
+
if (!id || seen.has(id)) continue;
|
|
5974
|
+
seen.add(id);
|
|
5975
|
+
const displayName = raw && typeof raw === "object" && typeof raw.displayName === "string" ? raw.displayName.trim() : "";
|
|
5976
|
+
out.push(displayName ? { id, displayName } : { id });
|
|
5977
|
+
}
|
|
5978
|
+
return out;
|
|
5979
|
+
}
|
|
5980
|
+
function autoSlotId(kind, ordinal, scope) {
|
|
5981
|
+
const prefix = normalizeScope(scope);
|
|
5982
|
+
return `${prefix ? `${prefix}-` : ""}${kind}-${ordinal + 1}`;
|
|
5983
|
+
}
|
|
5984
|
+
function autoSlotDisplayName(kind, ordinal, scopeName) {
|
|
5985
|
+
const label = `${kind.charAt(0).toUpperCase()}${kind.slice(1)} ${ordinal + 1}`;
|
|
5986
|
+
const name = typeof scopeName === "string" ? scopeName.trim() : "";
|
|
5987
|
+
return name ? `${label} \xB7 ${name}` : label;
|
|
5988
|
+
}
|
|
5989
|
+
function normalizeScope(scope) {
|
|
5990
|
+
if (typeof scope !== "string") return "";
|
|
5991
|
+
return scope.trim().replace(/\s+/g, "-");
|
|
5992
|
+
}
|
|
5993
|
+
function slotDeclarationKeys(locations) {
|
|
5994
|
+
return locations.map((l) => l.displayName ? `${l.id}${l.displayName}` : l.id).sort();
|
|
5995
|
+
}
|
|
5996
|
+
function slotDeclarationStorageKey(platform, apiKey) {
|
|
5997
|
+
return `vio.slots.${platform}.${fnv1a(apiKey)}`;
|
|
5998
|
+
}
|
|
5999
|
+
function fnv1a(input) {
|
|
6000
|
+
let hash = 2166136261;
|
|
6001
|
+
for (let i = 0; i < input.length; i++) {
|
|
6002
|
+
hash ^= input.charCodeAt(i);
|
|
6003
|
+
hash = Math.imul(hash, 16777619) >>> 0;
|
|
6004
|
+
}
|
|
6005
|
+
return hash.toString(16).padStart(8, "0");
|
|
6006
|
+
}
|
|
6007
|
+
function readSlotDeclaration(storageKey) {
|
|
6008
|
+
if (typeof localStorage === "undefined") return null;
|
|
6009
|
+
try {
|
|
6010
|
+
const raw = localStorage.getItem(storageKey);
|
|
6011
|
+
if (!raw) return null;
|
|
6012
|
+
const parsed = JSON.parse(raw);
|
|
6013
|
+
if (!parsed || !Array.isArray(parsed.keys) || typeof parsed.at !== "number") return null;
|
|
6014
|
+
return { keys: parsed.keys.filter((x) => typeof x === "string"), at: parsed.at };
|
|
6015
|
+
} catch {
|
|
6016
|
+
return null;
|
|
6017
|
+
}
|
|
6018
|
+
}
|
|
6019
|
+
function writeSlotDeclaration(storageKey, memory) {
|
|
6020
|
+
if (typeof localStorage === "undefined") return;
|
|
6021
|
+
try {
|
|
6022
|
+
localStorage.setItem(storageKey, JSON.stringify(memory));
|
|
6023
|
+
} catch {
|
|
6024
|
+
}
|
|
6025
|
+
}
|
|
6026
|
+
function isSlotDeclarationFresh(memory, keys, now = Date.now()) {
|
|
6027
|
+
if (!memory) return false;
|
|
6028
|
+
if (now - memory.at >= SLOT_DECLARATION_TTL_MS || now < memory.at) return false;
|
|
6029
|
+
if (memory.keys.length !== keys.length) return false;
|
|
6030
|
+
const remembered = [...memory.keys].sort();
|
|
6031
|
+
const wanted = [...keys].sort();
|
|
6032
|
+
return remembered.every((key, i) => key === wanted[i]);
|
|
6033
|
+
}
|
|
6034
|
+
|
|
5589
6035
|
// src/core/analytics/analytics-manager.ts
|
|
5590
6036
|
var SESSION_TTL_MS = 30 * 60 * 1e3;
|
|
5591
6037
|
var FLUSH_INTERVAL_MS = 5e3;
|
|
@@ -5747,15 +6193,16 @@ var AnalyticsManager = class extends EventTarget {
|
|
|
5747
6193
|
if (!Configuration.isInitialized) return;
|
|
5748
6194
|
if (Date.now() < this.retryNotBefore) return;
|
|
5749
6195
|
const batch = this.queue.splice(0, MAX_QUEUE);
|
|
6196
|
+
const apiKey = Configuration.get().apiKey;
|
|
5750
6197
|
const body = JSON.stringify({
|
|
5751
|
-
apiKey
|
|
6198
|
+
apiKey,
|
|
5752
6199
|
sent_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5753
6200
|
events: batch
|
|
5754
6201
|
});
|
|
5755
6202
|
try {
|
|
5756
6203
|
const res = await fetch(`${eventsBase()}/v1/events`, {
|
|
5757
6204
|
method: "POST",
|
|
5758
|
-
headers: { "Content-Type": "application/json" },
|
|
6205
|
+
headers: { "Content-Type": "application/json", "X-Api-Key": apiKey },
|
|
5759
6206
|
body,
|
|
5760
6207
|
keepalive: batch.length <= 20
|
|
5761
6208
|
// keepalive caps payload at ~64KB
|
|
@@ -5782,22 +6229,25 @@ var AnalyticsManager = class extends EventTarget {
|
|
|
5782
6229
|
if (!anonId) return null;
|
|
5783
6230
|
const boot = vioBootstrapCache();
|
|
5784
6231
|
const ctx = opts.context ?? {};
|
|
6232
|
+
const content = ctx.contentId === void 0 || ctx.contentTitle === void 0 ? vioContent() : {};
|
|
5785
6233
|
const context = compact({
|
|
5786
|
-
campaign_id: ctx
|
|
6234
|
+
campaign_id: resolveCampaignId(ctx, boot),
|
|
5787
6235
|
broadcast_id: ctx.broadcastId,
|
|
5788
6236
|
campaign_component_id: ctx.campaignComponentId,
|
|
5789
6237
|
app_placement_id: ctx.appPlacementId,
|
|
5790
|
-
|
|
6238
|
+
// A slot is a location id to the campaigns service (ADR-0027).
|
|
6239
|
+
location_id: ctx.locationId ?? ctx.slot,
|
|
5791
6240
|
component_template_id: ctx.componentTemplateId,
|
|
5792
6241
|
sponsor_id: ctx.sponsorId,
|
|
5793
6242
|
activation_id: ctx.activationId,
|
|
5794
6243
|
content_url: ctx.contentUrl ?? (typeof location !== "undefined" ? location.href : void 0),
|
|
5795
6244
|
// The host's own content id — its join key back onto its reporting.
|
|
5796
|
-
|
|
6245
|
+
// Explicit on the event, else what the SDK resolved for the page.
|
|
6246
|
+
content_id: ctx.contentId ?? content.id,
|
|
5797
6247
|
// Deletion-proof reporting: sources have no trash bin — the title
|
|
5798
6248
|
// snapshotted at event time keeps reports legible if content dies.
|
|
5799
6249
|
error_code: ctx.errorCode,
|
|
5800
|
-
content_title: ctx.contentTitle ?? (typeof document !== "undefined" && document.title ? document.title.slice(0, 255) : void 0),
|
|
6250
|
+
content_title: ctx.contentTitle ?? content.title ?? (typeof document !== "undefined" && document.title ? document.title.slice(0, 255) : void 0),
|
|
5801
6251
|
variant: ctx.variant
|
|
5802
6252
|
});
|
|
5803
6253
|
const com = opts.commerce;
|
|
@@ -5945,7 +6395,7 @@ var AnalyticsManager = class extends EventTarget {
|
|
|
5945
6395
|
const d = ev.detail ?? {};
|
|
5946
6396
|
const line = this.findLine(d.productId, d.variantId);
|
|
5947
6397
|
this.track("add_to_cart", {
|
|
5948
|
-
context: { sponsorId: d.sponsorId !== void 0 ? Number(d.sponsorId) : void 0 },
|
|
6398
|
+
context: { sponsorId: d.sponsorId !== void 0 ? Number(d.sponsorId) : void 0, slot: slotOf(d), ...contentOf(d) },
|
|
5949
6399
|
commerce: {
|
|
5950
6400
|
items: [
|
|
5951
6401
|
{
|
|
@@ -5966,7 +6416,7 @@ var AnalyticsManager = class extends EventTarget {
|
|
|
5966
6416
|
const d = ev.detail ?? {};
|
|
5967
6417
|
if (d.productId === void 0) return;
|
|
5968
6418
|
this.track("remove_from_cart", {
|
|
5969
|
-
context: { sponsorId: d.sponsorId },
|
|
6419
|
+
context: { sponsorId: d.sponsorId, slot: slotOf(d), ...contentOf(d) },
|
|
5970
6420
|
commerce: {
|
|
5971
6421
|
items: [
|
|
5972
6422
|
{
|
|
@@ -5995,7 +6445,7 @@ var AnalyticsManager = class extends EventTarget {
|
|
|
5995
6445
|
if (d.productId === void 0) return;
|
|
5996
6446
|
const price = Number(d.price);
|
|
5997
6447
|
this.track("select_item", {
|
|
5998
|
-
context: { sponsorId: d.sponsorId !== void 0 ? Number(d.sponsorId) : void 0 },
|
|
6448
|
+
context: { sponsorId: d.sponsorId !== void 0 ? Number(d.sponsorId) : void 0, slot: slotOf(d), ...contentOf(d) },
|
|
5999
6449
|
commerce: {
|
|
6000
6450
|
items: [
|
|
6001
6451
|
{
|
|
@@ -6014,26 +6464,128 @@ var AnalyticsManager = class extends EventTarget {
|
|
|
6014
6464
|
});
|
|
6015
6465
|
on("vio:checkout-open", (ev) => {
|
|
6016
6466
|
const d = ev.detail ?? {};
|
|
6467
|
+
const slices = this.cartSlices(d.sponsorId);
|
|
6468
|
+
if (slices.length > 1) {
|
|
6469
|
+
for (const slice of slices) {
|
|
6470
|
+
this.track("begin_checkout", {
|
|
6471
|
+
context: this.sliceContext(slice, { sponsorId: d.sponsorId, slot: slotOf(d), ...contentOf(d) }),
|
|
6472
|
+
commerce: slice.commerce
|
|
6473
|
+
});
|
|
6474
|
+
}
|
|
6475
|
+
return;
|
|
6476
|
+
}
|
|
6017
6477
|
const snap = this.cartCommerce(d.sponsorId);
|
|
6018
6478
|
this.track("begin_checkout", {
|
|
6019
|
-
context: { sponsorId: d.sponsorId },
|
|
6479
|
+
context: { sponsorId: d.sponsorId, slot: slotOf(d), ...contentOf(d) },
|
|
6020
6480
|
...snap ? { commerce: snap } : {}
|
|
6021
6481
|
});
|
|
6022
6482
|
});
|
|
6023
6483
|
on("vio:payment-success", (ev) => {
|
|
6024
6484
|
const d = ev.detail ?? {};
|
|
6025
6485
|
const sponsorId = d.sponsorId !== void 0 ? Number(d.sponsorId) : void 0;
|
|
6486
|
+
const slices = this.cartSlices(sponsorId);
|
|
6487
|
+
if (slices.length > 1) {
|
|
6488
|
+
for (const slice of slices) {
|
|
6489
|
+
this.track("purchase", {
|
|
6490
|
+
context: this.sliceContext(slice, { sponsorId, slot: slotOf(d), ...contentOf(d) }),
|
|
6491
|
+
commerce: { ...slice.commerce, ...purchaseFromPaymentSuccess(d), paymentMethod: d.method }
|
|
6492
|
+
});
|
|
6493
|
+
}
|
|
6494
|
+
return;
|
|
6495
|
+
}
|
|
6026
6496
|
const snap = this.cartCommerce(sponsorId);
|
|
6027
6497
|
this.track("purchase", {
|
|
6028
|
-
context: { sponsorId },
|
|
6498
|
+
context: { sponsorId, slot: slotOf(d), ...contentOf(d) },
|
|
6029
6499
|
commerce: {
|
|
6500
|
+
// value / currency / items: the cart snapshot, on every path.
|
|
6030
6501
|
...snap ?? {},
|
|
6031
|
-
|
|
6502
|
+
// order_id: Vio's id for the purchase (never the provider's).
|
|
6503
|
+
...purchaseFromPaymentSuccess(d),
|
|
6032
6504
|
paymentMethod: d.method
|
|
6033
6505
|
}
|
|
6034
6506
|
});
|
|
6035
6507
|
});
|
|
6036
6508
|
}
|
|
6509
|
+
/** The lines of the cart (current, else the pre-clear snapshot), optionally one sponsor's. */
|
|
6510
|
+
cartLines(sponsorId) {
|
|
6511
|
+
let items = [];
|
|
6512
|
+
const cart = vioCart();
|
|
6513
|
+
if (cart) {
|
|
6514
|
+
cart.getAllCarts().forEach((state, id) => {
|
|
6515
|
+
if (sponsorId === void 0 || id === sponsorId) items.push(...state.items);
|
|
6516
|
+
});
|
|
6517
|
+
}
|
|
6518
|
+
if (items.length === 0 && this.lastCartSnapshot) {
|
|
6519
|
+
items = this.lastCartSnapshot.items.filter(
|
|
6520
|
+
(i) => sponsorId === void 0 || i.sponsorId === sponsorId
|
|
6521
|
+
);
|
|
6522
|
+
}
|
|
6523
|
+
return items;
|
|
6524
|
+
}
|
|
6525
|
+
/**
|
|
6526
|
+
* The cart split per campaign (ADR-0034): every unit of every line goes to
|
|
6527
|
+
* the campaign of its origin — the one the place showed when it was added
|
|
6528
|
+
* — and units without one to the campaign the current rule resolves for
|
|
6529
|
+
* the line's sponsor. One slice per campaign: its items (quantities from
|
|
6530
|
+
* the origins), its value, and the slot / content when they are the same
|
|
6531
|
+
* for the whole slice. A cart with one campaign is one slice.
|
|
6532
|
+
*/
|
|
6533
|
+
cartSlices(sponsorId) {
|
|
6534
|
+
const lines = this.cartLines(sponsorId);
|
|
6535
|
+
if (lines.length === 0) return [];
|
|
6536
|
+
const boot = vioBootstrapCache();
|
|
6537
|
+
const slices = /* @__PURE__ */ new Map();
|
|
6538
|
+
for (const line of lines) {
|
|
6539
|
+
for (const origin of lineOrigins(line)) {
|
|
6540
|
+
const campaignId = origin.campaignId ?? resolveCampaignId({ slot: origin.slot, sponsorId: line.sponsorId }, boot);
|
|
6541
|
+
const key = campaignId === void 0 ? "default" : String(campaignId);
|
|
6542
|
+
let slice = slices.get(key);
|
|
6543
|
+
if (!slice) {
|
|
6544
|
+
slice = {
|
|
6545
|
+
campaignId,
|
|
6546
|
+
commerce: { items: [], value: 0, currency: line.currency ?? this.lastCartSnapshot?.currency },
|
|
6547
|
+
slots: /* @__PURE__ */ new Set(),
|
|
6548
|
+
contents: /* @__PURE__ */ new Set(),
|
|
6549
|
+
lines: /* @__PURE__ */ new Map()
|
|
6550
|
+
};
|
|
6551
|
+
slices.set(key, slice);
|
|
6552
|
+
}
|
|
6553
|
+
if (origin.slot) slice.slots.add(origin.slot);
|
|
6554
|
+
if (origin.contentId) slice.contents.add(origin.contentId);
|
|
6555
|
+
const itemKey = `${line.productId}::${line.variantId ?? ""}`;
|
|
6556
|
+
const item = slice.lines.get(itemKey);
|
|
6557
|
+
if (item) item.quantity = (item.quantity ?? 0) + origin.quantity;
|
|
6558
|
+
else {
|
|
6559
|
+
const fresh = {
|
|
6560
|
+
productId: line.productId,
|
|
6561
|
+
name: line.name,
|
|
6562
|
+
brand: line.brand,
|
|
6563
|
+
variantId: line.variantId,
|
|
6564
|
+
price: line.unitPrice,
|
|
6565
|
+
quantity: origin.quantity
|
|
6566
|
+
};
|
|
6567
|
+
slice.lines.set(itemKey, fresh);
|
|
6568
|
+
slice.commerce.items.push(fresh);
|
|
6569
|
+
}
|
|
6570
|
+
slice.commerce.value = (slice.commerce.value ?? 0) + line.unitPrice * origin.quantity;
|
|
6571
|
+
}
|
|
6572
|
+
}
|
|
6573
|
+
return [...slices.values()].map((s) => ({
|
|
6574
|
+
campaignId: s.campaignId,
|
|
6575
|
+
slot: s.slots.size === 1 ? [...s.slots][0] : void 0,
|
|
6576
|
+
contentId: s.contents.size === 1 ? [...s.contents][0] : void 0,
|
|
6577
|
+
commerce: s.commerce
|
|
6578
|
+
}));
|
|
6579
|
+
}
|
|
6580
|
+
/** The context of a per-campaign event: the slice's campaign, slot and content, over the event's own. */
|
|
6581
|
+
sliceContext(slice, base) {
|
|
6582
|
+
return compact({
|
|
6583
|
+
...base,
|
|
6584
|
+
campaignId: slice.campaignId ?? base.campaignId,
|
|
6585
|
+
slot: slice.slot ?? base.slot,
|
|
6586
|
+
contentId: slice.contentId ?? base.contentId
|
|
6587
|
+
});
|
|
6588
|
+
}
|
|
6037
6589
|
/** Current cart → commerce payload; falls back to the pre-clear snapshot. */
|
|
6038
6590
|
cartCommerce(sponsorId) {
|
|
6039
6591
|
let items = [];
|
|
@@ -6119,6 +6671,14 @@ function vioFacade() {
|
|
|
6119
6671
|
function vioBootstrapCache() {
|
|
6120
6672
|
return vioFacade()?.bootstrapCache ?? null;
|
|
6121
6673
|
}
|
|
6674
|
+
function vioContent() {
|
|
6675
|
+
try {
|
|
6676
|
+
const facade = vioFacade();
|
|
6677
|
+
return typeof facade?.content === "function" ? facade.content() : resolveContent();
|
|
6678
|
+
} catch {
|
|
6679
|
+
return {};
|
|
6680
|
+
}
|
|
6681
|
+
}
|
|
6122
6682
|
function vioCart() {
|
|
6123
6683
|
try {
|
|
6124
6684
|
return vioFacade()?.cart ?? null;
|
|
@@ -6126,6 +6686,67 @@ function vioCart() {
|
|
|
6126
6686
|
return null;
|
|
6127
6687
|
}
|
|
6128
6688
|
}
|
|
6689
|
+
function resolveCampaignId(ctx, boot) {
|
|
6690
|
+
if (ctx.campaignId !== void 0) return ctx.campaignId;
|
|
6691
|
+
const defaultId = boot?.campaign?.id;
|
|
6692
|
+
if (ctx.slot) {
|
|
6693
|
+
const platform = Configuration.isInitialized ? Configuration.get().platform : void 0;
|
|
6694
|
+
const assigned = toNumber2(findSlotAssignment(boot, ctx.slot, Date.now(), platform)?.id);
|
|
6695
|
+
if (assigned !== void 0) return assigned;
|
|
6696
|
+
}
|
|
6697
|
+
const sponsorId = toNumber2(ctx.sponsorId);
|
|
6698
|
+
if (sponsorId === void 0) return defaultId;
|
|
6699
|
+
try {
|
|
6700
|
+
const campaigns = Array.isArray(boot?.campaigns) ? boot.campaigns : [];
|
|
6701
|
+
const matches = campaigns.filter(
|
|
6702
|
+
(c) => !!c && typeof c === "object" && c.status === "active" && campaignSponsorIds(c).includes(sponsorId)
|
|
6703
|
+
);
|
|
6704
|
+
const chosen = matches.find((c) => c.id === defaultId) ?? matches[0];
|
|
6705
|
+
const chosenId = toNumber2(chosen?.id);
|
|
6706
|
+
if (chosenId !== void 0) return chosenId;
|
|
6707
|
+
} catch {
|
|
6708
|
+
}
|
|
6709
|
+
return defaultId;
|
|
6710
|
+
}
|
|
6711
|
+
function slotOf(detail) {
|
|
6712
|
+
return typeof detail.slot === "string" && detail.slot.trim() ? detail.slot.trim() : void 0;
|
|
6713
|
+
}
|
|
6714
|
+
function contentOf(detail) {
|
|
6715
|
+
const out = {};
|
|
6716
|
+
if (typeof detail.contentId === "number" && Number.isFinite(detail.contentId)) out.contentId = detail.contentId;
|
|
6717
|
+
else if (typeof detail.contentId === "string" && detail.contentId.trim()) out.contentId = detail.contentId.trim();
|
|
6718
|
+
if (typeof detail.contentTitle === "string" && detail.contentTitle.trim()) out.contentTitle = detail.contentTitle.trim();
|
|
6719
|
+
return out;
|
|
6720
|
+
}
|
|
6721
|
+
function campaignSponsorIds(campaign) {
|
|
6722
|
+
const ids = [];
|
|
6723
|
+
const primary = toNumber2(campaign.primarySponsor?.id);
|
|
6724
|
+
if (primary !== void 0) ids.push(primary);
|
|
6725
|
+
const secondaries = Array.isArray(campaign.secondarySponsors) ? campaign.secondarySponsors : [];
|
|
6726
|
+
for (const s of secondaries) {
|
|
6727
|
+
const id = toNumber2(s?.id);
|
|
6728
|
+
if (id !== void 0) ids.push(id);
|
|
6729
|
+
}
|
|
6730
|
+
return ids;
|
|
6731
|
+
}
|
|
6732
|
+
function purchaseFromPaymentSuccess(d) {
|
|
6733
|
+
const r = d.result ?? {};
|
|
6734
|
+
const orderId = firstId(d.orderId, r.confirmation?.orderId, d.checkoutId);
|
|
6735
|
+
return orderId !== void 0 ? { orderId } : {};
|
|
6736
|
+
}
|
|
6737
|
+
function firstId(...candidates) {
|
|
6738
|
+
for (const c of candidates) {
|
|
6739
|
+
if (c === void 0 || c === null) continue;
|
|
6740
|
+
const s = String(c);
|
|
6741
|
+
if (s !== "") return s;
|
|
6742
|
+
}
|
|
6743
|
+
return void 0;
|
|
6744
|
+
}
|
|
6745
|
+
function toNumber2(value) {
|
|
6746
|
+
if (value === void 0 || value === null || value === "") return void 0;
|
|
6747
|
+
const n = Number(value);
|
|
6748
|
+
return Number.isFinite(n) ? n : void 0;
|
|
6749
|
+
}
|
|
6129
6750
|
function pushGa4(event) {
|
|
6130
6751
|
const commerceNames = /* @__PURE__ */ new Set([
|
|
6131
6752
|
"view_item_list",
|
|
@@ -6162,6 +6783,7 @@ function pushGa4(event) {
|
|
|
6162
6783
|
}
|
|
6163
6784
|
|
|
6164
6785
|
// src/core/client.ts
|
|
6786
|
+
var SLOT_DECLARATION_DEBOUNCE_MS = 1e3;
|
|
6165
6787
|
var VioFacade = class {
|
|
6166
6788
|
constructor() {
|
|
6167
6789
|
this.apiInstance = null;
|
|
@@ -6171,6 +6793,18 @@ var VioFacade = class {
|
|
|
6171
6793
|
/** Last bootstrap failure — short backoff so cart mutations don't hammer a
|
|
6172
6794
|
* dead endpoint on every call. */
|
|
6173
6795
|
this.bootstrapFailedAt = 0;
|
|
6796
|
+
/** Bumped by every init(). A bootstrap started under an older configuration
|
|
6797
|
+
* (another key or environment) is still in flight after a re-init; its answer
|
|
6798
|
+
* belongs to that configuration and must not land in this one. */
|
|
6799
|
+
this.generation = 0;
|
|
6800
|
+
/** Slot ids present on the page (`Vio.registerSlot`): how many elements hold each + its display name. */
|
|
6801
|
+
this.slotRegistry = /* @__PURE__ */ new Map();
|
|
6802
|
+
/** Pending debounced declaration of the registry. */
|
|
6803
|
+
this.slotDeclareTimer = null;
|
|
6804
|
+
/** A name appeared (or the key changed) since the registry was last declared. */
|
|
6805
|
+
this.slotsDirty = false;
|
|
6806
|
+
/** `Vio.content()` resolved once per page; forgotten on every init(). */
|
|
6807
|
+
this.contentCache = null;
|
|
6174
6808
|
// Client-side managers — created lazily on first access. They don't
|
|
6175
6809
|
// need Vio.init() to work (state is purely local).
|
|
6176
6810
|
this.cartManager = null;
|
|
@@ -6184,7 +6818,33 @@ var VioFacade = class {
|
|
|
6184
6818
|
this.bootstrapPromise = null;
|
|
6185
6819
|
this.cachedBootstrap = null;
|
|
6186
6820
|
this.bootstrapFailedAt = 0;
|
|
6821
|
+
this.generation++;
|
|
6187
6822
|
this.checkoutManager?.clearPaymentMethodsCache();
|
|
6823
|
+
if (this.slotDeclareTimer) {
|
|
6824
|
+
clearTimeout(this.slotDeclareTimer);
|
|
6825
|
+
this.slotDeclareTimer = null;
|
|
6826
|
+
}
|
|
6827
|
+
this.slotsDirty = this.slotRegistry.size > 0;
|
|
6828
|
+
this.contentCache = null;
|
|
6829
|
+
}
|
|
6830
|
+
/**
|
|
6831
|
+
* The content (article, page) this SDK instance is rendered in — what every
|
|
6832
|
+
* analytics event carries as `content_id` / `content_title` when the event
|
|
6833
|
+
* has no explicit one. Resolved once per page (init option → URL query →
|
|
6834
|
+
* `vio:content-*` metas → canonical/og:url → pathname; see
|
|
6835
|
+
* `resolveContent`), forgotten on every `init()`. Never throws; `{}` without
|
|
6836
|
+
* a document.
|
|
6837
|
+
*/
|
|
6838
|
+
content() {
|
|
6839
|
+
if (this.contentCache) return this.contentCache;
|
|
6840
|
+
let init;
|
|
6841
|
+
if (Configuration.isInitialized) {
|
|
6842
|
+
const cfg = Configuration.get();
|
|
6843
|
+
init = { contentId: cfg.contentId, contentTitle: cfg.contentTitle };
|
|
6844
|
+
}
|
|
6845
|
+
const resolved = resolveContent(init);
|
|
6846
|
+
if (typeof document !== "undefined") this.contentCache = resolved;
|
|
6847
|
+
return resolved;
|
|
6188
6848
|
}
|
|
6189
6849
|
get isInitialized() {
|
|
6190
6850
|
return Configuration.isInitialized;
|
|
@@ -6223,6 +6883,11 @@ var VioFacade = class {
|
|
|
6223
6883
|
/**
|
|
6224
6884
|
* Bootstrap — fetches client config and caches it. Dedupes in-flight
|
|
6225
6885
|
* concurrent requests.
|
|
6886
|
+
*
|
|
6887
|
+
* A request that was started before the last init() (Vev starts with the key
|
|
6888
|
+
* the browser remembers, then the page's own key arrives) answers for a key
|
|
6889
|
+
* that is gone: it neither fills the cache nor arms the failure backoff, and
|
|
6890
|
+
* whoever awaited it gets the current configuration's bootstrap instead.
|
|
6226
6891
|
*/
|
|
6227
6892
|
async bootstrap() {
|
|
6228
6893
|
if (this.cachedBootstrap) return this.cachedBootstrap;
|
|
@@ -6230,17 +6895,25 @@ var VioFacade = class {
|
|
|
6230
6895
|
if (this.bootstrapFailedAt && Date.now() - this.bootstrapFailedAt < 5e3) {
|
|
6231
6896
|
throw new Error("[Vio] bootstrap recently failed \u2014 backing off");
|
|
6232
6897
|
}
|
|
6233
|
-
|
|
6234
|
-
|
|
6235
|
-
|
|
6236
|
-
|
|
6237
|
-
|
|
6238
|
-
|
|
6239
|
-
|
|
6240
|
-
|
|
6241
|
-
|
|
6898
|
+
const generation = this.generation;
|
|
6899
|
+
const promise = this.api.bootstrap(Configuration.get().platform).then(
|
|
6900
|
+
(res) => {
|
|
6901
|
+
if (generation !== this.generation) return this.bootstrap();
|
|
6902
|
+
this.cachedBootstrap = res;
|
|
6903
|
+
this.bootstrapFailedAt = 0;
|
|
6904
|
+
if (this.slotsDirty) this.scheduleSlotDeclaration();
|
|
6905
|
+
return res;
|
|
6906
|
+
},
|
|
6907
|
+
(err) => {
|
|
6908
|
+
if (generation !== this.generation) return this.bootstrap();
|
|
6909
|
+
this.bootstrapFailedAt = Date.now();
|
|
6910
|
+
throw err;
|
|
6911
|
+
}
|
|
6912
|
+
).finally(() => {
|
|
6913
|
+
if (this.bootstrapPromise === promise) this.bootstrapPromise = null;
|
|
6242
6914
|
});
|
|
6243
|
-
|
|
6915
|
+
this.bootstrapPromise = promise;
|
|
6916
|
+
return promise;
|
|
6244
6917
|
}
|
|
6245
6918
|
get bootstrapCache() {
|
|
6246
6919
|
return this.cachedBootstrap;
|
|
@@ -6269,14 +6942,176 @@ var VioFacade = class {
|
|
|
6269
6942
|
const cfg = Configuration.get();
|
|
6270
6943
|
return createCommerceClient({ endpoint: cfg.graphQLBase, apiKey: commerceApiKey });
|
|
6271
6944
|
}
|
|
6945
|
+
// MARK: - Slots (ADR-0027 / ADR-0034)
|
|
6946
|
+
/**
|
|
6947
|
+
* Declare this page's slots to the campaigns service
|
|
6948
|
+
* (`POST /v2/mobile/components/manifest`, `mode: 'merge'`, the init
|
|
6949
|
+
* platform). Best-effort: it NEVER throws — a failure is logged and resolves
|
|
6950
|
+
* `null`. The same set is not re-posted from this browser for 24 h unless
|
|
6951
|
+
* `force` (then `null` also means "skipped, already declared"). Ids are
|
|
6952
|
+
* trimmed, cut at 100 chars, deduped; empties are dropped.
|
|
6953
|
+
*/
|
|
6954
|
+
async declareSlots(slots, opts = {}) {
|
|
6955
|
+
try {
|
|
6956
|
+
const locations = normalizeSlotLocations(slots);
|
|
6957
|
+
if (locations.length === 0) return null;
|
|
6958
|
+
if (!Configuration.isInitialized) {
|
|
6959
|
+
warn("declareSlots() before Vio.init() \u2014 nothing declared");
|
|
6960
|
+
return null;
|
|
6961
|
+
}
|
|
6962
|
+
const cfg = Configuration.get();
|
|
6963
|
+
const storageKey = slotDeclarationStorageKey(cfg.platform, cfg.apiKey);
|
|
6964
|
+
const keys = slotDeclarationKeys(locations);
|
|
6965
|
+
const now = Date.now();
|
|
6966
|
+
if (!opts.force && isSlotDeclarationFresh(readSlotDeclaration(storageKey), keys, now)) {
|
|
6967
|
+
return null;
|
|
6968
|
+
}
|
|
6969
|
+
const result = await this.api.declareManifest({
|
|
6970
|
+
platform: cfg.platform,
|
|
6971
|
+
mode: "merge",
|
|
6972
|
+
locations
|
|
6973
|
+
});
|
|
6974
|
+
if (result?.mode === "merge") {
|
|
6975
|
+
writeSlotDeclaration(storageKey, { keys, at: now });
|
|
6976
|
+
} else {
|
|
6977
|
+
warn(
|
|
6978
|
+
"slots declared but the server ignored mode=merge (old server); not caching, will retry next load"
|
|
6979
|
+
);
|
|
6980
|
+
}
|
|
6981
|
+
return result;
|
|
6982
|
+
} catch (err) {
|
|
6983
|
+
warn("declareSlots() failed \u2014", err);
|
|
6984
|
+
return null;
|
|
6985
|
+
}
|
|
6986
|
+
}
|
|
6987
|
+
/**
|
|
6988
|
+
* Register a slot present on the page (the `<vio-*>` elements call it with
|
|
6989
|
+
* their effective id — the host's `vio-slot` or the auto-generated one —
|
|
6990
|
+
* and the optional display name). The registry is declared ONCE, debounced,
|
|
6991
|
+
* after the first successful bootstrap — and again whenever a new id or a
|
|
6992
|
+
* new display name appears. A `displayName` given later for an id already
|
|
6993
|
+
* registered replaces the one kept; `undefined` leaves it alone. The
|
|
6994
|
+
* returned function unregisters; removal never re-declares.
|
|
6995
|
+
*/
|
|
6996
|
+
registerSlot(name, displayName) {
|
|
6997
|
+
const id = normalizeSlotId(name);
|
|
6998
|
+
if (!id) return () => {
|
|
6999
|
+
};
|
|
7000
|
+
const label = typeof displayName === "string" && displayName.trim() ? displayName.trim() : void 0;
|
|
7001
|
+
const entry = this.slotRegistry.get(id);
|
|
7002
|
+
let changed = false;
|
|
7003
|
+
if (!entry) {
|
|
7004
|
+
this.slotRegistry.set(id, { count: 1, displayName: label });
|
|
7005
|
+
changed = true;
|
|
7006
|
+
} else {
|
|
7007
|
+
entry.count++;
|
|
7008
|
+
if (label !== void 0 && label !== entry.displayName) {
|
|
7009
|
+
entry.displayName = label;
|
|
7010
|
+
changed = true;
|
|
7011
|
+
}
|
|
7012
|
+
}
|
|
7013
|
+
if (changed) {
|
|
7014
|
+
this.slotsDirty = true;
|
|
7015
|
+
if (this.cachedBootstrap) this.scheduleSlotDeclaration();
|
|
7016
|
+
}
|
|
7017
|
+
let released = false;
|
|
7018
|
+
return () => {
|
|
7019
|
+
if (released) return;
|
|
7020
|
+
released = true;
|
|
7021
|
+
const current = this.slotRegistry.get(id);
|
|
7022
|
+
if (!current) return;
|
|
7023
|
+
if (current.count <= 1) this.slotRegistry.delete(id);
|
|
7024
|
+
else current.count--;
|
|
7025
|
+
};
|
|
7026
|
+
}
|
|
7027
|
+
/**
|
|
7028
|
+
* The campaign a slot shows now, from the last bootstrap: its assigned and
|
|
7029
|
+
* open campaign for the init platform, else the surface's default. With no
|
|
7030
|
+
* slot, the default. `null` before the bootstrap. See `resolveSlotCampaign`.
|
|
7031
|
+
*/
|
|
7032
|
+
campaignForSlot(slot) {
|
|
7033
|
+
const platform = Configuration.isInitialized ? Configuration.get().platform : void 0;
|
|
7034
|
+
return resolveSlotCampaign(this.cachedBootstrap, slot ?? null, Date.now(), platform);
|
|
7035
|
+
}
|
|
7036
|
+
/**
|
|
7037
|
+
* THE attribution helper — the SDK elements, Vev and Replit all use this
|
|
7038
|
+
* one. For a place (`slot`): the campaign it shows right now
|
|
7039
|
+
* (`campaignForSlot`: its assignment, else the surface's default) as
|
|
7040
|
+
* `campaignId` + that campaign's primary sponsor as `sponsorId`, plus the
|
|
7041
|
+
* page's content (`content()`). Pure, never throws; `{}` before the
|
|
7042
|
+
* bootstrap, except the content.
|
|
7043
|
+
*
|
|
7044
|
+
* `hint.sponsorId` — when the caller already knows the sponsor (a card with
|
|
7045
|
+
* `sponsor-id`) it is kept as `sponsorId`, and the campaign follows the same
|
|
7046
|
+
* cascade analytics uses (slot assignment → the sponsor's active campaign →
|
|
7047
|
+
* default), so what the cart remembers matches what the events say.
|
|
7048
|
+
*/
|
|
7049
|
+
attribution(slot, hint) {
|
|
7050
|
+
const out = {};
|
|
7051
|
+
try {
|
|
7052
|
+
const name = typeof slot === "string" ? slot.trim() : "";
|
|
7053
|
+
if (name) out.slot = name;
|
|
7054
|
+
const boot = this.cachedBootstrap;
|
|
7055
|
+
const hinted = toNumber3(hint?.sponsorId);
|
|
7056
|
+
if (boot) {
|
|
7057
|
+
if (hinted !== void 0) {
|
|
7058
|
+
const campaignId = resolveCampaignId({ slot: name || void 0, sponsorId: hinted }, boot);
|
|
7059
|
+
if (campaignId !== void 0) out.campaignId = campaignId;
|
|
7060
|
+
out.sponsorId = hinted;
|
|
7061
|
+
} else {
|
|
7062
|
+
const campaign = this.campaignForSlot(name || null);
|
|
7063
|
+
const campaignId = toNumber3(campaign?.id);
|
|
7064
|
+
if (campaignId !== void 0) out.campaignId = campaignId;
|
|
7065
|
+
const sponsorId = toNumber3(campaign?.primarySponsor?.id);
|
|
7066
|
+
if (sponsorId !== void 0) out.sponsorId = sponsorId;
|
|
7067
|
+
}
|
|
7068
|
+
} else if (hinted !== void 0) {
|
|
7069
|
+
out.sponsorId = hinted;
|
|
7070
|
+
}
|
|
7071
|
+
const content = this.content();
|
|
7072
|
+
if (content.id) out.contentId = content.id;
|
|
7073
|
+
if (content.title) out.contentTitle = content.title;
|
|
7074
|
+
} catch {
|
|
7075
|
+
}
|
|
7076
|
+
return out;
|
|
7077
|
+
}
|
|
7078
|
+
scheduleSlotDeclaration() {
|
|
7079
|
+
if (this.slotDeclareTimer) clearTimeout(this.slotDeclareTimer);
|
|
7080
|
+
this.slotDeclareTimer = setTimeout(() => {
|
|
7081
|
+
this.slotDeclareTimer = null;
|
|
7082
|
+
this.slotsDirty = false;
|
|
7083
|
+
void this.declareSlots(
|
|
7084
|
+
[...this.slotRegistry.entries()].map(
|
|
7085
|
+
([id, { displayName }]) => displayName ? { id, displayName } : id
|
|
7086
|
+
)
|
|
7087
|
+
);
|
|
7088
|
+
}, SLOT_DECLARATION_DEBOUNCE_MS);
|
|
7089
|
+
}
|
|
6272
7090
|
findSponsor(sponsorId) {
|
|
6273
7091
|
const boot = this.cachedBootstrap;
|
|
6274
7092
|
if (!boot) return void 0;
|
|
6275
7093
|
const id = Number(sponsorId);
|
|
6276
7094
|
if (Number(boot.primarySponsor?.id) === id) return boot.primarySponsor;
|
|
6277
|
-
|
|
7095
|
+
const secondary = boot.secondarySponsors?.find((s) => Number(s.id) === id);
|
|
7096
|
+
if (secondary) return secondary;
|
|
7097
|
+
const campaigns = Array.isArray(boot.campaigns) ? boot.campaigns : [];
|
|
7098
|
+
for (const c of campaigns) {
|
|
7099
|
+
if (!c || typeof c !== "object") continue;
|
|
7100
|
+
if (Number(c.primarySponsor?.id) === id) return c.primarySponsor ?? void 0;
|
|
7101
|
+
const s = Array.isArray(c.secondarySponsors) ? c.secondarySponsors.find((x) => Number(x?.id) === id) : void 0;
|
|
7102
|
+
if (s) return s;
|
|
7103
|
+
}
|
|
7104
|
+
return void 0;
|
|
6278
7105
|
}
|
|
6279
7106
|
};
|
|
7107
|
+
function warn(...args) {
|
|
7108
|
+
if (typeof console !== "undefined") console.warn("[Vio]", ...args);
|
|
7109
|
+
}
|
|
7110
|
+
function toNumber3(value) {
|
|
7111
|
+
if (value === void 0 || value === null || value === "") return void 0;
|
|
7112
|
+
const n = Number(value);
|
|
7113
|
+
return Number.isFinite(n) ? n : void 0;
|
|
7114
|
+
}
|
|
6280
7115
|
var _vioGlobal = globalThis;
|
|
6281
7116
|
var Vio = _vioGlobal.__VIO_FACADE__ ?? (_vioGlobal.__VIO_FACADE__ = new VioFacade());
|
|
6282
7117
|
|
|
@@ -6329,6 +7164,6 @@ function vippsButtonAttributes(verb = "buy", opts = {}) {
|
|
|
6329
7164
|
};
|
|
6330
7165
|
}
|
|
6331
7166
|
|
|
6332
|
-
export { ADD_ITEM_MUTATION, ADYEN_RETURN_METHOD, ADYEN_RETURN_QUERY_KEYS, AdyenOriginError, AnalyticsManager, CREATE_CART_MUTATION, CREATE_CHECKOUT_MUTATION, CREATE_PAYMENT_STRIPE_MUTATION, CREATE_PAYMENT_VIPPS_MUTATION, CartManager, CheckoutManager, CommerceClient, Configuration, DELETE_ITEM_MUTATION, GET_AVAILABLE_PAYMENT_METHODS_QUERY, GET_CART_QUERY, GET_CHECKOUT_QUERY, GET_LINE_ITEMS_BY_SUPPLIER_QUERY, GET_VIPPS_STATUS_QUERY, KUSTOM_RETURN_QUERY_KEYS, SDK_VERSION, UPDATE_CHECKOUT_MUTATION, UPDATE_ITEM_MUTATION, UPDATE_SHIPPINGS_BY_SUPPLIER_MUTATION, VIPPS_BUTTON_SCRIPT, VIPPS_BUTTON_TAG, Vio, VioApi, VioApiError, __decorateClass, addItem, adyenReturnOutcome, adyenReturnParams, checkApplePayAvailability, createCart, createCheckout, createCommerceClient, createPaymentStripe, createPaymentVipps, createVioApi, deleteItem, displayPrice, ensureVippsButton, executeCartGraphQL, formatPrice, getAvailablePaymentMethods, getCart, getCartGraphQLOptions, getCheckout, getCustomerSessionId, getGlobalCurrency, getLineItemsBySupplier, getVippsStatus, kustomReturnOutcome, nexiReturnPaymentId, primaryImageUrl, readNexiPending, showApplePaySheet, updateCheckout, updateItem, updateShippingsBySupplier, vippsButtonAttributes };
|
|
6333
|
-
//# sourceMappingURL=chunk-
|
|
6334
|
-
//# sourceMappingURL=chunk-
|
|
7167
|
+
export { ADD_ITEM_MUTATION, ADYEN_RETURN_METHOD, ADYEN_RETURN_QUERY_KEYS, AdyenOriginError, AnalyticsManager, CREATE_CART_MUTATION, CREATE_CHECKOUT_MUTATION, CREATE_PAYMENT_STRIPE_MUTATION, CREATE_PAYMENT_VIPPS_MUTATION, CartManager, CheckoutManager, CommerceClient, Configuration, DELETE_ITEM_MUTATION, GET_AVAILABLE_PAYMENT_METHODS_QUERY, GET_CART_QUERY, GET_CHECKOUT_QUERY, GET_LINE_ITEMS_BY_SUPPLIER_QUERY, GET_VIPPS_STATUS_QUERY, KUSTOM_RETURN_QUERY_KEYS, SDK_VERSION, UPDATE_CHECKOUT_MUTATION, UPDATE_ITEM_MUTATION, UPDATE_SHIPPINGS_BY_SUPPLIER_MUTATION, VIPPS_BUTTON_SCRIPT, VIPPS_BUTTON_TAG, Vio, VioApi, VioApiError, __decorateClass, addItem, adyenReturnOutcome, adyenReturnParams, autoSlotDisplayName, autoSlotId, checkApplePayAvailability, createCart, createCheckout, createCommerceClient, createPaymentStripe, createPaymentVipps, createVioApi, deleteItem, displayPrice, ensureVippsButton, executeCartGraphQL, formatPrice, getAvailablePaymentMethods, getCart, getCartGraphQLOptions, getCheckout, getCustomerSessionId, getGlobalCurrency, getLineItemsBySupplier, getVippsStatus, kustomReturnOutcome, lineOrigins, nexiReturnPaymentId, normalizeOrigin, normalizeScope, primaryImageUrl, purchaseFromPaymentSuccess, readNexiPending, resolveCampaignId, resolveContent, resolveSlotCampaign, sameOrigin, showApplePaySheet, updateCheckout, updateItem, updateShippingsBySupplier, vippsButtonAttributes };
|
|
7168
|
+
//# sourceMappingURL=chunk-AMDI7KH3.js.map
|
|
7169
|
+
//# sourceMappingURL=chunk-AMDI7KH3.js.map
|