@vio-live/web-sdk 0.21.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.
@@ -1,4 +1,4 @@
1
- import { Vio, __decorateClass, primaryImageUrl, displayPrice, getGlobalCurrency, ensureVippsButton, formatPrice, nexiReturnPaymentId, readNexiPending, kustomReturnOutcome, KUSTOM_RETURN_QUERY_KEYS, ADYEN_RETURN_METHOD, adyenReturnParams, ADYEN_RETURN_QUERY_KEYS, adyenReturnOutcome, AdyenOriginError } from './chunk-WTVGVDZX.js';
1
+ import { Vio, __decorateClass, primaryImageUrl, displayPrice, getGlobalCurrency, ensureVippsButton, formatPrice, nexiReturnPaymentId, readNexiPending, kustomReturnOutcome, KUSTOM_RETURN_QUERY_KEYS, ADYEN_RETURN_METHOD, adyenReturnParams, ADYEN_RETURN_QUERY_KEYS, adyenReturnOutcome, AdyenOriginError, autoSlotDisplayName, autoSlotId, normalizeScope } from './chunk-AMDI7KH3.js';
2
2
  import { LitElement, css, html } from 'lit';
3
3
  import { property, state, query } from 'lit/decorators.js';
4
4
 
@@ -165,6 +165,124 @@ function applyVioTheme(overrides, target = typeof document !== "undefined" ? doc
165
165
  }
166
166
  }
167
167
  }
168
+
169
+ // src/ui/slot-registration.ts
170
+ var SCOPE_ATTR = "vio-scope";
171
+ var SCOPE_NAME_ATTR = "vio-scope-name";
172
+ var SCOPE_DATA_ATTR = "data-vio-scope";
173
+ var SCOPE_NAME_DATA_ATTR = "data-vio-scope-name";
174
+ var SlotRegistration = class {
175
+ constructor(host, kind) {
176
+ this.host = host;
177
+ this.kind = kind;
178
+ this.identity = null;
179
+ this.release = null;
180
+ /** The element's position-based identity, fixed at connect time. */
181
+ this.position = null;
182
+ }
183
+ /** The effective slot id ('' before connect / after disconnect). */
184
+ get effective() {
185
+ return this.identity?.id ?? "";
186
+ }
187
+ /** The identity in effect, or null. */
188
+ get current() {
189
+ return this.identity;
190
+ }
191
+ /** On connect: fix scope + ordinal from the element's position, then register. */
192
+ connect(attrs) {
193
+ this.position = positionOf(this.host, attrs);
194
+ this.sync(attrs);
195
+ }
196
+ /** (Re)register when the attributes changed the identity. */
197
+ sync(attrs) {
198
+ if (this.position && scopeOf(this.host, attrs) !== this.position.scope) {
199
+ this.position = positionOf(this.host, attrs);
200
+ }
201
+ const next = this.resolve(attrs);
202
+ if (sameIdentity(next, this.identity)) return;
203
+ this.clear();
204
+ if (!next) return;
205
+ try {
206
+ this.release = Vio.registerSlot(next.id, next.displayName);
207
+ this.identity = next;
208
+ } catch {
209
+ this.release = null;
210
+ this.identity = null;
211
+ }
212
+ }
213
+ /** What the identity would be for these attributes (null before connect without a host id). */
214
+ resolve(attrs) {
215
+ const explicit = typeof attrs.vioSlot === "string" ? attrs.vioSlot.trim() : "";
216
+ const name = typeof attrs.vioSlotName === "string" && attrs.vioSlotName.trim() ? attrs.vioSlotName.trim() : void 0;
217
+ if (explicit) return { id: explicit, displayName: name, auto: false, scope: this.position?.scope ?? "" };
218
+ if (!this.position) return null;
219
+ const { scope, scopeName, ordinal } = this.position;
220
+ return {
221
+ id: autoSlotId(this.kind, ordinal, scope),
222
+ displayName: name ?? autoSlotDisplayName(this.kind, ordinal, scopeName),
223
+ auto: true,
224
+ scope
225
+ };
226
+ }
227
+ /** Unregister whatever is registered. */
228
+ clear() {
229
+ try {
230
+ this.release?.();
231
+ } catch {
232
+ }
233
+ this.release = null;
234
+ this.identity = null;
235
+ }
236
+ };
237
+ function sameIdentity(a, b) {
238
+ if (!a || !b) return a === b;
239
+ return a.id === b.id && a.displayName === b.displayName;
240
+ }
241
+ function scopeOf(el, attrs) {
242
+ const own = attrs && typeof attrs.vioScope === "string" ? attrs.vioScope : typeof el.vioScope === "string" ? el.vioScope ?? "" : el.getAttribute(SCOPE_ATTR) ?? "";
243
+ const value = normalizeScope(own) || normalizeScope(closestAcrossShadow(el, `[${SCOPE_DATA_ATTR}]`)?.getAttribute(SCOPE_DATA_ATTR));
244
+ return value;
245
+ }
246
+ function scopeNameOf(el, attrs) {
247
+ const own = attrs && typeof attrs.vioScopeName === "string" ? attrs.vioScopeName : typeof el.vioScopeName === "string" ? el.vioScopeName ?? "" : el.getAttribute(SCOPE_NAME_ATTR) ?? "";
248
+ const value = own.trim() || (closestAcrossShadow(el, `[${SCOPE_NAME_DATA_ATTR}]`)?.getAttribute(SCOPE_NAME_DATA_ATTR) ?? "").trim();
249
+ return value;
250
+ }
251
+ function positionOf(host, attrs) {
252
+ const scope = scopeOf(host, attrs);
253
+ const scopeName = scopeNameOf(host, attrs);
254
+ let ordinal = 0;
255
+ try {
256
+ const tag = host.tagName.toLowerCase();
257
+ const root = host.getRootNode();
258
+ const scopeRoot = root && typeof root.querySelectorAll === "function" ? root : typeof document !== "undefined" ? document : null;
259
+ if (scopeRoot) {
260
+ const peers = Array.from(scopeRoot.querySelectorAll(tag)).filter(
261
+ (el) => el === host || scopeOf(el) === scope
262
+ );
263
+ const index = peers.indexOf(host);
264
+ ordinal = index >= 0 ? index : 0;
265
+ }
266
+ } catch {
267
+ ordinal = 0;
268
+ }
269
+ return { scope, scopeName, ordinal };
270
+ }
271
+ function closestAcrossShadow(start, selector) {
272
+ try {
273
+ let node = start;
274
+ while (node) {
275
+ const found = typeof node.closest === "function" ? node.closest(selector) : null;
276
+ if (found) return found;
277
+ const root = node.getRootNode();
278
+ node = root && root !== node && typeof root.host === "object" ? root.host ?? null : null;
279
+ }
280
+ } catch {
281
+ }
282
+ return null;
283
+ }
284
+
285
+ // src/ui/components/vio-product.ts
168
286
  var VioProduct = class extends LitElement {
169
287
  constructor() {
170
288
  super(...arguments);
@@ -175,6 +293,13 @@ var VioProduct = class extends LitElement {
175
293
  this.imageUrl = "";
176
294
  this.productId = "";
177
295
  this.sponsorId = "";
296
+ this.vioSlot = "";
297
+ this.vioSlotName = "";
298
+ this.vioScope = "";
299
+ this.vioScopeName = "";
300
+ this.vioContentId = "";
301
+ this.vioContentTitle = "";
302
+ this.slotRegistration = new SlotRegistration(this, "card");
178
303
  this.hasVariants = false;
179
304
  this.variants = void 0;
180
305
  this.options = void 0;
@@ -184,6 +309,36 @@ var VioProduct = class extends LitElement {
184
309
  this.justAdded = false;
185
310
  this.boundCartChange = () => this.updateInCart();
186
311
  }
312
+ /** The slot id in effect: `vio-slot`, else the auto id (`card-N`). Read-only. */
313
+ get effectiveSlot() {
314
+ return this.slotRegistration.effective;
315
+ }
316
+ /** The slot campaign's sponsor as the string the attributes use ('' when unknown). */
317
+ sponsorFromAttribution() {
318
+ const id = Vio.attribution(this.effectiveSlot).sponsorId;
319
+ return id !== void 0 ? String(id) : "";
320
+ }
321
+ slotAttributes() {
322
+ return {
323
+ vioSlot: this.vioSlot,
324
+ vioSlotName: this.vioSlotName,
325
+ vioScope: this.vioScope,
326
+ vioScopeName: this.vioScopeName
327
+ };
328
+ }
329
+ /**
330
+ * What every event carries for attribution (`Vio.attribution`): the place,
331
+ * its campaign and sponsor, and the content — overridden here when the host
332
+ * set `vio-content-id` / `vio-content-title` on this card.
333
+ */
334
+ attribution() {
335
+ const base = Vio.attribution(this.effectiveSlot, { sponsorId: this.sponsorId || void 0 });
336
+ return {
337
+ ...base,
338
+ ...this.vioContentId ? { contentId: this.vioContentId } : {},
339
+ ...this.vioContentTitle ? { contentTitle: this.vioContentTitle } : {}
340
+ };
341
+ }
187
342
  static {
188
343
  this.styles = css`
189
344
  :host {
@@ -297,14 +452,19 @@ var VioProduct = class extends LitElement {
297
452
  connectedCallback() {
298
453
  super.connectedCallback();
299
454
  this.updateInCart();
455
+ this.slotRegistration.connect(this.slotAttributes());
300
456
  Vio.cart.addEventListener("change", this.boundCartChange);
301
457
  }
302
458
  disconnectedCallback() {
303
459
  Vio.cart.removeEventListener("change", this.boundCartChange);
460
+ this.slotRegistration.clear();
304
461
  super.disconnectedCallback();
305
462
  }
306
463
  updated(changed) {
307
464
  if (changed.has("productId") || changed.has("sponsorId")) this.updateInCart();
465
+ if (changed.has("vioSlot") || changed.has("vioSlotName") || changed.has("vioScope") || changed.has("vioScopeName")) {
466
+ this.slotRegistration.sync(this.slotAttributes());
467
+ }
308
468
  }
309
469
  /** Reflect whether (and how many units of) this product sit in the cart. */
310
470
  updateInCart() {
@@ -330,12 +490,14 @@ var VioProduct = class extends LitElement {
330
490
  composed: true,
331
491
  detail: {
332
492
  productId: this.productId,
333
- sponsorId: this.sponsorId,
334
493
  brand: this.brand,
335
494
  name: this.name,
336
495
  price: this.price,
337
496
  retailer: this.retailer,
338
- imageUrl: this.imageUrl
497
+ imageUrl: this.imageUrl,
498
+ ...this.attribution(),
499
+ // The host's sponsor-id wins; without one, the slot's campaign sponsor.
500
+ sponsorId: this.sponsorId || this.sponsorFromAttribution()
339
501
  }
340
502
  })
341
503
  );
@@ -355,18 +517,28 @@ var VioProduct = class extends LitElement {
355
517
  this.onClick();
356
518
  return;
357
519
  }
520
+ const attribution = this.attribution();
358
521
  this.dispatchEvent(
359
522
  new CustomEvent("vio:product-add", {
360
523
  bubbles: true,
361
524
  composed: true,
362
525
  detail: {
363
526
  productId: this.productId,
364
- sponsorId: this.sponsorId,
365
527
  brand: this.brand,
366
528
  name: this.name,
367
529
  price: this.price,
368
530
  retailer: this.retailer,
369
- imageUrl: this.imageUrl
531
+ imageUrl: this.imageUrl,
532
+ ...attribution,
533
+ // The host's sponsor-id wins; without one, the slot's campaign sponsor.
534
+ sponsorId: this.sponsorId || this.sponsorFromAttribution(),
535
+ // The host adds to the cart: pass this through as `origin` so the
536
+ // purchase is attributed to this place and campaign.
537
+ origin: {
538
+ campaignId: attribution.campaignId,
539
+ slot: attribution.slot,
540
+ contentId: attribution.contentId
541
+ }
370
542
  }
371
543
  })
372
544
  );
@@ -434,6 +606,24 @@ __decorateClass([
434
606
  __decorateClass([
435
607
  property({ type: String, attribute: "sponsor-id" })
436
608
  ], VioProduct.prototype, "sponsorId", 2);
609
+ __decorateClass([
610
+ property({ type: String, attribute: "vio-slot" })
611
+ ], VioProduct.prototype, "vioSlot", 2);
612
+ __decorateClass([
613
+ property({ type: String, attribute: "vio-slot-name" })
614
+ ], VioProduct.prototype, "vioSlotName", 2);
615
+ __decorateClass([
616
+ property({ type: String, attribute: "vio-scope" })
617
+ ], VioProduct.prototype, "vioScope", 2);
618
+ __decorateClass([
619
+ property({ type: String, attribute: "vio-scope-name" })
620
+ ], VioProduct.prototype, "vioScopeName", 2);
621
+ __decorateClass([
622
+ property({ type: String, attribute: "vio-content-id" })
623
+ ], VioProduct.prototype, "vioContentId", 2);
624
+ __decorateClass([
625
+ property({ type: String, attribute: "vio-content-title" })
626
+ ], VioProduct.prototype, "vioContentTitle", 2);
437
627
  __decorateClass([
438
628
  property({ type: Boolean, attribute: "has-variants" })
439
629
  ], VioProduct.prototype, "hasVariants", 2);
@@ -468,6 +658,13 @@ var VioProductCarousel = class extends LitElement {
468
658
  this.sponsorId = 0;
469
659
  this.productRefs = "";
470
660
  this.currency = "NOK";
661
+ this.vioSlot = "";
662
+ this.vioSlotName = "";
663
+ this.vioScope = "";
664
+ this.vioScopeName = "";
665
+ this.vioContentId = "";
666
+ this.vioContentTitle = "";
667
+ this.slotRegistration = new SlotRegistration(this, "carousel");
471
668
  this.fetched = null;
472
669
  this.isLoading = false;
473
670
  this.fetchError = "";
@@ -479,6 +676,27 @@ var VioProductCarousel = class extends LitElement {
479
676
  this.impressed = false;
480
677
  this.listTracked = false;
481
678
  }
679
+ /** The slot id in effect: `vio-slot`, else the auto id (`carousel-N`). Read-only. */
680
+ get effectiveSlot() {
681
+ return this.slotRegistration.effective;
682
+ }
683
+ slotAttributes() {
684
+ return {
685
+ vioSlot: this.vioSlot,
686
+ vioSlotName: this.vioSlotName,
687
+ vioScope: this.vioScope,
688
+ vioScopeName: this.vioScopeName
689
+ };
690
+ }
691
+ /** Attribution every analytics event of this carousel carries (`Vio.attribution` + the content overrides). */
692
+ attribution() {
693
+ const base = Vio.attribution(this.effectiveSlot, { sponsorId: this.sponsorId > 0 ? this.sponsorId : void 0 });
694
+ return {
695
+ ...base,
696
+ ...this.vioContentId ? { contentId: this.vioContentId } : {},
697
+ ...this.vioContentTitle ? { contentTitle: this.vioContentTitle } : {}
698
+ };
699
+ }
482
700
  static {
483
701
  this.styles = css`
484
702
  :host {
@@ -626,6 +844,7 @@ var VioProductCarousel = class extends LitElement {
626
844
  }
627
845
  connectedCallback() {
628
846
  super.connectedCallback();
847
+ this.slotRegistration.connect(this.slotAttributes());
629
848
  this.observeForAnalytics();
630
849
  if (this.productRefs.trim() || this.productIds.trim()) {
631
850
  void this.autoFetch();
@@ -634,14 +853,28 @@ var VioProductCarousel = class extends LitElement {
634
853
  disconnectedCallback() {
635
854
  this.unobserveImpression?.();
636
855
  this.unobserveImpression = null;
856
+ this.slotRegistration.clear();
637
857
  super.disconnectedCallback();
638
858
  }
639
859
  updated(changed) {
640
- const propChanged = changed.has("productRefs") || changed.has("productIds") || changed.has("commerceKey") || changed.has("sponsorId");
860
+ if (changed.has("vioSlot") || changed.has("vioSlotName") || changed.has("vioScope") || changed.has("vioScopeName")) {
861
+ this.slotRegistration.sync(this.slotAttributes());
862
+ }
863
+ const propChanged = changed.has("productRefs") || changed.has("productIds") || changed.has("commerceKey") || changed.has("sponsorId") || changed.has("vioSlot");
641
864
  if (propChanged && (this.productRefs.trim() || this.productIds.trim())) {
642
865
  void this.autoFetch();
643
866
  }
644
867
  }
868
+ /**
869
+ * The sponsor of this carousel: the host's `sponsor-id`, else the primary
870
+ * sponsor of the campaign the slot shows (ADR-0034 — "the campaign of the
871
+ * block is its sponsor"), else 0 (unknown).
872
+ */
873
+ effectiveSponsorId() {
874
+ if (this.sponsorId > 0) return this.sponsorId;
875
+ const fromSlot = Number(Vio.campaignForSlot(this.effectiveSlot || null)?.primarySponsor?.id);
876
+ return Number.isFinite(fromSlot) && fromSlot > 0 ? fromSlot : 0;
877
+ }
645
878
  async autoFetch() {
646
879
  if (!Vio.isInitialized) {
647
880
  this.fetchError = "Vio not initialized \u2014 call Vio.init({ apiKey }) first";
@@ -650,7 +883,14 @@ var VioProductCarousel = class extends LitElement {
650
883
  this.isLoading = true;
651
884
  this.fetchError = "";
652
885
  try {
653
- const refs = this.productRefs.trim() ? this.parseRefs(this.productRefs) : this.productIds.split(",").map((s) => parseInt(s.trim(), 10)).filter((n) => !Number.isNaN(n)).map((id) => ({ sponsorId: this.sponsorId, productId: id }));
886
+ if (this.productRefs.trim() === "" && !this.commerceKey && this.sponsorId <= 0 && !Vio.bootstrapCache) {
887
+ try {
888
+ await Vio.bootstrap();
889
+ } catch {
890
+ }
891
+ }
892
+ const sponsorId = this.effectiveSponsorId();
893
+ const refs = this.productRefs.trim() ? this.parseRefs(this.productRefs) : this.productIds.split(",").map((s) => parseInt(s.trim(), 10)).filter((n) => !Number.isNaN(n)).map((id) => ({ sponsorId, productId: id }));
654
894
  if (refs.length === 0) {
655
895
  this.fetched = [];
656
896
  return;
@@ -662,23 +902,23 @@ var VioProductCarousel = class extends LitElement {
662
902
  bySponsor.set(r.sponsorId, arr);
663
903
  }
664
904
  const fetched = await Promise.all(
665
- Array.from(bySponsor.entries()).map(async ([sponsorId, ids]) => {
666
- const commerce = this.commerceKey && this.productRefs.trim() === "" ? Vio.commerceWithKey(this.commerceKey) : Vio.commerceFor(sponsorId);
905
+ Array.from(bySponsor.entries()).map(async ([sponsorId2, ids]) => {
906
+ const commerce = this.commerceKey && this.productRefs.trim() === "" ? Vio.commerceWithKey(this.commerceKey) : Vio.commerceFor(sponsorId2);
667
907
  const products = await commerce.channel.product.getByIds({
668
908
  product_ids: ids,
669
909
  currency: this.currency,
670
910
  image_size: "large"
671
911
  });
672
- return { sponsorId, products };
912
+ return { sponsorId: sponsorId2, products };
673
913
  })
674
914
  );
675
915
  const lookup = /* @__PURE__ */ new Map();
676
- for (const { sponsorId, products } of fetched) {
677
- for (const p of products) lookup.set(`${sponsorId}:${p.id}`, p);
916
+ for (const { sponsorId: sponsorId2, products } of fetched) {
917
+ for (const p of products) lookup.set(`${sponsorId2}:${p.id}`, p);
678
918
  }
679
- this.fetched = refs.map(({ sponsorId, productId }) => {
680
- const product = lookup.get(`${sponsorId}:${productId}`);
681
- return product ? { product, sponsorId } : null;
919
+ this.fetched = refs.map(({ sponsorId: sponsorId2, productId }) => {
920
+ const product = lookup.get(`${sponsorId2}:${productId}`);
921
+ return product ? { product, sponsorId: sponsorId2 } : null;
682
922
  }).filter((x) => x !== null);
683
923
  this.maybeTrackList();
684
924
  } catch (err) {
@@ -695,10 +935,12 @@ var VioProductCarousel = class extends LitElement {
695
935
  {
696
936
  sponsorId: this.sponsorId > 0 ? this.sponsorId : void 0,
697
937
  componentTemplateId: "product-carousel",
698
- locationId: this.label || void 0
938
+ // The slot is the real location id; the label only stands in for it.
939
+ locationId: this.effectiveSlot || this.label || void 0,
940
+ ...this.attribution()
699
941
  },
700
942
  {
701
- key: `carousel:${this.label || this.productIds || this.productRefs || "default"}`,
943
+ key: `carousel:${this.effectiveSlot || this.label || this.productIds || this.productRefs || "default"}`,
702
944
  onImpression: () => {
703
945
  this.impressed = true;
704
946
  this.maybeTrackList();
@@ -716,14 +958,16 @@ var VioProductCarousel = class extends LitElement {
716
958
  if (entries.length === 0) return;
717
959
  this.listTracked = true;
718
960
  try {
961
+ const sponsorId = this.effectiveSponsorId();
719
962
  Vio.analytics.track("view_item_list", {
720
963
  context: {
721
- sponsorId: this.sponsorId > 0 ? this.sponsorId : void 0,
964
+ sponsorId: sponsorId > 0 ? sponsorId : void 0,
722
965
  componentTemplateId: "product-carousel",
723
- locationId: this.label || void 0
966
+ locationId: this.effectiveSlot || this.label || void 0,
967
+ ...this.attribution()
724
968
  },
725
969
  commerce: {
726
- items: entries.map(({ product, sponsorId }) => ({
970
+ items: entries.map(({ product, sponsorId: sponsorId2 }) => ({
727
971
  productId: product.id,
728
972
  name: product.title,
729
973
  price: Number(product.price?.amount_incl_taxes ?? product.price?.amount) || void 0
@@ -776,6 +1020,9 @@ var VioProductCarousel = class extends LitElement {
776
1020
  retailer=${retailer}
777
1021
  image-url=${img}
778
1022
  ?has-variants=${(p.variants?.length ?? 0) > 1}
1023
+ .vioSlot=${this.effectiveSlot}
1024
+ .vioContentId=${this.vioContentId}
1025
+ .vioContentTitle=${this.vioContentTitle}
779
1026
  ></vio-product>
780
1027
  `;
781
1028
  })}`;
@@ -845,6 +1092,24 @@ __decorateClass([
845
1092
  __decorateClass([
846
1093
  property({ type: String })
847
1094
  ], VioProductCarousel.prototype, "currency", 2);
1095
+ __decorateClass([
1096
+ property({ type: String, attribute: "vio-slot" })
1097
+ ], VioProductCarousel.prototype, "vioSlot", 2);
1098
+ __decorateClass([
1099
+ property({ type: String, attribute: "vio-slot-name" })
1100
+ ], VioProductCarousel.prototype, "vioSlotName", 2);
1101
+ __decorateClass([
1102
+ property({ type: String, attribute: "vio-scope" })
1103
+ ], VioProductCarousel.prototype, "vioScope", 2);
1104
+ __decorateClass([
1105
+ property({ type: String, attribute: "vio-scope-name" })
1106
+ ], VioProductCarousel.prototype, "vioScopeName", 2);
1107
+ __decorateClass([
1108
+ property({ type: String, attribute: "vio-content-id" })
1109
+ ], VioProductCarousel.prototype, "vioContentId", 2);
1110
+ __decorateClass([
1111
+ property({ type: String, attribute: "vio-content-title" })
1112
+ ], VioProductCarousel.prototype, "vioContentTitle", 2);
848
1113
  __decorateClass([
849
1114
  query(".scroller")
850
1115
  ], VioProductCarousel.prototype, "scroller", 2);
@@ -1083,6 +1348,17 @@ var VioProductDetail = class extends LitElement {
1083
1348
  this.sponsorId = 0;
1084
1349
  this.autoOpen = true;
1085
1350
  this.currency = "NOK";
1351
+ this.vioSlot = "";
1352
+ this.vioSlotName = "";
1353
+ this.vioScope = "";
1354
+ this.vioScopeName = "";
1355
+ this.vioContentId = "";
1356
+ this.vioContentTitle = "";
1357
+ this.slotRegistration = new SlotRegistration(this, "detail");
1358
+ /** What the click that opened the detail carried ('' when nothing). */
1359
+ this.eventSlot = "";
1360
+ this.eventContentId = "";
1361
+ this.eventContentTitle = "";
1086
1362
  this.product = null;
1087
1363
  this.sellerName = null;
1088
1364
  this.sellerSettled = false;
@@ -1113,8 +1389,16 @@ var VioProductDetail = class extends LitElement {
1113
1389
  const ev = e;
1114
1390
  const productIdNum = parseInt(ev.detail.productId, 10);
1115
1391
  if (Number.isNaN(productIdNum)) return;
1116
- const sponsorIdNum = parseInt(ev.detail.sponsorId, 10);
1117
- if (Number.isNaN(sponsorIdNum) || sponsorIdNum <= 0) return;
1392
+ const slot = typeof ev.detail.slot === "string" ? ev.detail.slot.trim() : "";
1393
+ let sponsorIdNum = parseInt(ev.detail.sponsorId, 10);
1394
+ if (Number.isNaN(sponsorIdNum) || sponsorIdNum <= 0) {
1395
+ const fromSlot = Number(Vio.campaignForSlot(slot || this.effectiveSlot || null)?.primarySponsor?.id);
1396
+ if (!Number.isFinite(fromSlot) || fromSlot <= 0) return;
1397
+ sponsorIdNum = fromSlot;
1398
+ }
1399
+ this.eventSlot = slot;
1400
+ this.eventContentId = ev.detail.contentId !== void 0 && ev.detail.contentId !== null ? String(ev.detail.contentId).trim() : "";
1401
+ this.eventContentTitle = typeof ev.detail.contentTitle === "string" ? ev.detail.contentTitle.trim() : "";
1118
1402
  this.productId = ev.detail.productId;
1119
1403
  this.sponsorId = sponsorIdNum;
1120
1404
  this.currency = ev.detail.currency || getGlobalCurrency();
@@ -1122,6 +1406,33 @@ var VioProductDetail = class extends LitElement {
1122
1406
  void this.fetchProduct();
1123
1407
  };
1124
1408
  }
1409
+ /** The slot id in effect for this element: `vio-slot`, else the auto id (`detail-N`). Read-only. */
1410
+ get effectiveSlot() {
1411
+ return this.slotRegistration.effective;
1412
+ }
1413
+ /** The slot the open product is attributed to: the opening card's, else this element's. */
1414
+ get activeSlot() {
1415
+ return this.eventSlot || this.effectiveSlot;
1416
+ }
1417
+ slotAttributes() {
1418
+ return {
1419
+ vioSlot: this.vioSlot,
1420
+ vioSlotName: this.vioSlotName,
1421
+ vioScope: this.vioScope,
1422
+ vioScopeName: this.vioScopeName
1423
+ };
1424
+ }
1425
+ /** Attribution every event of the open product carries (`Vio.attribution` of the opening card's slot, else this element's). */
1426
+ attribution() {
1427
+ const base = Vio.attribution(this.activeSlot, { sponsorId: this.sponsorId > 0 ? this.sponsorId : void 0 });
1428
+ const contentId = this.eventContentId || this.vioContentId;
1429
+ const contentTitle = this.eventContentTitle || this.vioContentTitle;
1430
+ return {
1431
+ ...base,
1432
+ ...contentId ? { contentId } : {},
1433
+ ...contentTitle ? { contentTitle } : {}
1434
+ };
1435
+ }
1125
1436
  static {
1126
1437
  this.styles = css`
1127
1438
  :host { display: contents; }
@@ -1569,15 +1880,20 @@ var VioProductDetail = class extends LitElement {
1569
1880
  }
1570
1881
  connectedCallback() {
1571
1882
  super.connectedCallback();
1883
+ this.slotRegistration.connect(this.slotAttributes());
1572
1884
  void this.loadVippsButton();
1573
1885
  document.addEventListener("vio:product-click", this.boundProductClick);
1574
1886
  }
1575
1887
  disconnectedCallback() {
1576
1888
  document.removeEventListener("vio:product-click", this.boundProductClick);
1889
+ this.slotRegistration.clear();
1577
1890
  super.disconnectedCallback();
1578
1891
  }
1579
1892
  updated(changed) {
1580
1893
  if (changed.has("open")) this.dialogFocus.update(this.open);
1894
+ if (changed.has("vioSlot") || changed.has("vioSlotName") || changed.has("vioScope") || changed.has("vioScopeName")) {
1895
+ this.slotRegistration.sync(this.slotAttributes());
1896
+ }
1581
1897
  }
1582
1898
  show() {
1583
1899
  this.open = true;
@@ -1644,7 +1960,8 @@ var VioProductDetail = class extends LitElement {
1644
1960
  Vio.analytics.track("view_item", {
1645
1961
  context: {
1646
1962
  sponsorId: this.sponsorId > 0 ? Number(this.sponsorId) : void 0,
1647
- componentTemplateId: "product-detail"
1963
+ componentTemplateId: "product-detail",
1964
+ ...this.attribution()
1648
1965
  },
1649
1966
  commerce: {
1650
1967
  items: [
@@ -1684,7 +2001,7 @@ var VioProductDetail = class extends LitElement {
1684
2001
  }
1685
2002
  /** Which buy buttons to render — backend-configured per sponsor. */
1686
2003
  async loadAvailablePaymentMethods() {
1687
- const spId = this.sponsorId || globalThis.__VIO_SPONSOR_ID__;
2004
+ const spId = this.sponsorId || Number(Vio.campaignForSlot(this.activeSlot || null)?.primarySponsor?.id) || globalThis.__VIO_SPONSOR_ID__;
1688
2005
  if (!spId) return;
1689
2006
  try {
1690
2007
  const methods = await Vio.checkout.getAvailablePaymentMethods(spId);
@@ -1966,12 +2283,14 @@ var VioProductDetail = class extends LitElement {
1966
2283
  const sponsor = Vio.bootstrapCache?.primarySponsor;
1967
2284
  const secondaries = Vio.bootstrapCache?.secondarySponsors ?? [];
1968
2285
  const sponsorName = sponsor?.id === this.sponsorId ? sponsor.name : secondaries.find((s) => s.id === this.sponsorId)?.name;
2286
+ const { campaignId, slot, contentId } = this.attribution();
1969
2287
  Vio.cart.addProduct({
1970
2288
  product: this.product,
1971
2289
  sponsorId: this.sponsorId,
1972
2290
  sponsorName,
1973
2291
  quantity: this.quantity,
1974
- variantId: variant?.id
2292
+ variantId: variant?.id,
2293
+ origin: { campaignId, slot, contentId }
1975
2294
  });
1976
2295
  return variant?.id;
1977
2296
  }
@@ -1988,7 +2307,8 @@ var VioProductDetail = class extends LitElement {
1988
2307
  productId: this.product.id,
1989
2308
  sponsorId: this.sponsorId,
1990
2309
  quantity: this.quantity,
1991
- variantId
2310
+ variantId,
2311
+ ...this.attribution()
1992
2312
  }
1993
2313
  })
1994
2314
  );
@@ -2013,7 +2333,7 @@ var VioProductDetail = class extends LitElement {
2013
2333
  new CustomEvent("vio:checkout-open", {
2014
2334
  bubbles: true,
2015
2335
  composed: true,
2016
- detail: { sponsorId: this.sponsorId, paymentMethod: "klarna", express: true }
2336
+ detail: { sponsorId: this.sponsorId, paymentMethod: "klarna", express: true, ...this.attribution() }
2017
2337
  })
2018
2338
  );
2019
2339
  } finally {
@@ -2043,7 +2363,7 @@ var VioProductDetail = class extends LitElement {
2043
2363
  new CustomEvent("vio:checkout-open", {
2044
2364
  bubbles: true,
2045
2365
  composed: true,
2046
- detail: { sponsorId: this.sponsorId, express: false }
2366
+ detail: { sponsorId: this.sponsorId, express: false, ...this.attribution() }
2047
2367
  })
2048
2368
  );
2049
2369
  }
@@ -2079,7 +2399,7 @@ var VioProductDetail = class extends LitElement {
2079
2399
  new CustomEvent("vio:checkout-open", {
2080
2400
  bubbles: true,
2081
2401
  composed: true,
2082
- detail: { sponsorId: this.sponsorId, paymentMethod: "vipps", express: false }
2402
+ detail: { sponsorId: this.sponsorId, paymentMethod: "vipps", express: false, ...this.attribution() }
2083
2403
  })
2084
2404
  );
2085
2405
  }
@@ -2092,7 +2412,7 @@ var VioProductDetail = class extends LitElement {
2092
2412
  new CustomEvent("vio:checkout-open", {
2093
2413
  bubbles: true,
2094
2414
  composed: true,
2095
- detail: { sponsorId: this.sponsorId, paymentMethod: "stripe", express: false }
2415
+ detail: { sponsorId: this.sponsorId, paymentMethod: "stripe", express: false, ...this.attribution() }
2096
2416
  })
2097
2417
  );
2098
2418
  }
@@ -2375,6 +2695,24 @@ __decorateClass([
2375
2695
  __decorateClass([
2376
2696
  property({ type: String })
2377
2697
  ], VioProductDetail.prototype, "currency", 2);
2698
+ __decorateClass([
2699
+ property({ type: String, attribute: "vio-slot" })
2700
+ ], VioProductDetail.prototype, "vioSlot", 2);
2701
+ __decorateClass([
2702
+ property({ type: String, attribute: "vio-slot-name" })
2703
+ ], VioProductDetail.prototype, "vioSlotName", 2);
2704
+ __decorateClass([
2705
+ property({ type: String, attribute: "vio-scope" })
2706
+ ], VioProductDetail.prototype, "vioScope", 2);
2707
+ __decorateClass([
2708
+ property({ type: String, attribute: "vio-scope-name" })
2709
+ ], VioProductDetail.prototype, "vioScopeName", 2);
2710
+ __decorateClass([
2711
+ property({ type: String, attribute: "vio-content-id" })
2712
+ ], VioProductDetail.prototype, "vioContentId", 2);
2713
+ __decorateClass([
2714
+ property({ type: String, attribute: "vio-content-title" })
2715
+ ], VioProductDetail.prototype, "vioContentTitle", 2);
2378
2716
  __decorateClass([
2379
2717
  state()
2380
2718
  ], VioProductDetail.prototype, "product", 2);
@@ -3001,6 +3339,7 @@ var VioCart = class extends LitElement {
3001
3339
  async startVippsExpressOrCheckout(sponsorId) {
3002
3340
  let fallback = true;
3003
3341
  try {
3342
+ await Vio.cart.waitForCartMutations(sponsorId);
3004
3343
  if (!await Vio.checkout.getVippsExpressEnabled(sponsorId)) throw new Error("express off");
3005
3344
  const res = await Vio.checkout.startVippsExpress(sponsorId);
3006
3345
  fallback = !res?.payment_url;
@@ -3042,11 +3381,14 @@ var VioCart = class extends LitElement {
3042
3381
  for (const cart of this.carts.values()) return cart.currency;
3043
3382
  return "NOK";
3044
3383
  }
3384
+ // A delta on the manager's live quantity, not a target computed from this
3385
+ // render's snapshot: two clicks before Lit re-renders used to ask for the
3386
+ // same quantity twice (Alan, 2026-10-15).
3045
3387
  onIncrement(item) {
3046
- Vio.cart.updateQuantity(item.id, item.sponsorId, item.quantity + 1);
3388
+ Vio.cart.adjustQuantity(item.id, item.sponsorId, 1);
3047
3389
  }
3048
3390
  onDecrement(item) {
3049
- Vio.cart.updateQuantity(item.id, item.sponsorId, item.quantity - 1);
3391
+ Vio.cart.adjustQuantity(item.id, item.sponsorId, -1);
3050
3392
  }
3051
3393
  onRemove(item) {
3052
3394
  Vio.cart.removeItem(item.id, item.sponsorId);
@@ -6665,5 +7007,5 @@ function registerVioElements() {
6665
7007
  }
6666
7008
 
6667
7009
  export { VioCart, VioCheckout, VioProduct, VioProductCarousel, VioProductDetail, VioTokens, applyVioTheme, registerVioElements };
6668
- //# sourceMappingURL=chunk-MRJR6DG5.js.map
6669
- //# sourceMappingURL=chunk-MRJR6DG5.js.map
7010
+ //# sourceMappingURL=chunk-COCHGJ2K.js.map
7011
+ //# sourceMappingURL=chunk-COCHGJ2K.js.map