@vio-live/web-sdk 0.32.0 → 0.35.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.
@@ -841,6 +841,17 @@ var ChannelProduct = class {
841
841
  }
842
842
  }
843
843
  };
844
+ var FILLER_SHIPPING_DESCRIPTIONS = Object.freeze(["by outshifter", "default shipping method"]);
845
+ function shippingDescriptionOf(value, repeats = []) {
846
+ const text3 = typeof value === "string" ? value.trim() : "";
847
+ if (!text3) return "";
848
+ const key = text3.toLowerCase();
849
+ if (FILLER_SHIPPING_DESCRIPTIONS.includes(key)) return "";
850
+ for (const r of repeats) {
851
+ if (typeof r === "string" && r.trim() && r.trim().toLowerCase() === key) return "";
852
+ }
853
+ return text3;
854
+ }
844
855
  function shippingRatesFor(productShipping, country) {
845
856
  if (!Array.isArray(productShipping)) return [];
846
857
  const want = String(country || "").trim().toUpperCase();
@@ -851,13 +862,15 @@ function shippingRatesFor(productShipping, country) {
851
862
  const price = row?.price;
852
863
  const amount = typeof price?.amount_incl_taxes === "number" ? price.amount_incl_taxes : price?.amount;
853
864
  if (typeof amount !== "number" || !Number.isFinite(amount) || amount < 0) continue;
865
+ const name = String(s.name ?? "");
866
+ const description = shippingDescriptionOf(s.description, [name]);
854
867
  out.push({
855
868
  id: String(s.id ?? ""),
856
- name: String(s.name ?? ""),
869
+ name,
857
870
  price: amount,
858
871
  currency: String(price?.currency_code ?? ""),
859
872
  default: s.default === true,
860
- description: typeof s.description === "string" ? s.description.trim() : "",
873
+ description,
861
874
  logoUrl: httpsUrlOrEmpty(s.logo_url ?? s.logoUrl)
862
875
  });
863
876
  }
@@ -1026,6 +1039,154 @@ async function currentPrivacyMode() {
1026
1039
  }
1027
1040
  }
1028
1041
 
1042
+ // src/core/detail-layout.ts
1043
+ var DETAIL_SECTION_IDS = [
1044
+ "brand",
1045
+ "title",
1046
+ "stock",
1047
+ "price",
1048
+ "options",
1049
+ "purchase",
1050
+ "highlights",
1051
+ "description",
1052
+ "shippingReturns"
1053
+ ];
1054
+ var ESSENTIAL_DETAIL_SECTIONS = ["title", "price", "options", "purchase"];
1055
+ var LAYOUT_TOKEN = /^[A-Za-z0-9._:-]{1,64}$/;
1056
+ function layoutToken(value) {
1057
+ if (typeof value !== "string") return void 0;
1058
+ const v = value.trim();
1059
+ return LAYOUT_TOKEN.test(v) ? v : void 0;
1060
+ }
1061
+ var KNOWN = new Set(DETAIL_SECTION_IDS);
1062
+ function defaultDetailLayout() {
1063
+ return { id: "default", revision: "0", sections: [...DETAIL_SECTION_IDS], adjusted: [], source: "default" };
1064
+ }
1065
+ var warned = /* @__PURE__ */ new Set();
1066
+ function warnOnce(key, message) {
1067
+ if (warned.has(key)) return;
1068
+ warned.add(key);
1069
+ if (typeof console !== "undefined") console.warn("[Vio] detail layout ignored:", message);
1070
+ }
1071
+ function normalizeDetailLayout(raw) {
1072
+ if (raw === null || raw === void 0) return null;
1073
+ if (typeof raw !== "object" || Array.isArray(raw)) {
1074
+ warnOnce(`type:${typeof raw}`, "it must be an object");
1075
+ return null;
1076
+ }
1077
+ const r = raw;
1078
+ const id = layoutToken(r.id);
1079
+ if (!id) {
1080
+ warnOnce(`id:${String(r.id)}`, `invalid id ${JSON.stringify(r.id)} (\u2264 64 chars of A-Z a-z 0-9 . _ : -)`);
1081
+ return null;
1082
+ }
1083
+ if (!Array.isArray(r.sections)) {
1084
+ warnOnce(`sections:${id}`, `layout ${id}: sections must be an array of section ids`);
1085
+ return null;
1086
+ }
1087
+ const out = {
1088
+ id,
1089
+ revision: layoutToken(r.revision) ?? "",
1090
+ sections: r.sections.map((s) => typeof s === "string" ? s.trim() : String(s))
1091
+ };
1092
+ const experimentId = layoutToken(r.experimentId);
1093
+ if (experimentId) out.experimentId = experimentId;
1094
+ const variantId = layoutToken(r.variantId);
1095
+ if (variantId) out.variantId = variantId;
1096
+ return out;
1097
+ }
1098
+ function effectiveDetailSections(requested) {
1099
+ const sections = [];
1100
+ const adjusted = [];
1101
+ for (const raw of requested) {
1102
+ const s = String(raw).slice(0, 64);
1103
+ if (!KNOWN.has(s)) {
1104
+ adjusted.push({ section: s, reason: "unknown" });
1105
+ continue;
1106
+ }
1107
+ if (sections.includes(s)) {
1108
+ adjusted.push({ section: s, reason: "duplicate" });
1109
+ continue;
1110
+ }
1111
+ sections.push(s);
1112
+ }
1113
+ for (const essential of ESSENTIAL_DETAIL_SECTIONS) {
1114
+ if (!sections.includes(essential)) {
1115
+ sections.push(essential);
1116
+ adjusted.push({ section: essential, reason: "added-essential" });
1117
+ }
1118
+ }
1119
+ return { sections, adjusted };
1120
+ }
1121
+ function resolveDetailLayout(elementLayout, globalLayout = currentGlobalDetailLayout()) {
1122
+ const own = normalizeDetailLayout(elementLayout);
1123
+ const page = own ? null : normalizeDetailLayout(globalLayout);
1124
+ const chosen = own ?? page;
1125
+ if (!chosen) return defaultDetailLayout();
1126
+ const { sections, adjusted } = effectiveDetailSections(chosen.sections);
1127
+ return {
1128
+ id: chosen.id,
1129
+ revision: chosen.revision,
1130
+ ...chosen.experimentId ? { experimentId: chosen.experimentId } : {},
1131
+ ...chosen.variantId ? { variantId: chosen.variantId } : {},
1132
+ sections,
1133
+ adjusted,
1134
+ source: own ? "element" : "global"
1135
+ };
1136
+ }
1137
+ function copyAppliedLayout(applied) {
1138
+ return { ...applied, sections: [...applied.sections], adjusted: applied.adjusted.map((a) => ({ ...a })) };
1139
+ }
1140
+ function layoutSignature(applied) {
1141
+ return [applied.source, applied.id, applied.revision, applied.experimentId ?? "", applied.variantId ?? "", applied.sections.join(",")].join("|");
1142
+ }
1143
+ function layoutOriginOf(applied) {
1144
+ if (!applied || applied.source === "default") return {};
1145
+ return {
1146
+ layoutId: applied.id,
1147
+ ...applied.revision ? { layoutRevision: applied.revision } : {},
1148
+ ...applied.experimentId ? { experimentId: applied.experimentId } : {},
1149
+ ...applied.variantId ? { variantId: applied.variantId } : {}
1150
+ };
1151
+ }
1152
+ function normalizeLayoutOrigin(origin) {
1153
+ if (!origin || typeof origin !== "object") return {};
1154
+ const o = origin;
1155
+ const layoutId = layoutToken(o.layoutId);
1156
+ if (!layoutId) return {};
1157
+ const out = { layoutId };
1158
+ const layoutRevision = layoutToken(o.layoutRevision);
1159
+ if (layoutRevision) out.layoutRevision = layoutRevision;
1160
+ const experimentId = layoutToken(o.experimentId);
1161
+ if (experimentId) out.experimentId = experimentId;
1162
+ const variantId = layoutToken(o.variantId);
1163
+ if (variantId) out.variantId = variantId;
1164
+ return out;
1165
+ }
1166
+ function store() {
1167
+ const g = globalThis;
1168
+ return g.__VIO_DETAIL_LAYOUT__ ??= { layout: null, listeners: /* @__PURE__ */ new Set() };
1169
+ }
1170
+ function currentGlobalDetailLayout() {
1171
+ return store().layout;
1172
+ }
1173
+ function setGlobalDetailLayout(layout) {
1174
+ const s = store();
1175
+ s.layout = layout ?? null;
1176
+ normalizeDetailLayout(s.layout);
1177
+ for (const listener of [...s.listeners]) {
1178
+ try {
1179
+ listener();
1180
+ } catch {
1181
+ }
1182
+ }
1183
+ }
1184
+ function onGlobalDetailLayoutChange(listener) {
1185
+ const s = store();
1186
+ s.listeners.add(listener);
1187
+ return () => s.listeners.delete(listener);
1188
+ }
1189
+
1029
1190
  // src/core/buyer-ref.ts
1030
1191
  var BUYER_REF_MAX_LENGTH = 128;
1031
1192
  var BUYER_REF_PATTERN = /^[A-Za-z0-9._~-]+$/;
@@ -1044,18 +1205,18 @@ var FEATURE_TOKENS = {
1044
1205
  };
1045
1206
  var UNKNOWN_PATTERN = /cannot query field|unknown field|unknown argument|unknown type|is not defined by type|not defined by type|field "[^"]+" is not defined/i;
1046
1207
  var refused = /* @__PURE__ */ new Set();
1047
- var warned = /* @__PURE__ */ new Set();
1208
+ var warned2 = /* @__PURE__ */ new Set();
1048
1209
  function gatewayHas(feature) {
1049
1210
  return !refused.has(feature);
1050
1211
  }
1051
1212
  function resetGatewayFeatures(feature) {
1052
1213
  if (feature) {
1053
1214
  refused.delete(feature);
1054
- warned.delete(feature);
1215
+ warned2.delete(feature);
1055
1216
  return;
1056
1217
  }
1057
1218
  refused.clear();
1058
- warned.clear();
1219
+ warned2.clear();
1059
1220
  }
1060
1221
  function messagesOf(err) {
1061
1222
  if (!err || typeof err !== "object") return [];
@@ -1084,8 +1245,8 @@ async function withGatewayFeature(feature, request) {
1084
1245
  } catch (err) {
1085
1246
  if (!isUnknownFeatureError(err, feature)) throw err;
1086
1247
  refused.add(feature);
1087
- if (!warned.has(feature) && typeof console !== "undefined") {
1088
- warned.add(feature);
1248
+ if (!warned2.has(feature) && typeof console !== "undefined") {
1249
+ warned2.add(feature);
1089
1250
  console.warn(
1090
1251
  `[Vio] the commerce gateway does not know "${FEATURE_TOKENS[feature][0]}" (${feature}) yet \u2014 sent without it on this page load:`,
1091
1252
  messagesOf(err)[0] ?? ""
@@ -2014,7 +2175,7 @@ query Config {
2014
2175
  }
2015
2176
  }
2016
2177
  `;
2017
- async function getCartGraphQLOptions(sponsorId) {
2178
+ async function getCartGraphQLOptions(sponsorId, ctx) {
2018
2179
  const facade = globalThis.__VIO_FACADE__;
2019
2180
  if (!Configuration.isInitialized) {
2020
2181
  await Configuration.whenReady();
@@ -2032,14 +2193,31 @@ async function getCartGraphQLOptions(sponsorId) {
2032
2193
  const anyCfg = cfg;
2033
2194
  let commerceKey = anyCfg.apiKey;
2034
2195
  let sponsorResolved = false;
2035
- if (sponsorId && typeof facade?.findSponsor === "function") {
2196
+ let channelId;
2197
+ let channelMissing = false;
2198
+ const askedChannel = ctx?.channelId === null || ctx?.channelId === void 0 ? "" : String(ctx.channelId).trim();
2199
+ if (sponsorId && typeof facade?.sponsorCommerce === "function") {
2200
+ const sponsor = facade.sponsorCommerce(sponsorId, ctx ?? {});
2201
+ if (sponsor?.commerce?.apiKey) {
2202
+ commerceKey = sponsor.commerce.apiKey;
2203
+ sponsorResolved = true;
2204
+ const ch = sponsor.commerce.channelId;
2205
+ if (ch !== null && ch !== void 0 && String(ch).trim()) channelId = String(ch).trim();
2206
+ } else if (askedChannel && facade.bootstrapCache) {
2207
+ channelMissing = true;
2208
+ }
2209
+ } else if (sponsorId && !askedChannel && typeof facade?.findSponsor === "function") {
2036
2210
  const sponsor = facade.findSponsor(sponsorId);
2037
2211
  if (sponsor?.commerce?.apiKey) {
2038
2212
  commerceKey = sponsor.commerce.apiKey;
2039
2213
  sponsorResolved = true;
2040
2214
  }
2041
2215
  }
2042
- if (sponsorId && !sponsorResolved && typeof console !== "undefined") {
2216
+ if (channelMissing && typeof console !== "undefined") {
2217
+ console.warn(
2218
+ `[Vio] channel ${askedChannel} of sponsor ${sponsorId} is no longer in the bootstrap \u2014 not calling it with another channel's key`
2219
+ );
2220
+ } else if (sponsorId && !sponsorResolved && typeof console !== "undefined") {
2043
2221
  console.warn(
2044
2222
  `[Vio] no commerce key for sponsor ${sponsorId} \u2014 falling back to the platform apiKey, which the commerce API will reject. Is the sponsor configured, and did Vio.init run before this call?`
2045
2223
  );
@@ -2048,7 +2226,9 @@ async function getCartGraphQLOptions(sponsorId) {
2048
2226
  endpoint: anyCfg.graphQLBase || "https://graph-ql-dev.vio.live",
2049
2227
  apiKey: anyCfg.apiKey,
2050
2228
  commerceKey,
2051
- sponsorResolved
2229
+ sponsorResolved,
2230
+ ...channelId ? { channelId } : {},
2231
+ ...channelMissing ? { channelMissing } : {}
2052
2232
  };
2053
2233
  }
2054
2234
  var CUSTOMER_SESSION_KEY = "vio.customer_session_id.v1";
@@ -2311,7 +2491,18 @@ async function getVippsStatus(variables, options) {
2311
2491
  }
2312
2492
 
2313
2493
  // src/core/cart/cart-manager.ts
2314
- var STORAGE_KEY = "vio.cart.v1";
2494
+ var CART_STORAGE_PREFIX = "vio.cart.v1";
2495
+ function cartStorageKey(scope) {
2496
+ return scope ? `${CART_STORAGE_PREFIX}:${scope}` : CART_STORAGE_PREFIX;
2497
+ }
2498
+ function cartScopeOf(environment, apiKey) {
2499
+ let h = 2166136261;
2500
+ for (let i = 0; i < apiKey.length; i++) {
2501
+ h ^= apiKey.charCodeAt(i);
2502
+ h = Math.imul(h, 16777619) >>> 0;
2503
+ }
2504
+ return `${environment || "production"}:${h.toString(16).padStart(8, "0")}`;
2505
+ }
2315
2506
  function finalTarget(sync) {
2316
2507
  return sync.steps.reduce((quantity, step) => quantity + step.delta, sync.sent);
2317
2508
  }
@@ -2339,6 +2530,17 @@ function queueStep(sync, target, origin) {
2339
2530
  else sync.steps.push({ delta: -left });
2340
2531
  }
2341
2532
  }
2533
+ function cartCampaignOf(cart) {
2534
+ const items = cart?.items ?? [];
2535
+ for (let i = items.length - 1; i >= 0; i--) {
2536
+ const origins = items[i]?.origins ?? [];
2537
+ for (let j = origins.length - 1; j >= 0; j--) {
2538
+ const id = Number(origins[j]?.campaignId);
2539
+ if (Number.isInteger(id) && id > 0) return id;
2540
+ }
2541
+ }
2542
+ return void 0;
2543
+ }
2342
2544
  function lineId() {
2343
2545
  return "li_" + Math.random().toString(36).slice(2, 10) + Date.now().toString(36);
2344
2546
  }
@@ -2349,7 +2551,7 @@ function normId(val) {
2349
2551
  return String(val).trim();
2350
2552
  }
2351
2553
  var CartManager = class extends EventTarget {
2352
- constructor() {
2554
+ constructor(scope = "") {
2353
2555
  super();
2354
2556
  /** In-flight CreateCart per sponsor — two quick adds must share ONE backend
2355
2557
  * cart (two CreateCarts split the items and one silently disappears). */
@@ -2373,6 +2575,21 @@ var CartManager = class extends EventTarget {
2373
2575
  * in parallel; each response re-applied the whole backend cart, so an
2374
2576
  * older one landing after a newer optimistic change rolled the line back.) */
2375
2577
  this.qtySyncs = /* @__PURE__ */ new Map();
2578
+ /**
2579
+ * Bumped per sponsor each time its backend cart is dropped because its
2580
+ * channel went away: the lines as they stood then (`carryOver`) go to the
2581
+ * new backend cart, so an add whose local units were already counted in
2582
+ * them must not send its own AddItem as well (see `addManual`).
2583
+ */
2584
+ this.rebuildEpoch = /* @__PURE__ */ new Map();
2585
+ /** The lines to give the next backend cart of a sponsor, after its channel went away. */
2586
+ this.carryOver = /* @__PURE__ */ new Map();
2587
+ /**
2588
+ * Whether this is the page's connection now. A manager left behind by a
2589
+ * re-init (another key) still finishes its requests and keeps its own
2590
+ * carts, but says nothing to the page: no `vio:cart-*` events.
2591
+ */
2592
+ this.active = true;
2376
2593
  /** Quantity intents for lines whose backend id hasn't arrived yet — replayed
2377
2594
  * when the AddItem response reconciles (keyed by product::variant). */
2378
2595
  this.pendingQtyIntents = /* @__PURE__ */ new Map();
@@ -2386,7 +2603,7 @@ var CartManager = class extends EventTarget {
2386
2603
  * that changed (`source: 'storage'`); nothing when nothing did.
2387
2604
  */
2388
2605
  this.onStorage = (e) => {
2389
- if (e.key !== null && e.key !== STORAGE_KEY) return;
2606
+ if (e.key !== null && e.key !== this.storageKey) return;
2390
2607
  const stored = this.loadFromStorage();
2391
2608
  const changed = [];
2392
2609
  for (const sponsorId of /* @__PURE__ */ new Set([...this.cartsBySponsor.keys(), ...stored.keys()])) {
@@ -2402,11 +2619,28 @@ var CartManager = class extends EventTarget {
2402
2619
  this.emit();
2403
2620
  for (const sponsorId of changed) this.emitCartUpdated(sponsorId, "storage");
2404
2621
  };
2622
+ this.storageKey = cartStorageKey(scope);
2405
2623
  this.cartsBySponsor = this.loadFromStorage();
2624
+ if (scope && this.cartsBySponsor.size === 0) {
2625
+ const legacy = this.loadFromStorage(CART_STORAGE_PREFIX);
2626
+ if (legacy.size > 0) {
2627
+ this.cartsBySponsor = legacy;
2628
+ this.persist();
2629
+ storageRemove("local", CART_STORAGE_PREFIX);
2630
+ }
2631
+ }
2406
2632
  if (typeof window !== "undefined" && typeof window.addEventListener === "function") {
2407
2633
  window.addEventListener("storage", this.onStorage);
2408
2634
  }
2409
2635
  }
2636
+ /** The page's connection is (or is no longer) this one; see `active`. */
2637
+ setActive(active) {
2638
+ this.active = active;
2639
+ }
2640
+ /** Tell the listeners what this connection's carts hold now (after the page switched to it). */
2641
+ announce() {
2642
+ this.emit();
2643
+ }
2410
2644
  nextSeq(sponsorId) {
2411
2645
  const n = (this.mutationSeq.get(sponsorId) ?? 0) + 1;
2412
2646
  this.mutationSeq.set(sponsorId, n);
@@ -2500,15 +2734,118 @@ var CartManager = class extends EventTarget {
2500
2734
  emitError(message, err) {
2501
2735
  if (typeof console !== "undefined") console.warn("[CartManager]", message, err ?? "");
2502
2736
  this.dispatchEvent(new CustomEvent("error", { detail: { message } }));
2503
- if (typeof document !== "undefined") {
2737
+ if (this.active && typeof document !== "undefined") {
2504
2738
  document.dispatchEvent(
2505
2739
  new CustomEvent("vio:cart-error", { bubbles: true, detail: { message } })
2506
2740
  );
2507
2741
  }
2508
2742
  }
2509
2743
  // MARK: - Backend cart sync (Vio Commerce GraphQL)
2510
- /** Ensure the sponsor cart exists server-side; returns its backend cart id. */
2511
- async ensureCartId(sponsorId, currency, shippingCountry) {
2744
+ /**
2745
+ * The options of a sponsor's backend cart: the key of the channel it was
2746
+ * created on (`channelMissing` when the bootstrap no longer lists it). A
2747
+ * cart from before channels were remembered keeps the sponsor's first
2748
+ * block, as when it was created, and remembers that channel from now on.
2749
+ */
2750
+ async cartOptions(sponsorId) {
2751
+ const cart = this.cartsBySponsor.get(sponsorId);
2752
+ if (cart?.cartId && cart.channelId) {
2753
+ return getCartGraphQLOptions(sponsorId, { channelId: cart.channelId });
2754
+ }
2755
+ if (!cart?.cartId) return getCartGraphQLOptions(sponsorId);
2756
+ const opts = await getCartGraphQLOptions(sponsorId, { campaignId: null });
2757
+ const now = this.cartsBySponsor.get(sponsorId);
2758
+ if (now?.cartId && !now.channelId && opts.sponsorResolved && opts.channelId) {
2759
+ now.channelId = opts.channelId;
2760
+ this.persist();
2761
+ }
2762
+ return opts;
2763
+ }
2764
+ /**
2765
+ * The backend cart to call for a sponsor, with the key of its channel. When
2766
+ * the bootstrap no longer lists that channel the stale cart is dropped and a
2767
+ * new one is created on the sponsor's current channel with the same lines
2768
+ * (`dropStaleBackendCart` + `ensureCartId`) — never a call with a key of
2769
+ * another channel. `undefined` when there is no backend cart (or none could
2770
+ * be made).
2771
+ */
2772
+ async liveCart(sponsorId) {
2773
+ const cart = this.cartsBySponsor.get(sponsorId);
2774
+ const cartId = cart?.cartId;
2775
+ if (!cart || !cartId) return void 0;
2776
+ const opts = await this.cartOptions(sponsorId);
2777
+ if (!opts.channelMissing) {
2778
+ const current = this.cartsBySponsor.get(sponsorId)?.cartId;
2779
+ if (current && current !== cartId) return { cartId: current, opts: await this.cartOptions(sponsorId) };
2780
+ return { cartId, opts };
2781
+ }
2782
+ const now = this.cartsBySponsor.get(sponsorId);
2783
+ if (now?.cartId === cartId) this.dropStaleBackendCart(sponsorId);
2784
+ const rebuilt = await this.ensureCartId(sponsorId, now?.currency || void 0, now?.shippingCountry || void 0);
2785
+ if (!rebuilt) return void 0;
2786
+ return { cartId: rebuilt, opts: await this.cartOptions(sponsorId) };
2787
+ }
2788
+ /**
2789
+ * The channel of a sponsor's cart is gone from the bootstrap (the campaign
2790
+ * moved, a refresh after hours): forget the backend cart and its line ids,
2791
+ * keep the lines, and remember them as they stand now for the next backend
2792
+ * cart (`carryOver`). Queued quantity syncs of the old cart are dropped —
2793
+ * their quantities are in the lines carried.
2794
+ */
2795
+ dropStaleBackendCart(sponsorId) {
2796
+ const cart = this.cartsBySponsor.get(sponsorId);
2797
+ if (!cart?.cartId) return;
2798
+ const carried = cart.items.filter((item) => item.quantity > 0).map((item) => ({ productId: item.productId, variantId: item.variantId, origins: lineOrigins(item) }));
2799
+ delete cart.cartId;
2800
+ delete cart.channelId;
2801
+ for (const item of cart.items) delete item.cartItemId;
2802
+ for (const key of [...this.qtySyncs.keys()]) {
2803
+ if (key.startsWith(`${sponsorId}:`)) this.qtySyncs.delete(key);
2804
+ }
2805
+ this.carryOver.set(sponsorId, carried);
2806
+ this.rebuildEpoch.set(sponsorId, (this.rebuildEpoch.get(sponsorId) ?? 0) + 1);
2807
+ this.persist();
2808
+ }
2809
+ /** Give a new backend cart the lines of the one whose channel went away — each unit with its own origin. */
2810
+ async replayCarriedLines(sponsorId, cartId, carried, opts) {
2811
+ let last;
2812
+ const seq = this.nextSeq(sponsorId);
2813
+ try {
2814
+ const privacyMode = await currentPrivacyMode();
2815
+ for (const line of carried) {
2816
+ for (const origin of line.origins) {
2817
+ if (!(origin.quantity > 0)) continue;
2818
+ const { quantity, ...place } = origin;
2819
+ const wire = Object.keys(place).length ? lineOriginWire(normalizeOrigin(place), { page: currentOriginPage(), privacyMode }) : void 0;
2820
+ last = await addItem(
2821
+ cartId,
2822
+ [
2823
+ {
2824
+ product_id: Number(line.productId || 0),
2825
+ quantity,
2826
+ variant_id: line.variantId != null ? Number(line.variantId) : null,
2827
+ ...wire ? { origin: wire } : {}
2828
+ }
2829
+ ],
2830
+ opts
2831
+ );
2832
+ }
2833
+ }
2834
+ } catch (err) {
2835
+ this.emitError("moving the cart to the current channel failed \u2014 some lines may not be in it", err);
2836
+ }
2837
+ const res = last;
2838
+ if (res && Array.isArray(res.line_items)) this.applyBackendCart(sponsorId, res, seq);
2839
+ }
2840
+ /**
2841
+ * Ensure the sponsor cart exists server-side; returns its backend cart id.
2842
+ * A new backend cart is created on the channel of the campaign in context —
2843
+ * `ctx.campaignId` (the add's origin), else the campaign the cart's lines
2844
+ * came from, else `Vio.init`'s — and remembers it (`channelId`). An existing
2845
+ * one whose channel the bootstrap no longer lists is created again on the
2846
+ * current channel, with its lines (`liveCart`).
2847
+ */
2848
+ async ensureCartId(sponsorId, currency, shippingCountry, ctx = {}) {
2512
2849
  const activeCurrency = currency || getGlobalCurrency();
2513
2850
  const activeCountry = shippingCountry || getGlobalCountryCode();
2514
2851
  let cart = this.cartsBySponsor.get(sponsorId);
@@ -2518,11 +2855,15 @@ var CartManager = class extends EventTarget {
2518
2855
  } else {
2519
2856
  cart.shippingCountry = activeCountry;
2520
2857
  }
2521
- if (cart.cartId) return cart.cartId;
2858
+ if (cart.cartId) {
2859
+ if (!cart.channelId) return cart.cartId;
2860
+ return (await this.liveCart(sponsorId))?.cartId;
2861
+ }
2522
2862
  const inFlight = this.cartIdPromises.get(sponsorId);
2523
2863
  if (inFlight) return inFlight;
2524
2864
  const creation = (async () => {
2525
- const opts = await getCartGraphQLOptions(sponsorId);
2865
+ const campaignId = ctx.campaignId ?? cartCampaignOf(cart);
2866
+ const opts = await getCartGraphQLOptions(sponsorId, campaignId !== void 0 ? { campaignId } : void 0);
2526
2867
  if (!opts.sponsorResolved) {
2527
2868
  this.emitError(`no commerce key resolved for sponsor ${sponsorId} \u2014 cart not synced`);
2528
2869
  return void 0;
@@ -2537,6 +2878,8 @@ var CartManager = class extends EventTarget {
2537
2878
  );
2538
2879
  if (backendCart?.cart_id) {
2539
2880
  cart.cartId = backendCart.cart_id;
2881
+ if (opts.channelId) cart.channelId = opts.channelId;
2882
+ else delete cart.channelId;
2540
2883
  if (Array.isArray(backendCart.line_items) && backendCart.line_items.length > 0) {
2541
2884
  this.updateFromBackendCart(sponsorId, backendCart);
2542
2885
  } else {
@@ -2550,6 +2893,11 @@ var CartManager = class extends EventTarget {
2550
2893
  this.persist();
2551
2894
  this.emit();
2552
2895
  }
2896
+ const carried = this.carryOver.get(sponsorId);
2897
+ if (carried) {
2898
+ this.carryOver.delete(sponsorId);
2899
+ if (carried.length > 0) await this.replayCarriedLines(sponsorId, backendCart.cart_id, carried, opts);
2900
+ }
2553
2901
  }
2554
2902
  } catch (err) {
2555
2903
  this.emitError("createCart failed", err);
@@ -2642,7 +2990,7 @@ var CartManager = class extends EventTarget {
2642
2990
  this.emit();
2643
2991
  const syncedCartId = cart.cartId;
2644
2992
  if (syncedCartId) {
2645
- for (const id of removedBeforeSync) this.syncQuantity(sponsorId, syncedCartId, id, 0);
2993
+ for (const id of removedBeforeSync) this.syncQuantity(sponsorId, syncedCartId, id, 0, void 0, cart.channelId);
2646
2994
  }
2647
2995
  if (this.pendingQtyIntents.size > 0) {
2648
2996
  const currentCart = this.cartsBySponsor.get(sponsorId);
@@ -2734,13 +3082,17 @@ var CartManager = class extends EventTarget {
2734
3082
  this.emit();
2735
3083
  const addedItem = existing ?? cart.items[cart.items.length - 1];
2736
3084
  const addedLineId = addedItem.id;
3085
+ const epochAtAdd = this.rebuildEpoch.get(opts.sponsorId) ?? 0;
2737
3086
  const seq = this.nextSeq(opts.sponsorId);
2738
3087
  let settling;
2739
3088
  const mutation = (async () => {
2740
3089
  try {
2741
- const cartId = await this.ensureCartId(opts.sponsorId, opts.currency || void 0);
3090
+ const cartId = await this.ensureCartId(opts.sponsorId, opts.currency || void 0, void 0, {
3091
+ campaignId: opts.origin?.campaignId
3092
+ });
3093
+ if ((this.rebuildEpoch.get(opts.sponsorId) ?? 0) !== epochAtAdd) return;
2742
3094
  if (cartId) {
2743
- const gqlOpts = await getCartGraphQLOptions(opts.sponsorId);
3095
+ const gqlOpts = await this.cartOptions(opts.sponsorId);
2744
3096
  const origin = opts.origin ? lineOriginWire(normalizeOrigin(opts.origin), {
2745
3097
  page: currentOriginPage(),
2746
3098
  privacyMode: await currentPrivacyMode()
@@ -2809,7 +3161,7 @@ var CartManager = class extends EventTarget {
2809
3161
  const addedOrigin = increase ? latestOrigin(item) : void 0;
2810
3162
  if (target <= 0) {
2811
3163
  cart.items = cart.items.filter((i) => i !== item);
2812
- if (typeof document !== "undefined") {
3164
+ if (this.active && typeof document !== "undefined") {
2813
3165
  document.dispatchEvent(
2814
3166
  new CustomEvent("vio:removed-from-cart", {
2815
3167
  bubbles: true,
@@ -2825,7 +3177,9 @@ var CartManager = class extends EventTarget {
2825
3177
  // Attribution of the removed units: their most recent origin.
2826
3178
  slot: removedOrigin?.slot,
2827
3179
  campaignId: removedOrigin?.campaignId,
2828
- contentId: removedOrigin?.contentId
3180
+ contentId: removedOrigin?.contentId,
3181
+ // The detail layout of those units (0.34), when they came from one.
3182
+ ...removedOrigin?.layoutId ? { layout: normalizeLayoutOrigin(removedOrigin) } : {}
2829
3183
  }
2830
3184
  })
2831
3185
  );
@@ -2836,12 +3190,17 @@ var CartManager = class extends EventTarget {
2836
3190
  if (cart.items.length === 0) this.cartsBySponsor.delete(sponsorId);
2837
3191
  this.persist();
2838
3192
  this.emit();
2839
- if (!cart.cartId) return;
3193
+ if (!cart.cartId) {
3194
+ if (this.cartIdPromises.has(sponsorId)) {
3195
+ this.pendingQtyIntents.set(this.intentKey(item.productId, item.variantId), target);
3196
+ }
3197
+ return;
3198
+ }
2840
3199
  if (!targetItemId) {
2841
3200
  this.pendingQtyIntents.set(this.intentKey(item.productId, item.variantId), target);
2842
3201
  return;
2843
3202
  }
2844
- this.syncQuantity(sponsorId, cart.cartId, targetItemId, target, addedOrigin);
3203
+ this.syncQuantity(sponsorId, cart.cartId, targetItemId, target, addedOrigin, cart.channelId);
2845
3204
  }
2846
3205
  /** Send a line's target quantity to the backend — ONE request in flight per
2847
3206
  * line. Targets that arrive while one is in flight queue behind it as
@@ -2853,7 +3212,7 @@ var CartManager = class extends EventTarget {
2853
3212
  * optimistic quantity through any backend response (see
2854
3213
  * `updateFromBackendCart`); a rejection drops what was queued and
2855
3214
  * reconciles the cart from the server. */
2856
- syncQuantity(sponsorId, backendCartId, cartItemId, target, origin) {
3215
+ syncQuantity(sponsorId, backendCartId, cartItemId, target, origin, channelId) {
2857
3216
  const key = this.qtySyncKey(sponsorId, cartItemId);
2858
3217
  const active = this.qtySyncs.get(key);
2859
3218
  if (active) {
@@ -2868,9 +3227,19 @@ var CartManager = class extends EventTarget {
2868
3227
  let nextOrigin = origin;
2869
3228
  let removalRetried = false;
2870
3229
  while (next !== void 0) {
3230
+ let gqlOpts;
3231
+ if (this.cartsBySponsor.get(sponsorId)?.cartId) {
3232
+ const live = await this.liveCart(sponsorId);
3233
+ if (!live || live.cartId !== backendCartId) {
3234
+ if (this.qtySyncs.get(key) === sync) this.qtySyncs.delete(key);
3235
+ break;
3236
+ }
3237
+ gqlOpts = live.opts;
3238
+ } else {
3239
+ gqlOpts = await getCartGraphQLOptions(sponsorId, channelId ? { channelId, retired: true } : void 0);
3240
+ }
2871
3241
  const seq = this.nextSeq(sponsorId);
2872
3242
  try {
2873
- const gqlOpts = await getCartGraphQLOptions(sponsorId);
2874
3243
  const originWire = next > 0 && nextOrigin ? lineOriginWire(nextOrigin, { page: currentOriginPage(), privacyMode: await currentPrivacyMode() }) : void 0;
2875
3244
  const resCart = next <= 0 ? await deleteItem(backendCartId, cartItemId, gqlOpts) : originWire ? await updateItem(backendCartId, cartItemId, null, next, gqlOpts, { origin: originWire }) : await updateItem(backendCartId, cartItemId, null, next, gqlOpts);
2876
3245
  const step = sync.steps.shift();
@@ -2930,10 +3299,9 @@ var CartManager = class extends EventTarget {
2930
3299
  }
2931
3300
  /** Refresh the local cart from the backend (source of truth). */
2932
3301
  async fetchCartFromBackend(sponsorId) {
2933
- const cart = this.cartsBySponsor.get(sponsorId);
2934
- if (!cart?.cartId) return null;
2935
- const gqlOpts = await getCartGraphQLOptions(sponsorId);
2936
- const resCart = await getCart(cart.cartId, gqlOpts);
3302
+ const live = await this.liveCart(sponsorId);
3303
+ if (!live) return null;
3304
+ const resCart = await getCart(live.cartId, live.opts);
2937
3305
  if (resCart) this.updateFromBackendCart(sponsorId, resCart);
2938
3306
  return resCart;
2939
3307
  }
@@ -2957,8 +3325,9 @@ var CartManager = class extends EventTarget {
2957
3325
  if (!cart?.cartId) return cart;
2958
3326
  let resCart;
2959
3327
  try {
2960
- const gqlOpts = await getCartGraphQLOptions(sponsorId);
2961
- resCart = await getCart(cart.cartId, gqlOpts);
3328
+ const live = await this.liveCart(sponsorId);
3329
+ if (!live) return this.cartsBySponsor.get(sponsorId);
3330
+ resCart = await getCart(live.cartId, live.opts);
2962
3331
  } catch (err) {
2963
3332
  if (typeof console !== "undefined") {
2964
3333
  console.warn("[CartManager] GetCart before the checkout failed \u2014 the local cart stands", err);
@@ -2977,7 +3346,7 @@ var CartManager = class extends EventTarget {
2977
3346
  }
2978
3347
  /** `vio:cart-updated` on `document` — see `CartUpdatedDetail`. */
2979
3348
  emitCartUpdated(sponsorId, source) {
2980
- if (typeof document === "undefined") return;
3349
+ if (!this.active || typeof document === "undefined") return;
2981
3350
  const cart = this.cartsBySponsor.get(sponsorId);
2982
3351
  const items = (cart?.items ?? []).map((item) => ({ ...item }));
2983
3352
  const detail = {
@@ -2993,17 +3362,15 @@ var CartManager = class extends EventTarget {
2993
3362
  }
2994
3363
  /** Line items grouped by supplier (shipping is per-supplier server-side). */
2995
3364
  async fetchLineItemsBySupplier(sponsorId) {
2996
- const cart = this.cartsBySponsor.get(sponsorId);
2997
- if (!cart?.cartId) return null;
2998
- const gqlOpts = await getCartGraphQLOptions(sponsorId);
2999
- return getLineItemsBySupplier(cart.cartId, gqlOpts);
3365
+ const live = await this.liveCart(sponsorId);
3366
+ if (!live) return null;
3367
+ return getLineItemsBySupplier(live.cartId, live.opts);
3000
3368
  }
3001
3369
  /** Set the chosen shipping option per supplier on the backend cart. */
3002
3370
  async updateShippingsBySupplier(sponsorId, data) {
3003
- const cart = this.cartsBySponsor.get(sponsorId);
3004
- if (!cart?.cartId) return null;
3005
- const gqlOpts = await getCartGraphQLOptions(sponsorId);
3006
- const resCart = await updateShippingsBySupplier(cart.cartId, data, gqlOpts);
3371
+ const live = await this.liveCart(sponsorId);
3372
+ if (!live) return null;
3373
+ const resCart = await updateShippingsBySupplier(live.cartId, data, live.opts);
3007
3374
  if (resCart && Array.isArray(resCart.line_items)) {
3008
3375
  this.updateFromBackendCart(sponsorId, resCart);
3009
3376
  }
@@ -3053,13 +3420,13 @@ var CartManager = class extends EventTarget {
3053
3420
  try {
3054
3421
  const obj = {};
3055
3422
  for (const [k, v] of this.cartsBySponsor) obj[String(k)] = v;
3056
- storageSet("local", STORAGE_KEY, JSON.stringify(obj), { memory: false });
3423
+ storageSet("local", this.storageKey, JSON.stringify(obj), { memory: false });
3057
3424
  } catch {
3058
3425
  }
3059
3426
  }
3060
- loadFromStorage() {
3427
+ loadFromStorage(key = this.storageKey) {
3061
3428
  try {
3062
- const raw = storageGet("local", STORAGE_KEY);
3429
+ const raw = storageGet("local", key);
3063
3430
  if (!raw) return /* @__PURE__ */ new Map();
3064
3431
  const obj = JSON.parse(raw);
3065
3432
  const map = /* @__PURE__ */ new Map();
@@ -3098,10 +3465,11 @@ function normalizeOrigin(origin) {
3098
3465
  if (typeof origin.contentTitle === "string" && origin.contentTitle.trim()) {
3099
3466
  out.contentTitle = origin.contentTitle.trim().slice(0, CONTENT_TITLE_MAX_LENGTH);
3100
3467
  }
3468
+ Object.assign(out, normalizeLayoutOrigin(origin));
3101
3469
  return out;
3102
3470
  }
3103
3471
  function sameOrigin(a, b) {
3104
- return a.campaignId === b.campaignId && a.slot === b.slot && a.contentId === b.contentId && a.componentKind === b.componentKind;
3472
+ return a.campaignId === b.campaignId && a.slot === b.slot && a.contentId === b.contentId && a.componentKind === b.componentKind && a.layoutId === b.layoutId && a.layoutRevision === b.layoutRevision && a.experimentId === b.experimentId && a.variantId === b.variantId;
3105
3473
  }
3106
3474
  function latestOrigin(line) {
3107
3475
  const origins = Array.isArray(line.origins) ? line.origins : [];
@@ -3210,6 +3578,56 @@ function findSponsorIn(boot, sponsorId) {
3210
3578
  }
3211
3579
  return void 0;
3212
3580
  }
3581
+ function channelIdOf(value) {
3582
+ if (value === null || value === void 0) return void 0;
3583
+ const s = String(value).trim();
3584
+ return s ? s : void 0;
3585
+ }
3586
+ function sells(s) {
3587
+ const commerce = s?.commerce;
3588
+ if (!commerce || typeof commerce !== "object") return false;
3589
+ const key = commerce.apiKey;
3590
+ return typeof key === "string" && key.trim() !== "";
3591
+ }
3592
+ function sellingBlocksIn(boot, sponsorId) {
3593
+ const id = toSponsorId(sponsorId);
3594
+ const out = [];
3595
+ if (id === void 0 || !boot || typeof boot !== "object") return out;
3596
+ const take = (s, campaignId) => {
3597
+ if (!s || typeof s !== "object" || Number(s.id) !== id || !sells(s)) return;
3598
+ out.push({ campaignId: toCampaignId(campaignId), sponsor: s });
3599
+ };
3600
+ try {
3601
+ const defaultId = boot.campaign?.id;
3602
+ take(boot.primarySponsor, defaultId);
3603
+ if (Array.isArray(boot.secondarySponsors)) for (const s of boot.secondarySponsors) take(s, defaultId);
3604
+ const campaigns = Array.isArray(boot.campaigns) ? boot.campaigns : [];
3605
+ for (const c of campaigns) {
3606
+ if (!c || typeof c !== "object") continue;
3607
+ take(c.primarySponsor, c.id);
3608
+ if (Array.isArray(c.secondarySponsors)) for (const s of c.secondarySponsors) take(s, c.id);
3609
+ }
3610
+ } catch {
3611
+ }
3612
+ return out;
3613
+ }
3614
+ function sponsorCommerceIn(boot, sponsorId, ctx = {}) {
3615
+ try {
3616
+ const blocks = sellingBlocksIn(boot, sponsorId);
3617
+ if (blocks.length === 0) return void 0;
3618
+ const channel = channelIdOf(ctx?.channelId);
3619
+ if (channel !== void 0) {
3620
+ return blocks.find((b) => channelIdOf(b.sponsor.commerce.channelId) === channel)?.sponsor;
3621
+ }
3622
+ for (const campaignId of campaignCandidates(ctx?.campaignId)) {
3623
+ const found = blocks.find((b) => b.campaignId === campaignId);
3624
+ if (found) return found.sponsor;
3625
+ }
3626
+ return blocks[0].sponsor;
3627
+ } catch {
3628
+ return void 0;
3629
+ }
3630
+ }
3213
3631
  function sponsorIdentityFrom(boot, sponsorId) {
3214
3632
  try {
3215
3633
  const id = toSponsorId(sponsorId);
@@ -3255,13 +3673,14 @@ function normalizeEntry(raw) {
3255
3673
  const checkoutId = typeof r.checkoutId === "string" ? r.checkoutId.trim() : "";
3256
3674
  const sponsorId = toSponsorId(r.sponsorId);
3257
3675
  if (!checkoutId || sponsorId === void 0) return null;
3258
- return { checkoutId, sponsorId, name: nameOf(r.name), logoUrl: urlOrNull(r.logoUrl) };
3676
+ const channelId = channelIdOf(r.channelId);
3677
+ return { checkoutId, sponsorId, name: nameOf(r.name), logoUrl: urlOrNull(r.logoUrl), ...channelId ? { channelId } : {} };
3259
3678
  }
3260
3679
  function readEntries() {
3261
- const store = storageRefusedWrite ? null : sessionStore();
3262
- if (store) {
3680
+ const store2 = storageRefusedWrite ? null : sessionStore();
3681
+ if (store2) {
3263
3682
  try {
3264
- const raw = store.getItem(CHECKOUT_SPONSOR_STORAGE_KEY);
3683
+ const raw = store2.getItem(CHECKOUT_SPONSOR_STORAGE_KEY);
3265
3684
  if (!raw) return [];
3266
3685
  const parsed = JSON.parse(raw);
3267
3686
  if (Array.isArray(parsed)) {
@@ -3274,10 +3693,10 @@ function readEntries() {
3274
3693
  }
3275
3694
  function writeEntries(entries) {
3276
3695
  inMemory = entries.slice();
3277
- const store = sessionStore();
3278
- if (!store) return;
3696
+ const store2 = sessionStore();
3697
+ if (!store2) return;
3279
3698
  try {
3280
- store.setItem(CHECKOUT_SPONSOR_STORAGE_KEY, JSON.stringify(entries));
3699
+ store2.setItem(CHECKOUT_SPONSOR_STORAGE_KEY, JSON.stringify(entries));
3281
3700
  } catch {
3282
3701
  storageRefusedWrite = true;
3283
3702
  }
@@ -3295,8 +3714,9 @@ function rememberCheckoutSponsor(checkoutId, sponsor) {
3295
3714
  name = previous.name;
3296
3715
  logoUrl = previous.logoUrl;
3297
3716
  }
3717
+ const channelId = channelIdOf(sponsor.channelId) ?? (previous && previous.sponsorId === sponsorId ? previous.channelId : void 0);
3298
3718
  const next = entries.filter((e) => e.checkoutId !== id);
3299
- next.push({ checkoutId: id, sponsorId, name, logoUrl });
3719
+ next.push({ checkoutId: id, sponsorId, name, logoUrl, ...channelId ? { channelId } : {} });
3300
3720
  writeEntries(next.slice(-CHECKOUT_SPONSOR_MEMORY_MAX));
3301
3721
  } catch {
3302
3722
  }
@@ -3535,7 +3955,7 @@ async function prepareApplePay(config) {
3535
3955
  }
3536
3956
 
3537
3957
  // src/core/version.ts
3538
- var SDK_VERSION = "0.32.0";
3958
+ var SDK_VERSION = "0.35.0";
3539
3959
 
3540
3960
  // src/core/checkout/payments/stripe-embedded.ts
3541
3961
  var CREATE_PAYMENT_INTENT_STRIPE_MUTATION = `
@@ -5104,8 +5524,8 @@ function resolveHttpsReturnUrl(overrideUrl, params = null) {
5104
5524
  }
5105
5525
  return url;
5106
5526
  }
5107
- async function executeKlarnaGraphQL(query, variables, sponsorId) {
5108
- return executeCartGraphQL(query, variables, await getCartGraphQLOptions(sponsorId));
5527
+ async function executeKlarnaGraphQL(query, variables, options) {
5528
+ return executeCartGraphQL(query, variables, options);
5109
5529
  }
5110
5530
  function usesStripe(methods) {
5111
5531
  if (!Array.isArray(methods)) return false;
@@ -5188,8 +5608,44 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
5188
5608
  return this.currentStateValue;
5189
5609
  }
5190
5610
  set state(next) {
5611
+ if (next?.checkoutId && next.sponsorId && !next.channelId) {
5612
+ const remembered = rememberedCheckoutSponsor(next.checkoutId);
5613
+ const channelId = (remembered?.sponsorId === next.sponsorId ? remembered.channelId : void 0) ?? this.cartManager.getCart(next.sponsorId)?.channelId;
5614
+ if (channelId) next = { ...next, channelId };
5615
+ }
5191
5616
  this.currentStateValue = next;
5192
- if (next?.checkoutId && next.sponsorId) this.rememberSponsorOfCheckout(next.checkoutId, next.sponsorId);
5617
+ if (next?.checkoutId && next.sponsorId) {
5618
+ this.rememberSponsorOfCheckout(next.checkoutId, next.sponsorId, next.channelId);
5619
+ }
5620
+ }
5621
+ /**
5622
+ * The options of a call for a sponsor's cart or checkout: the key of the
5623
+ * channel the checkout lives on — what this tab remembered for the
5624
+ * checkout (a return from a redirect, after the cart was cleared), the open
5625
+ * checkout's, else the cart's — even if a bootstrap refresh no longer lists
5626
+ * it (an open checkout keeps paying with its own key). Without any, the
5627
+ * sponsor's block as `getCartGraphQLOptions` resolves it.
5628
+ */
5629
+ async gqlOptions(sponsorId, checkoutId) {
5630
+ const channelId = this.channelOf(sponsorId, checkoutId);
5631
+ return getCartGraphQLOptions(sponsorId, channelId ? { channelId, retired: true } : void 0);
5632
+ }
5633
+ /** See `gqlOptions`. */
5634
+ channelOf(sponsorId, checkoutId) {
5635
+ try {
5636
+ if (!sponsorId) return void 0;
5637
+ const state = this.state?.sponsorId === sponsorId ? this.state : null;
5638
+ const id = checkoutId ?? state?.checkoutId;
5639
+ if (id) {
5640
+ const remembered = rememberedCheckoutSponsor(id);
5641
+ if (remembered?.channelId && remembered.sponsorId === sponsorId) return remembered.channelId;
5642
+ if (state?.checkoutId === id && state.channelId) return state.channelId;
5643
+ }
5644
+ const cart = this.cartManager.getCart(sponsorId);
5645
+ return cart?.cartId ? cart.channelId : void 0;
5646
+ } catch {
5647
+ return void 0;
5648
+ }
5193
5649
  }
5194
5650
  /** The open checkout's lines as a signature, to tell a real change from an echo. */
5195
5651
  cartSignature(sponsorId) {
@@ -5264,7 +5720,7 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
5264
5720
  throw new Error(`[CheckoutManager] Failed to obtain cart_id for sponsor ${sponsorId}`);
5265
5721
  }
5266
5722
  }
5267
- const opts = await getCartGraphQLOptions(sponsorId);
5723
+ const opts = await this.gqlOptions(sponsorId);
5268
5724
  const attribution = await this.attributionFor(sponsorId);
5269
5725
  const checkout = attribution ? await createCheckout(cartId, opts, { attribution }) : await createCheckout(cartId, opts);
5270
5726
  if (checkout?.id) {
@@ -5347,7 +5803,7 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
5347
5803
  /** Update the backend checkout (terms are re-asserted unless overridden; the attribution rides along unless the caller gives its own). */
5348
5804
  async updateCheckout(variables, sponsorId) {
5349
5805
  const spId = sponsorId ?? this.state?.sponsorId;
5350
- const opts = await getCartGraphQLOptions(spId);
5806
+ const opts = await this.gqlOptions(spId);
5351
5807
  const attribution = "attribution" in variables ? void 0 : await this.attributionFor(spId);
5352
5808
  const fullVars = {
5353
5809
  buyerAcceptsPurchaseConditions: true,
@@ -5363,7 +5819,7 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
5363
5819
  return updated;
5364
5820
  }
5365
5821
  async getCheckout(checkoutId, sponsorId) {
5366
- const opts = await getCartGraphQLOptions(sponsorId ?? this.state?.sponsorId);
5822
+ const opts = await this.gqlOptions(sponsorId ?? this.sponsorIdOfCheckout(checkoutId) ?? this.state?.sponsorId, checkoutId);
5367
5823
  return getCheckout(checkoutId, opts);
5368
5824
  }
5369
5825
  /**
@@ -5386,8 +5842,9 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
5386
5842
  if (!request) {
5387
5843
  const asked = (async () => {
5388
5844
  try {
5389
- const opts = await getCartGraphQLOptions(
5390
- sponsorId ?? this.sponsorIdOfCheckout(checkoutId) ?? this.state?.sponsorId
5845
+ const opts = await this.gqlOptions(
5846
+ sponsorId ?? this.sponsorIdOfCheckout(checkoutId) ?? this.state?.sponsorId,
5847
+ checkoutId
5391
5848
  );
5392
5849
  return await getCheckoutReference(checkoutId, opts);
5393
5850
  } catch {
@@ -5440,7 +5897,7 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
5440
5897
  * sponsor's name and logo as known now — so a confirmation after a redirect
5441
5898
  * or a reload can name the brand. Written again only when something changed.
5442
5899
  */
5443
- rememberSponsorOfCheckout(checkoutId, sponsorId) {
5900
+ rememberSponsorOfCheckout(checkoutId, sponsorId, channelId) {
5444
5901
  try {
5445
5902
  let identity = null;
5446
5903
  try {
@@ -5450,10 +5907,10 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
5450
5907
  }
5451
5908
  const name = identity?.name || this.cartManager.getCart(sponsorId)?.sponsorName || "";
5452
5909
  const logoUrl = identity?.logoUrl ?? null;
5453
- const key = JSON.stringify([checkoutId, sponsorId, name, logoUrl]);
5910
+ const key = JSON.stringify([checkoutId, sponsorId, name, logoUrl, channelId ?? null]);
5454
5911
  if (key === this.lastRememberedSponsor) return;
5455
5912
  this.lastRememberedSponsor = key;
5456
- rememberCheckoutSponsor(checkoutId, { sponsorId, name, logoUrl });
5913
+ rememberCheckoutSponsor(checkoutId, { sponsorId, name, logoUrl, channelId });
5457
5914
  } catch {
5458
5915
  }
5459
5916
  }
@@ -5471,7 +5928,7 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
5471
5928
  if (inFlight) return inFlight;
5472
5929
  const promise = (async () => {
5473
5930
  try {
5474
- const opts = await getCartGraphQLOptions(spId);
5931
+ const opts = await this.gqlOptions(spId);
5475
5932
  const res = await getAvailablePaymentMethods(opts);
5476
5933
  if (res) this.paymentMethodsCache.set(spId, res);
5477
5934
  if (usesStripe(res)) preloadStripeJs();
@@ -5508,7 +5965,7 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
5508
5965
  cartId = await this.cartManager.ensureCartId(spId);
5509
5966
  }
5510
5967
  if (!cartId) return [];
5511
- const opts = await getCartGraphQLOptions(spId);
5968
+ const opts = await this.gqlOptions(spId);
5512
5969
  const supplierGroups = await getLineItemsBySupplier(cartId, opts);
5513
5970
  this.shippingsResolved = Array.isArray(supplierGroups);
5514
5971
  const firstGroup = Array.isArray(supplierGroups) ? supplierGroups[0] : null;
@@ -5562,7 +6019,7 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
5562
6019
  if (!spId) return null;
5563
6020
  const cart = this.cartManager.getCart(spId);
5564
6021
  if (!cart?.cartId) return null;
5565
- const opts = await getCartGraphQLOptions(spId);
6022
+ const opts = await this.gqlOptions(spId);
5566
6023
  const updated = await updateShippingsBySupplier(cart.cartId, data, opts);
5567
6024
  if (updated) {
5568
6025
  this.cartManager.updateFromBackendCart(spId, updated);
@@ -5597,9 +6054,9 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
5597
6054
  if (!cartId) {
5598
6055
  throw new Error(`[CheckoutManager] Failed to obtain cart_id for sponsor ${spId}`);
5599
6056
  }
5600
- const opts = await getCartGraphQLOptions(spId);
6057
+ const opts = await this.gqlOptions(spId);
5601
6058
  const attribution = await this.attributionFor(spId);
5602
- let checkoutId = this.state?.checkoutId;
6059
+ let checkoutId = this.state?.sponsorId === spId ? this.state.checkoutId : void 0;
5603
6060
  if (!checkoutId) {
5604
6061
  const checkoutRes = attribution ? await createCheckout(cartId, opts, { attribution }) : await createCheckout(cartId, opts);
5605
6062
  checkoutId = checkoutRes?.id;
@@ -5756,7 +6213,7 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
5756
6213
  /** Vipps' own payment state for the checkout — see getVippsStatus in
5757
6214
  * cart-queries.ts for why this exists instead of trusting GetCheckout. */
5758
6215
  async getVippsStatus(checkoutId, sponsorId) {
5759
- const opts = await getCartGraphQLOptions(sponsorId ?? this.state?.sponsorId);
6216
+ const opts = await this.gqlOptions(sponsorId ?? this.sponsorIdOfCheckout(checkoutId) ?? this.state?.sponsorId, checkoutId);
5760
6217
  return getVippsStatus({ checkoutId }, opts);
5761
6218
  }
5762
6219
  /**
@@ -5773,7 +6230,7 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
5773
6230
  if (!cartId) {
5774
6231
  throw new Error(`[CheckoutManager] Failed to obtain cart_id for sponsor ${spId}`);
5775
6232
  }
5776
- const opts = await getCartGraphQLOptions(spId);
6233
+ const opts = await this.gqlOptions(spId);
5777
6234
  let checkoutId = this.state?.checkoutId;
5778
6235
  if (!checkoutId) {
5779
6236
  const checkoutRes = await this.createCheckout(spId, "kustom");
@@ -5819,7 +6276,7 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
5819
6276
  const spId = sponsorId ?? this.state?.sponsorId;
5820
6277
  const checkoutId = this.state?.checkoutId;
5821
6278
  if (!spId || !checkoutId) return null;
5822
- const opts = await getCartGraphQLOptions(spId);
6279
+ const opts = await this.gqlOptions(spId);
5823
6280
  suspendKustom();
5824
6281
  try {
5825
6282
  return await syncPaymentKustom(checkoutId, opts);
@@ -5829,7 +6286,7 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
5829
6286
  }
5830
6287
  /** Re-read a Kustom order — used on the confirmation return trip. */
5831
6288
  async getKustomOrder(orderId, sponsorId) {
5832
- const opts = await getCartGraphQLOptions(sponsorId ?? this.state?.sponsorId);
6289
+ const opts = await this.gqlOptions(sponsorId ?? this.state?.sponsorId);
5833
6290
  return getKustomOrder(orderId, opts);
5834
6291
  }
5835
6292
  /** Render a Kustom html_snippet (e.g. the confirmation receipt) into a container. */
@@ -5849,7 +6306,7 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
5849
6306
  if (!cartId) {
5850
6307
  throw new Error(`[CheckoutManager] Failed to obtain cart_id for sponsor ${spId}`);
5851
6308
  }
5852
- const opts = await getCartGraphQLOptions(spId);
6309
+ const opts = await this.gqlOptions(spId);
5853
6310
  let checkoutId = this.state?.checkoutId;
5854
6311
  if (!checkoutId) {
5855
6312
  const checkoutRes = await this.createCheckout(spId, "qliro");
@@ -5919,7 +6376,7 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
5919
6376
  }
5920
6377
  /** Re-read the Qliro order owned by a checkout — the confirmation trip. */
5921
6378
  async getQliroOrder(checkoutId, sponsorId) {
5922
- const opts = await getCartGraphQLOptions(sponsorId ?? this.state?.sponsorId);
6379
+ const opts = await this.gqlOptions(sponsorId ?? this.sponsorIdOfCheckout(checkoutId) ?? this.state?.sponsorId, checkoutId);
5923
6380
  return getQliroOrder(checkoutId, opts);
5924
6381
  }
5925
6382
  /**
@@ -5937,7 +6394,7 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
5937
6394
  if (!cartId) {
5938
6395
  throw new Error(`[CheckoutManager] Failed to obtain cart_id for sponsor ${spId}`);
5939
6396
  }
5940
- const opts = await getCartGraphQLOptions(spId);
6397
+ const opts = await this.gqlOptions(spId);
5941
6398
  let checkoutId = this.state?.checkoutId;
5942
6399
  if (!checkoutId) {
5943
6400
  const checkoutRes = await this.createCheckout(spId, "walley");
@@ -5974,7 +6431,7 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
5974
6431
  }
5975
6432
  /** Re-read the Walley session owned by a checkout — the confirmation trip. */
5976
6433
  async getWalleyOrder(checkoutId, sponsorId) {
5977
- const opts = await getCartGraphQLOptions(sponsorId ?? this.state?.sponsorId);
6434
+ const opts = await this.gqlOptions(sponsorId ?? this.sponsorIdOfCheckout(checkoutId) ?? this.state?.sponsorId, checkoutId);
5978
6435
  return getWalleyOrder(checkoutId, opts);
5979
6436
  }
5980
6437
  /**
@@ -6000,7 +6457,7 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
6000
6457
  if (!cartId) {
6001
6458
  throw new Error(`[CheckoutManager] Failed to obtain cart_id for sponsor ${spId}`);
6002
6459
  }
6003
- const opts = await getCartGraphQLOptions(spId);
6460
+ const opts = await this.gqlOptions(spId);
6004
6461
  const stored = resume ? readNexiPending() : null;
6005
6462
  const pending2 = stored && stored.paymentId === resume?.paymentId ? stored : null;
6006
6463
  let checkoutId = this.state?.checkoutId;
@@ -6234,7 +6691,7 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
6234
6691
  }
6235
6692
  /** Re-read the Nexi payment owned by a checkout. */
6236
6693
  async getNexiOrder(checkoutId, sponsorId) {
6237
- const opts = await getCartGraphQLOptions(sponsorId ?? this.state?.sponsorId);
6694
+ const opts = await this.gqlOptions(sponsorId ?? this.sponsorIdOfCheckout(checkoutId) ?? this.state?.sponsorId, checkoutId);
6238
6695
  return getNexiOrder(checkoutId, opts);
6239
6696
  }
6240
6697
  /**
@@ -6312,7 +6769,7 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
6312
6769
  * finalised on the server.
6313
6770
  */
6314
6771
  async getAdyenPayment(checkoutId, proof = {}, sponsorId) {
6315
- const opts = await getCartGraphQLOptions(sponsorId ?? this.state?.sponsorId);
6772
+ const opts = await this.gqlOptions(sponsorId ?? this.sponsorIdOfCheckout(checkoutId) ?? this.state?.sponsorId, checkoutId);
6316
6773
  return getAdyenPayment({ checkoutId, ...proof }, opts);
6317
6774
  }
6318
6775
  /* eslint-enable @typescript-eslint/no-explicit-any */
@@ -6591,7 +7048,7 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
6591
7048
  console.warn("[VioCheckout] updateCheckout before Apple Pay confirm failed:", err);
6592
7049
  }
6593
7050
  }
6594
- const gqlOpts = await getCartGraphQLOptions(spId);
7051
+ const gqlOpts = await this.gqlOptions(spId);
6595
7052
  const createRes = await createPaymentApplePay({ checkoutId }, gqlOpts);
6596
7053
  const gatewayId = String(createRes?.gateway_merchant_id ?? "");
6597
7054
  if (gatewayId.startsWith("pk_")) this.applePayPublishableKey = gatewayId;
@@ -6843,7 +7300,7 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
6843
7300
  const json = await executeKlarnaGraphQL(
6844
7301
  CREATE_PAYMENT_KLARNA_NATIVE_MUTATION,
6845
7302
  variables,
6846
- sponsorId
7303
+ await this.gqlOptions(sponsorId, checkoutId)
6847
7304
  );
6848
7305
  const nativeRes = json?.data?.Payment?.CreatePaymentKlarnaNative;
6849
7306
  if (!nativeRes || !nativeRes.client_token) {
@@ -6993,7 +7450,7 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
6993
7450
  const json = await executeKlarnaGraphQL(
6994
7451
  CREATE_PAYMENT_KLARNA_NATIVE_MUTATION,
6995
7452
  variables,
6996
- sponsorId
7453
+ await this.gqlOptions(sponsorId, checkoutId)
6997
7454
  );
6998
7455
  const nativeRes = json?.data?.Payment?.CreatePaymentKlarnaNative;
6999
7456
  if (!nativeRes || !nativeRes.client_token) {
@@ -7135,12 +7592,10 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
7135
7592
  shippingAddress: buildKlarnaAddress(address),
7136
7593
  customer: buildKlarnaCustomer(address)
7137
7594
  };
7595
+ const klarnaSponsorId = ctx?.sponsorId ?? this.state?.sponsorId;
7596
+ const klarnaOptions = await this.gqlOptions(klarnaSponsorId, ctx?.checkoutId ?? this.state?.checkoutId);
7138
7597
  const json = await withBuyerRef(
7139
- (withRef) => executeKlarnaGraphQL(
7140
- confirmPaymentKlarnaNativeMutation(withRef),
7141
- variables,
7142
- ctx?.sponsorId ?? this.state?.sponsorId
7143
- )
7598
+ (withRef) => executeKlarnaGraphQL(confirmPaymentKlarnaNativeMutation(withRef), variables, klarnaOptions)
7144
7599
  );
7145
7600
  const confirmRes = json?.data?.Payment?.ConfirmPaymentKlarnaNative;
7146
7601
  if (!confirmRes) {
@@ -7473,7 +7928,8 @@ function cartSlicesFrom(lines, resolveCampaign, fallbackCurrency) {
7473
7928
  if (Number.isFinite(sponsorId)) slice.sponsors.add(sponsorId);
7474
7929
  if (origin.slot) slice.slots.add(origin.slot);
7475
7930
  if (origin.contentId) slice.contents.add(origin.contentId);
7476
- const itemKey = `${line.productId}::${line.variantId ?? ""}`;
7931
+ const layout = itemLayoutOf(origin);
7932
+ const itemKey = `${line.productId}::${line.variantId ?? ""}::${layoutKeyOf(layout)}`;
7477
7933
  const item = slice.items.get(itemKey);
7478
7934
  if (item) item.quantity = (item.quantity ?? 0) + origin.quantity;
7479
7935
  else {
@@ -7483,7 +7939,8 @@ function cartSlicesFrom(lines, resolveCampaign, fallbackCurrency) {
7483
7939
  brand: line.brand,
7484
7940
  variantId: line.variantId,
7485
7941
  price: unitPrice,
7486
- quantity: origin.quantity
7942
+ quantity: origin.quantity,
7943
+ ...layout
7487
7944
  });
7488
7945
  }
7489
7946
  slice.value += unitPrice * origin.quantity;
@@ -7505,6 +7962,48 @@ function cartSlicesFrom(lines, resolveCampaign, fallbackCurrency) {
7505
7962
  function hasExplicitCampaign(lines) {
7506
7963
  return lines.some((line) => lineOrigins(line).some((o) => o.campaignId !== void 0));
7507
7964
  }
7965
+ function itemLayoutOf(origin) {
7966
+ const l = normalizeLayoutOrigin(origin);
7967
+ if (!l.layoutId) return {};
7968
+ return {
7969
+ layoutId: l.layoutId,
7970
+ layoutRevision: l.layoutRevision ?? "",
7971
+ experimentId: l.experimentId ?? "",
7972
+ experimentVariantId: l.variantId ?? ""
7973
+ };
7974
+ }
7975
+ function layoutKeyOf(layout) {
7976
+ return layout.layoutId ? [layout.layoutId, layout.layoutRevision, layout.experimentId, layout.experimentVariantId].join("|") : "";
7977
+ }
7978
+ function lineItemsByLayout(lines) {
7979
+ const out = [];
7980
+ for (const line of lines) {
7981
+ if (!line || typeof line !== "object") continue;
7982
+ const base = {
7983
+ productId: line.productId,
7984
+ name: line.name,
7985
+ brand: line.brand,
7986
+ variantId: line.variantId,
7987
+ price: line.unitPrice,
7988
+ quantity: line.quantity
7989
+ };
7990
+ const origins = lineOrigins(line);
7991
+ if (!origins.some((o) => normalizeLayoutOrigin(o).layoutId)) {
7992
+ out.push(base);
7993
+ continue;
7994
+ }
7995
+ const parts = /* @__PURE__ */ new Map();
7996
+ for (const origin of origins) {
7997
+ const layout = itemLayoutOf(origin);
7998
+ const key = layoutKeyOf(layout);
7999
+ const part = parts.get(key);
8000
+ if (part) part.quantity = (part.quantity ?? 0) + origin.quantity;
8001
+ else parts.set(key, { ...base, quantity: origin.quantity, ...layout });
8002
+ }
8003
+ out.push(...parts.values());
8004
+ }
8005
+ return out;
8006
+ }
7508
8007
 
7509
8008
  // src/core/analytics/analytics-manager.ts
7510
8009
  var ORDER_NUMBER_BACKOFF_MS = [500, 1e3, 2e3, 3e3, 3e3];
@@ -7826,21 +8325,27 @@ var AnalyticsManager = class extends EventTarget {
7826
8325
  content_title: this.options.privateContent ? void 0 : capTitle(
7827
8326
  ctx.contentTitle ?? content.title ?? (typeof document !== "undefined" && document.title ? document.title : void 0)
7828
8327
  ),
7829
- variant: ctx.variant
8328
+ variant: ctx.variant,
8329
+ // The detail layout shown (0.34) — `view_item` of a non-default layout.
8330
+ layout_id: ctx.layoutId,
8331
+ layout_revision: ctx.layoutRevision,
8332
+ experiment_id: ctx.experimentId,
8333
+ variant_id: ctx.variantId
7830
8334
  });
7831
8335
  const com = opts.commerce;
7832
8336
  const commerce = com ? compact({
7833
8337
  // The collector rejects an event with more lines than its cap, whole.
7834
- items: com.items?.slice(0, MAX_ITEMS).map(
7835
- (i) => compact({
8338
+ items: com.items?.slice(0, MAX_ITEMS).map((i) => ({
8339
+ ...compact({
7836
8340
  product_id: String(i.productId),
7837
8341
  name: this.options.privateContent ? void 0 : i.name,
7838
8342
  brand: this.options.privateContent ? void 0 : i.brand,
7839
8343
  variant_id: i.variantId !== void 0 ? String(i.variantId) : void 0,
7840
8344
  price: i.price,
7841
8345
  quantity: i.quantity
7842
- })
7843
- ),
8346
+ }),
8347
+ ...itemLayoutWire(i)
8348
+ })),
7844
8349
  value: com.value,
7845
8350
  currency: com.currency,
7846
8351
  order_id: com.orderId,
@@ -7997,16 +8502,19 @@ var AnalyticsManager = class extends EventTarget {
7997
8502
  this.track("add_to_cart", {
7998
8503
  context: { sponsorId: d.sponsorId !== void 0 ? Number(d.sponsorId) : void 0, campaignId: campaignOf(d), slot: slotOf(d), componentTemplateId: templateOf(d), ...contentOf(d) },
7999
8504
  commerce: {
8000
- items: [
8505
+ // The detail layout the units were added from (0.34): the add's own
8506
+ // (`detail.layout`, set by <vio-product-detail>), '' when none.
8507
+ items: withLineLayouts([
8001
8508
  {
8002
8509
  productId: d.productId ?? line?.productId ?? "",
8003
8510
  name: line?.name,
8004
8511
  brand: line?.brand,
8005
8512
  variantId: d.variantId ?? line?.variantId,
8006
8513
  price: line?.unitPrice,
8007
- quantity: d.quantity ?? 1
8514
+ quantity: d.quantity ?? 1,
8515
+ ...itemLayoutOf(normalizeLayoutOrigin(d.layout))
8008
8516
  }
8009
- ],
8517
+ ]),
8010
8518
  value: line ? line.unitPrice * (d.quantity ?? 1) : void 0,
8011
8519
  currency: line?.currency
8012
8520
  }
@@ -8018,16 +8526,17 @@ var AnalyticsManager = class extends EventTarget {
8018
8526
  this.track("remove_from_cart", {
8019
8527
  context: { sponsorId: d.sponsorId, campaignId: campaignOf(d), slot: slotOf(d), ...contentOf(d) },
8020
8528
  commerce: {
8021
- items: [
8529
+ items: withLineLayouts([
8022
8530
  {
8023
8531
  productId: d.productId,
8024
8532
  variantId: d.variantId,
8025
8533
  name: d.name,
8026
8534
  brand: d.brand,
8027
8535
  price: d.price,
8028
- quantity: d.quantity
8536
+ quantity: d.quantity,
8537
+ ...itemLayoutOf(normalizeLayoutOrigin(d.layout))
8029
8538
  }
8030
- ],
8539
+ ]),
8031
8540
  value: (d.price ?? 0) * (d.quantity ?? 1),
8032
8541
  currency: d.currency
8033
8542
  }
@@ -8330,14 +8839,8 @@ var AnalyticsManager = class extends EventTarget {
8330
8839
  }
8331
8840
  if (items.length === 0) return null;
8332
8841
  return {
8333
- items: items.map((i) => ({
8334
- productId: i.productId,
8335
- name: i.name,
8336
- brand: i.brand,
8337
- variantId: i.variantId,
8338
- price: i.unitPrice,
8339
- quantity: i.quantity
8340
- })),
8842
+ // One item per line and detail layout (0.34), each saying its layout.
8843
+ items: withLineLayouts(lineItemsByLayout(items)),
8341
8844
  value: items.reduce((sum, i) => sum + i.unitPrice * (i.quantity ?? 1), 0),
8342
8845
  currency: items[0]?.currency ?? this.lastCartSnapshot?.currency
8343
8846
  };
@@ -8391,6 +8894,27 @@ function impressionCommerce(read) {
8391
8894
  return void 0;
8392
8895
  }
8393
8896
  }
8897
+ function itemLayoutWire(i) {
8898
+ if (i.layoutId === void 0 && i.layoutRevision === void 0 && i.experimentId === void 0 && i.experimentVariantId === void 0) {
8899
+ return {};
8900
+ }
8901
+ const token = (v) => layoutToken(v) ?? "";
8902
+ return {
8903
+ layout_id: token(i.layoutId),
8904
+ layout_revision: token(i.layoutRevision),
8905
+ experiment_id: token(i.experimentId),
8906
+ experiment_variant_id: token(i.experimentVariantId)
8907
+ };
8908
+ }
8909
+ function withLineLayouts(items) {
8910
+ return items?.map((i) => ({
8911
+ ...i,
8912
+ layoutId: i.layoutId ?? "",
8913
+ layoutRevision: i.layoutRevision ?? "",
8914
+ experimentId: i.experimentId ?? "",
8915
+ experimentVariantId: i.experimentVariantId ?? ""
8916
+ }));
8917
+ }
8394
8918
  function compact(obj) {
8395
8919
  const out = {};
8396
8920
  for (const [k, v] of Object.entries(obj)) {
@@ -8458,7 +8982,7 @@ function capTitle(value) {
8458
8982
  return value.length > CONTENT_TITLE_MAX_LENGTH ? value.slice(0, CONTENT_TITLE_MAX_LENGTH) : value;
8459
8983
  }
8460
8984
  function sliceCommerce(slice) {
8461
- return { items: slice.items, value: slice.value, currency: slice.currency };
8985
+ return { items: withLineLayouts(slice.items), value: slice.value, currency: slice.currency };
8462
8986
  }
8463
8987
  function slotOf(detail) {
8464
8988
  return typeof detail.slot === "string" && detail.slot.trim() ? detail.slot.trim() : void 0;
@@ -8664,6 +9188,8 @@ function cut3(value, max) {
8664
9188
 
8665
9189
  // src/core/client.ts
8666
9190
  var SLOT_DECLARATION_DEBOUNCE_MS = 1e3;
9191
+ var BOOTSTRAP_MAX_AGE_MS = 5 * 6e4;
9192
+ var BOOTSTRAP_REFRESH_BACKOFF_MS = 6e4;
8667
9193
  var PRIVACY_MODE_TIMEOUT_MS = 1500;
8668
9194
  var PRIVACY_UNKNOWN_TTL_MS = 6e4;
8669
9195
  function privacyModeOf(value) {
@@ -8679,9 +9205,23 @@ function privacyModeOfSurface(res) {
8679
9205
  var VioFacade = class {
8680
9206
  constructor() {
8681
9207
  this.apiInstance = null;
9208
+ /** Commerce clients by sponsor AND key: one sponsor may sell on several channels (one per campaign). */
8682
9209
  this.commerceClients = /* @__PURE__ */ new Map();
8683
9210
  this.bootstrapPromise = null;
8684
9211
  this.cachedBootstrap = null;
9212
+ /** When the cached bootstrap arrived (ms epoch). */
9213
+ this.bootstrapAt = 0;
9214
+ /** A refresh of the bootstrap in flight (`refreshStaleBootstrap`). */
9215
+ this.bootstrapRefresh = null;
9216
+ this.bootstrapRefreshFailedAt = 0;
9217
+ /** The `visibilitychange` listener is installed once per page. */
9218
+ this.visibilityHooked = false;
9219
+ /**
9220
+ * Every selling block this page has seen, by `sponsorId:channelId` — so a
9221
+ * checkout opened on a channel keeps its key after a bootstrap refresh no
9222
+ * longer lists that channel (`sponsorCommerce(…, { retired: true })`).
9223
+ */
9224
+ this.seenChannels = /* @__PURE__ */ new Map();
8685
9225
  /** Last bootstrap failure — short backoff so cart mutations don't hammer a
8686
9226
  * dead endpoint on every call. */
8687
9227
  this.bootstrapFailedAt = 0;
@@ -8705,7 +9245,17 @@ var VioFacade = class {
8705
9245
  this.warnedDeclarationCap = false;
8706
9246
  // Client-side managers — created lazily on first access. They don't
8707
9247
  // need Vio.init() to work (state is purely local).
8708
- this.cartManager = null;
9248
+ /**
9249
+ * One cart manager per connection (environment + key), each with its own
9250
+ * storage entry; `Vio.cart` is a stable front for the current one, so a
9251
+ * re-init with another key switches carts without the page re-subscribing.
9252
+ * A manager left behind keeps its carts (they come back if the page switches
9253
+ * back) and finishes its requests into ITS carts, never the new one's.
9254
+ */
9255
+ this.cartManagers = /* @__PURE__ */ new Map();
9256
+ this.cartScope = "";
9257
+ this.cartFront = null;
9258
+ this.cartEvents = new EventTarget();
8709
9259
  this.checkoutManager = null;
8710
9260
  this.analyticsManager = null;
8711
9261
  /** What the surface answered of its privacy mode (`GET /v2/mobile/surface`) and when; `unknown` expires after a minute. */
@@ -8715,10 +9265,19 @@ var VioFacade = class {
8715
9265
  }
8716
9266
  init(config) {
8717
9267
  Configuration.init(config);
9268
+ try {
9269
+ const resolved = Configuration.get();
9270
+ this.switchCartScope(cartScopeOf(resolved.environment, resolved.apiKey));
9271
+ } catch {
9272
+ }
8718
9273
  this.apiInstance = null;
8719
9274
  this.commerceClients.clear();
8720
9275
  this.bootstrapPromise = null;
8721
9276
  this.cachedBootstrap = null;
9277
+ this.bootstrapAt = 0;
9278
+ this.bootstrapRefresh = null;
9279
+ this.bootstrapRefreshFailedAt = 0;
9280
+ this.seenChannels.clear();
8722
9281
  this.bootstrapFailedAt = 0;
8723
9282
  this.generation++;
8724
9283
  this.surfaceMode = null;
@@ -8757,6 +9316,25 @@ var VioFacade = class {
8757
9316
  }
8758
9317
  return resolveContent(init);
8759
9318
  }
9319
+ /**
9320
+ * Page-wide options of every `<vio-product-detail>` (0.34). `layout` orders
9321
+ * the sections below the gallery — `{ id, revision, sections, experimentId?,
9322
+ * variantId? }`; `null` goes back to the default; a detail's own `layout`
9323
+ * wins. Open details re-render at once, later ones open with it. A key left
9324
+ * out of the call is left as it is. See README «Section order and layout
9325
+ * tracking».
9326
+ */
9327
+ configureDetail(options = {}) {
9328
+ if (options && typeof options === "object" && "layout" in options) setGlobalDetailLayout(options.layout ?? null);
9329
+ }
9330
+ /**
9331
+ * The layout a `<vio-product-detail>` without its own `layout` applies now:
9332
+ * the page's (`source: 'global'`) or the default. A detail's own is
9333
+ * `el.appliedLayout`. A copy; never throws.
9334
+ */
9335
+ detailLayout() {
9336
+ return copyAppliedLayout(resolveDetailLayout(null));
9337
+ }
8760
9338
  get isInitialized() {
8761
9339
  return Configuration.isInitialized;
8762
9340
  }
@@ -8766,8 +9344,66 @@ var VioFacade = class {
8766
9344
  // MARK: - Client-side managers (no init required)
8767
9345
  /** Multi-sponsor cart manager. Reactive — listen via addEventListener('change', ...). */
8768
9346
  get cart() {
8769
- if (!this.cartManager) this.cartManager = new CartManager();
8770
- return this.cartManager;
9347
+ if (!this.cartFront) this.cartFront = this.makeCartFront();
9348
+ return this.cartFront;
9349
+ }
9350
+ /** The cart manager of the page's connection now (created on first use). */
9351
+ currentCartManager() {
9352
+ const scope = this.cartScope;
9353
+ let manager = this.cartManagers.get(scope);
9354
+ if (!manager) {
9355
+ const created = new CartManager(scope);
9356
+ for (const type of ["change", "error"]) {
9357
+ created.addEventListener(type, (e) => {
9358
+ if (this.cartManagers.get(this.cartScope) !== created) return;
9359
+ this.cartEvents.dispatchEvent(new CustomEvent(type, { detail: e.detail }));
9360
+ });
9361
+ }
9362
+ this.cartManagers.set(scope, created);
9363
+ manager = created;
9364
+ }
9365
+ return manager;
9366
+ }
9367
+ /**
9368
+ * `Vio.cart`: every property and method is the current connection's
9369
+ * manager's; the listeners live on the front, so they survive a switch.
9370
+ */
9371
+ makeCartFront() {
9372
+ const events = this.cartEvents;
9373
+ const listenerMethods = {
9374
+ addEventListener: events.addEventListener.bind(events),
9375
+ removeEventListener: events.removeEventListener.bind(events),
9376
+ dispatchEvent: events.dispatchEvent.bind(events)
9377
+ };
9378
+ return new Proxy(Object.create(CartManager.prototype), {
9379
+ get: (_target, prop) => {
9380
+ if (typeof prop === "string" && prop in listenerMethods) return listenerMethods[prop];
9381
+ const manager = this.currentCartManager();
9382
+ const value = Reflect.get(manager, prop, manager);
9383
+ if (typeof value !== "function" || Object.prototype.hasOwnProperty.call(manager, prop)) return value;
9384
+ return value.bind(manager);
9385
+ },
9386
+ set: (_target, prop, value) => Reflect.set(this.currentCartManager(), prop, value),
9387
+ has: (_target, prop) => Reflect.has(this.currentCartManager(), prop),
9388
+ // A property defined on Vio.cart (a test's spy, a host's patch) lands on
9389
+ // the current manager, where its own methods find it.
9390
+ defineProperty: (_target, prop, descriptor) => Reflect.defineProperty(this.currentCartManager(), prop, descriptor),
9391
+ deleteProperty: (_target, prop) => Reflect.deleteProperty(this.currentCartManager(), prop),
9392
+ getOwnPropertyDescriptor: (_target, prop) => {
9393
+ const own = Reflect.getOwnPropertyDescriptor(this.currentCartManager(), prop);
9394
+ return own ? { ...own, configurable: true } : void 0;
9395
+ }
9396
+ });
9397
+ }
9398
+ /** After `init`: the carts are the new connection's. Says `change` when that switched them. */
9399
+ switchCartScope(scope) {
9400
+ if (scope === this.cartScope) return;
9401
+ this.cartManagers.get(this.cartScope)?.setActive(false);
9402
+ this.cartScope = scope;
9403
+ if (!this.cartFront) return;
9404
+ const manager = this.currentCartManager();
9405
+ manager.setActive(true);
9406
+ manager.announce();
8771
9407
  }
8772
9408
  /** Checkout flow manager. Subscribes to cart state via the CartManager; asks here for the order's attribution. */
8773
9409
  get checkout() {
@@ -8815,7 +9451,7 @@ var VioFacade = class {
8815
9451
  const promise = this.api.bootstrap(Configuration.get().platform).then(
8816
9452
  (res) => {
8817
9453
  if (generation !== this.generation) return this.bootstrap();
8818
- this.cachedBootstrap = res;
9454
+ this.adoptBootstrap(res);
8819
9455
  this.bootstrapFailedAt = 0;
8820
9456
  if (this.slotsDirty) this.scheduleSlotDeclaration();
8821
9457
  this.preloadPrivacyMode();
@@ -8835,6 +9471,85 @@ var VioFacade = class {
8835
9471
  get bootstrapCache() {
8836
9472
  return this.cachedBootstrap;
8837
9473
  }
9474
+ /** A bootstrap becomes the page's: when it arrived, the channels it lists, and the tab-visible refresh. */
9475
+ adoptBootstrap(res) {
9476
+ this.cachedBootstrap = res;
9477
+ this.bootstrapAt = Date.now();
9478
+ try {
9479
+ const ids = /* @__PURE__ */ new Set();
9480
+ const add = (s) => {
9481
+ const id = toSponsorId(s?.id);
9482
+ if (id !== void 0) ids.add(id);
9483
+ };
9484
+ add(res?.primarySponsor);
9485
+ if (Array.isArray(res?.secondarySponsors)) res.secondarySponsors.forEach(add);
9486
+ if (Array.isArray(res?.campaigns)) {
9487
+ for (const c of res.campaigns) {
9488
+ if (!c || typeof c !== "object") continue;
9489
+ add(c.primarySponsor);
9490
+ if (Array.isArray(c.secondarySponsors)) c.secondarySponsors.forEach(add);
9491
+ }
9492
+ }
9493
+ for (const id of ids) {
9494
+ for (const { sponsor } of sellingBlocksIn(res, id)) {
9495
+ const channel = channelIdOf(sponsor.commerce.channelId);
9496
+ if (channel !== void 0) this.seenChannels.set(`${id}:${channel}`, sponsor);
9497
+ }
9498
+ }
9499
+ } catch {
9500
+ }
9501
+ this.hookVisibility();
9502
+ }
9503
+ /**
9504
+ * The bootstrap is cached for the page's life, but a tab can stay open for
9505
+ * days: when it becomes visible again and the cached one is older than
9506
+ * five minutes, it is fetched again (new campaigns, channels). The old one
9507
+ * stays until the new one arrives — and stays if the refresh fails. Carts
9508
+ * keep their lines; a cart whose channel the new bootstrap no longer lists
9509
+ * is moved to the current channel the next time it is used (CartManager),
9510
+ * and an open checkout keeps paying with the key of its own channel.
9511
+ */
9512
+ hookVisibility() {
9513
+ if (this.visibilityHooked) return;
9514
+ if (typeof document === "undefined" || typeof document.addEventListener !== "function") return;
9515
+ this.visibilityHooked = true;
9516
+ document.addEventListener("visibilitychange", () => {
9517
+ try {
9518
+ if (document.visibilityState === "visible") void this.refreshStaleBootstrap();
9519
+ } catch {
9520
+ }
9521
+ });
9522
+ }
9523
+ /** Fetch the bootstrap again when the cached one is older than `maxAgeMs`; never throws, never clears the cache. */
9524
+ refreshStaleBootstrap(maxAgeMs = BOOTSTRAP_MAX_AGE_MS) {
9525
+ try {
9526
+ if (this.bootstrapRefresh) return this.bootstrapRefresh;
9527
+ if (!this.cachedBootstrap || this.bootstrapPromise || !Configuration.isInitialized) return Promise.resolve();
9528
+ const now = Date.now();
9529
+ if (now - this.bootstrapAt < maxAgeMs) return Promise.resolve();
9530
+ if (this.bootstrapRefreshFailedAt && now - this.bootstrapRefreshFailedAt < BOOTSTRAP_REFRESH_BACKOFF_MS) {
9531
+ return Promise.resolve();
9532
+ }
9533
+ const generation = this.generation;
9534
+ const refresh = this.api.bootstrap(Configuration.get().platform).then(
9535
+ (res) => {
9536
+ if (generation !== this.generation || !res || typeof res !== "object") return;
9537
+ this.adoptBootstrap(res);
9538
+ this.bootstrapRefreshFailedAt = 0;
9539
+ if (this.slotsDirty) this.scheduleSlotDeclaration();
9540
+ },
9541
+ () => {
9542
+ if (generation === this.generation) this.bootstrapRefreshFailedAt = Date.now();
9543
+ }
9544
+ ).finally(() => {
9545
+ if (this.bootstrapRefresh === refresh) this.bootstrapRefresh = null;
9546
+ });
9547
+ this.bootstrapRefresh = refresh;
9548
+ return refresh;
9549
+ } catch {
9550
+ return Promise.resolve();
9551
+ }
9552
+ }
8838
9553
  /**
8839
9554
  * Who a sponsor is to a shopper — `{ id, name, logoUrl, avatarUrl }` — by
8840
9555
  * the ONE rule every element uses (`sponsorIdentityFrom`): the live
@@ -8869,22 +9584,60 @@ var VioFacade = class {
8869
9584
  }
8870
9585
  }
8871
9586
  /**
8872
- * Get a CommerceClient scoped to a specific sponsor (via bootstrap).
8873
- * Throws if bootstrap hasn't been called or the sponsor is unknown.
9587
+ * The block a sponsor sells with — its `commerce.apiKey` and `channelId` —
9588
+ * by the rule one article = one campaign = one channel = one sponsor (see
9589
+ * `sponsorCommerceIn`): `ctx.channelId` → that channel's block only (a cart
9590
+ * created on a channel is only called with its key); else the campaign in
9591
+ * context — `ctx.campaignId` candidates (the click's, the element's
9592
+ * `vio-campaign`, an ancestor's `data-vio-campaign`), then
9593
+ * `Vio.init({ campaignId })` — when the sponsor takes part in it; else the
9594
+ * sponsor's first block that sells (the behaviour before). `campaignId:
9595
+ * null` means no campaign at all, not even `Vio.init`'s. `retired: true`
9596
+ * also answers a channel the current bootstrap no longer lists with the
9597
+ * block this page saw for it (an open checkout after a bootstrap refresh).
9598
+ * Blocks with `commerce: null` are skipped. `null` when none; never throws.
8874
9599
  */
8875
- commerceFor(sponsorId) {
9600
+ sponsorCommerce(sponsorId, ctx = {}) {
9601
+ try {
9602
+ const id = toSponsorId(sponsorId);
9603
+ if (id === void 0) return null;
9604
+ const channel = channelIdOf(ctx?.channelId);
9605
+ if (channel !== void 0) {
9606
+ const listed = sponsorCommerceIn(this.cachedBootstrap, id, { channelId: channel });
9607
+ if (listed) return listed;
9608
+ return ctx.retired ? this.seenChannels.get(`${id}:${channel}`) ?? null : null;
9609
+ }
9610
+ const campaignId = ctx?.campaignId === null ? [] : this.pageCampaignCandidates(ctx?.campaignId);
9611
+ return sponsorCommerceIn(this.cachedBootstrap, id, { campaignId }) ?? null;
9612
+ } catch {
9613
+ return null;
9614
+ }
9615
+ }
9616
+ /**
9617
+ * Get a CommerceClient scoped to a sponsor (via bootstrap) — on the
9618
+ * channel of the campaign in context (`ctx.campaignId`, the element's
9619
+ * candidates first; see `sponsorCommerce`) or of `ctx.channelId`. Without
9620
+ * `ctx`, the page's campaign (`Vio.init`) when the sponsor takes part in
9621
+ * it, else the sponsor's first selling block, as before. Clients are kept
9622
+ * per sponsor and key. Throws if bootstrap hasn't been called or nothing of
9623
+ * the sponsor sells there.
9624
+ */
9625
+ commerceFor(sponsorId, ctx) {
8876
9626
  sponsorId = Number(sponsorId);
8877
- const existing = this.commerceClients.get(sponsorId);
8878
- if (existing) return existing;
8879
- const sponsor = this.findSponsor(sponsorId);
9627
+ const sponsor = this.sponsorCommerce(sponsorId, ctx ?? {});
8880
9628
  if (!sponsor) {
9629
+ const channel = channelIdOf(ctx?.channelId);
8881
9630
  throw new Error(
8882
- `[Vio] Sponsor ${sponsorId} not found in bootstrap. Call Vio.bootstrap() first or pass commerce-key directly.`
9631
+ channel !== void 0 ? `[Vio] Channel ${channel} of sponsor ${sponsorId} not found in bootstrap.` : `[Vio] Sponsor ${sponsorId} not found in bootstrap. Call Vio.bootstrap() first or pass commerce-key directly.`
8883
9632
  );
8884
9633
  }
9634
+ const apiKey = sponsor.commerce.apiKey;
9635
+ const cacheKey = `${sponsorId}:${apiKey}`;
9636
+ const existing = this.commerceClients.get(cacheKey);
9637
+ if (existing) return existing;
8885
9638
  const cfg = Configuration.get();
8886
- const client = createCommerceClient({ endpoint: cfg.graphQLBase, apiKey: sponsor.commerce.apiKey });
8887
- this.commerceClients.set(sponsorId, client);
9639
+ const client = createCommerceClient({ endpoint: cfg.graphQLBase, apiKey });
9640
+ this.commerceClients.set(cacheKey, client);
8888
9641
  return client;
8889
9642
  }
8890
9643
  /** Get a CommerceClient using an explicit commerce apiKey (skip bootstrap). */
@@ -9223,9 +9976,10 @@ var VioFacade = class {
9223
9976
  * The attribution of the order a sponsor's cart is about to become
9224
9977
  * (ADR-0035 phase B) — what `CreateCheckout` / `UpdateCheckout` send as
9225
9978
  * `attribution` and shopcart writes on the order: the surface (its SDK
9226
- * key), the page's campaign (`pageCampaign()`; without one, the campaign
9227
- * that explains most units of the cart under the same rule `purchase` is
9228
- * split by — `cartSlices`) and, on a surface KNOWN to be `standard`
9979
+ * key), the campaign of the order — always one the cart's sponsor takes part
9980
+ * in (`orderCampaign`: the cart lines' own campaign, else the page's;
9981
+ * without one, the campaign that explains most units of the cart under the
9982
+ * same rule `purchase` is split by — `cartSlices`) and, on a surface KNOWN to be `standard`
9229
9983
  * (`privacyModeNow`), the page (`content()`'s url as origin + path, and
9230
9984
  * title) and where the session came in: the landing the session REMEMBERS
9231
9985
  * (`storedLanding`, else the one analytics resolved on this page) — never
@@ -9237,10 +9991,13 @@ var VioFacade = class {
9237
9991
  try {
9238
9992
  const config = Configuration.isInitialized ? Configuration.get() : void 0;
9239
9993
  const content = this.content();
9994
+ const sponsor = toSponsorId(sponsorId);
9995
+ const belongs = (campaignId) => campaignId === void 0 || campaignTakesSponsor(this.cachedBootstrap, campaignId, sponsor);
9240
9996
  return checkoutAttributionOf({
9241
9997
  surfaceKey: config?.apiKey,
9242
- pageCampaignId: toNumber(this.pageCampaign()?.id),
9243
- slices: this.cartSlices(sponsorId),
9998
+ pageCampaignId: this.orderCampaign(sponsor),
9999
+ // The fallback campaign of the slices, too, only when it is the sponsor's.
10000
+ slices: this.cartSlices(sponsorId).filter((slice) => belongs(slice.campaignId)),
9244
10001
  content: { url: content.url, title: content.title },
9245
10002
  landing: this.rememberedLanding(),
9246
10003
  privacyMode: this.privacyModeNow()
@@ -9249,6 +10006,29 @@ var VioFacade = class {
9249
10006
  return void 0;
9250
10007
  }
9251
10008
  }
10009
+ /**
10010
+ * The campaign an order of `sponsorId` is credited to — one article = one
10011
+ * campaign = one sponsor, so it is always one the sponsor takes part in:
10012
+ * the campaign the cart's own lines were added from (their origins; the one
10013
+ * that explains most units), else the page's — the elements' campaigns on
10014
+ * the page (`vio-campaign` / `data-vio-campaign`, in document order), then
10015
+ * `Vio.init({ campaignId })` — the first one active with that sponsor.
10016
+ * `undefined` when neither says one (the slices then decide). Without a
10017
+ * sponsor, the page's campaign as before.
10018
+ */
10019
+ orderCampaign(sponsorId) {
10020
+ const boot = this.cachedBootstrap;
10021
+ if (sponsorId !== void 0) {
10022
+ const lines = [...this.cart.getCart(sponsorId)?.items ?? []];
10023
+ const fromLines = dominantCampaignOf(
10024
+ cartSlicesFrom(lines, () => void 0).filter(
10025
+ (slice) => slice.campaignId !== void 0 && campaignTakesSponsor(boot, slice.campaignId, sponsorId)
10026
+ )
10027
+ );
10028
+ if (fromLines !== void 0) return fromLines;
10029
+ }
10030
+ return toNumber(activeCampaignEntry(boot, this.pageCampaignCandidates(documentCampaignHints()), sponsorId)?.id);
10031
+ }
9252
10032
  /** The landing the session remembers: what the first page stored, else what analytics resolved on this page load (consent pending); null otherwise. */
9253
10033
  rememberedLanding() {
9254
10034
  try {
@@ -9350,6 +10130,35 @@ var VioFacade = class {
9350
10130
  return findSponsorIn(this.cachedBootstrap, sponsorId);
9351
10131
  }
9352
10132
  };
10133
+ function campaignTakesSponsor(boot, campaignId, sponsorId) {
10134
+ if (sponsorId === void 0 || !boot) return true;
10135
+ try {
10136
+ const listed = (Array.isArray(boot.campaigns) ? boot.campaigns : []).filter(
10137
+ (c) => !!c && typeof c === "object" && toNumber(c.id) === campaignId
10138
+ );
10139
+ if (listed.length > 0) return listed.some((c) => campaignSponsorIds(c).includes(sponsorId));
10140
+ if (toNumber(boot.campaign?.id) === campaignId) {
10141
+ const entry = defaultCampaignEntry(boot);
10142
+ return !!entry && campaignSponsorIds(entry).includes(sponsorId);
10143
+ }
10144
+ return true;
10145
+ } catch {
10146
+ return true;
10147
+ }
10148
+ }
10149
+ function documentCampaignHints() {
10150
+ try {
10151
+ if (typeof document === "undefined" || typeof document.querySelectorAll !== "function") return [];
10152
+ const out = [];
10153
+ document.querySelectorAll("[vio-campaign], [data-vio-campaign]").forEach((el) => {
10154
+ const value = el.getAttribute("vio-campaign") || el.getAttribute("data-vio-campaign");
10155
+ if (value) out.push(value);
10156
+ });
10157
+ return out;
10158
+ } catch {
10159
+ return [];
10160
+ }
10161
+ }
9353
10162
  function warn(...args) {
9354
10163
  if (typeof console !== "undefined") console.warn("[Vio]", ...args);
9355
10164
  }
@@ -9408,6 +10217,6 @@ function vippsButtonAttributes(verb = "buy", opts = {}) {
9408
10217
  };
9409
10218
  }
9410
10219
 
9411
- export { ADD_ITEM_MUTATION, ADYEN_RETURN_METHOD, ADYEN_RETURN_QUERY_KEYS, AdyenOriginError, AnalyticsManager, BUYER_REF_MAX_LENGTH, CHECKOUT_ATTRIBUTION_CAPS, CHECKOUT_SPONSOR_STORAGE_KEY, CLIENT_LANDING_URL_MAX_LENGTH, CONTENT_URL_MAX_LENGTH, CREATE_CART_MUTATION, CREATE_CHECKOUT_MUTATION, CREATE_CHECKOUT_WITH_ATTRIBUTION_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, GRAPHQL_INT_MAX, INTERNAL_KEY, INTERNAL_PARAM, KUSTOM_RETURN_QUERY_KEYS, LINE_ORIGIN_CAPS, SDK_VERSION, UPDATE_CHECKOUT_MUTATION, UPDATE_CHECKOUT_WITH_ATTRIBUTION_MUTATION, UPDATE_ITEM_MUTATION, UPDATE_ITEM_WITH_ORIGIN_MUTATION, UPDATE_SHIPPINGS_BY_SUPPLIER_MUTATION, VIPPS_BUTTON_SCRIPT, VIPPS_BUTTON_TAG, Vio, VioApi, VioApiError, __decorateClass, activeCampaignEntry, addItem, adyenReturnOutcome, adyenReturnParams, autoSlotDisplayName, autoSlotId, buyerRefOf, campaignCandidates, capContentUrl, cartSlicesFrom, checkApplePayAvailability, checkoutAttributionOf, checkoutSponsorOf, componentKindOf, createCart, createCheckout, createCommerceClient, createPaymentStripe, createPaymentVipps, createVioApi, deleteItem, displayPrice, dominantCampaignOf, ensureVippsButton, entryContextOf, executeCartGraphQL, formatPrice, gatewayHas, gatewayHasBuyerRef, getAvailablePaymentMethods, getCart, getCartGraphQLOptions, getCheckout, getCheckoutReference, getCustomerSessionId, getGlobalCountryCode, getGlobalCurrency, getLineItemsBySupplier, getVippsStatus, isUnknownFeatureError, isUnknownTokenError, isValidPhone, kustomReturnOutcome, landingUrl, lineOriginWire, lineOrigins, nexiReturnPaymentId, normalizeBuyerRef, normalizeOrigin, normalizeScope, normalizeSlotId, normalizeSlotKind, originAndPath, primaryImageUrl, purchaseFromPaymentSuccess, purchaseIdsOf, readInternal, readNexiPending, rememberedCheckoutSponsor, resetGatewayFeatures, resolveCampaignId, resolveClientContext, resolveContent, resolveInternal, resolveLanding, resolveSlotCampaign, sameOrigin, showApplePaySheet, sponsorCampaignEntry, sponsorIdentityFrom, storageGet, storedLanding, toCampaignId, updateCheckout, updateItem, updateShippingsBySupplier, vippsButtonAttributes, withGatewayFeature, writeInternal };
9412
- //# sourceMappingURL=chunk-IR4XYTZL.js.map
9413
- //# sourceMappingURL=chunk-IR4XYTZL.js.map
10220
+ export { ADD_ITEM_MUTATION, ADYEN_RETURN_METHOD, ADYEN_RETURN_QUERY_KEYS, AdyenOriginError, AnalyticsManager, BUYER_REF_MAX_LENGTH, CHECKOUT_ATTRIBUTION_CAPS, CHECKOUT_SPONSOR_STORAGE_KEY, CLIENT_LANDING_URL_MAX_LENGTH, CONTENT_URL_MAX_LENGTH, CREATE_CART_MUTATION, CREATE_CHECKOUT_MUTATION, CREATE_CHECKOUT_WITH_ATTRIBUTION_MUTATION, CREATE_PAYMENT_STRIPE_MUTATION, CREATE_PAYMENT_VIPPS_MUTATION, CartManager, CheckoutManager, CommerceClient, Configuration, DELETE_ITEM_MUTATION, DETAIL_SECTION_IDS, ESSENTIAL_DETAIL_SECTIONS, GET_AVAILABLE_PAYMENT_METHODS_QUERY, GET_CART_QUERY, GET_CHECKOUT_QUERY, GET_LINE_ITEMS_BY_SUPPLIER_QUERY, GET_VIPPS_STATUS_QUERY, GRAPHQL_INT_MAX, INTERNAL_KEY, INTERNAL_PARAM, KUSTOM_RETURN_QUERY_KEYS, LINE_ORIGIN_CAPS, SDK_VERSION, UPDATE_CHECKOUT_MUTATION, UPDATE_CHECKOUT_WITH_ATTRIBUTION_MUTATION, UPDATE_ITEM_MUTATION, UPDATE_ITEM_WITH_ORIGIN_MUTATION, UPDATE_SHIPPINGS_BY_SUPPLIER_MUTATION, VIPPS_BUTTON_SCRIPT, VIPPS_BUTTON_TAG, Vio, VioApi, VioApiError, __decorateClass, activeCampaignEntry, addItem, adyenReturnOutcome, adyenReturnParams, autoSlotDisplayName, autoSlotId, buyerRefOf, campaignCandidates, capContentUrl, cartSlicesFrom, checkApplePayAvailability, checkoutAttributionOf, checkoutSponsorOf, componentKindOf, copyAppliedLayout, createCart, createCheckout, createCommerceClient, createPaymentStripe, createPaymentVipps, createVioApi, defaultDetailLayout, deleteItem, displayPrice, dominantCampaignOf, effectiveDetailSections, ensureVippsButton, entryContextOf, executeCartGraphQL, formatPrice, gatewayHas, gatewayHasBuyerRef, getAvailablePaymentMethods, getCart, getCartGraphQLOptions, getCheckout, getCheckoutReference, getCustomerSessionId, getGlobalCountryCode, getGlobalCurrency, getLineItemsBySupplier, getVippsStatus, isUnknownFeatureError, isUnknownTokenError, isValidPhone, kustomReturnOutcome, landingUrl, layoutOriginOf, layoutSignature, lineOriginWire, lineOrigins, nexiReturnPaymentId, normalizeBuyerRef, normalizeDetailLayout, normalizeOrigin, normalizeScope, normalizeSlotId, normalizeSlotKind, onGlobalDetailLayoutChange, originAndPath, primaryImageUrl, purchaseFromPaymentSuccess, purchaseIdsOf, readInternal, readNexiPending, rememberedCheckoutSponsor, resetGatewayFeatures, resolveCampaignId, resolveClientContext, resolveContent, resolveDetailLayout, resolveInternal, resolveLanding, resolveSlotCampaign, sameOrigin, shippingDescriptionOf, showApplePaySheet, sponsorCampaignEntry, sponsorIdentityFrom, storageGet, storedLanding, toCampaignId, updateCheckout, updateItem, updateShippingsBySupplier, vippsButtonAttributes, withGatewayFeature, writeInternal };
10221
+ //# sourceMappingURL=chunk-QA5R4VYF.js.map
10222
+ //# sourceMappingURL=chunk-QA5R4VYF.js.map