@vio-live/web-sdk 0.19.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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-CMNTHF66.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);
@@ -885,6 +1150,192 @@ function isMethodEnabled(available, ...names) {
885
1150
  );
886
1151
  }
887
1152
 
1153
+ // src/ui/dialog-focus.ts
1154
+ var DialogFocus = class {
1155
+ constructor(opts) {
1156
+ this.opts = opts;
1157
+ this.opener = null;
1158
+ this.active = false;
1159
+ /** The element inside the dialog that had focus last — where a pull-back continues from. */
1160
+ this.lastInside = null;
1161
+ this.onKeydown = (e) => {
1162
+ if (e.defaultPrevented) return;
1163
+ if (e.key === "Escape" || e.key === "Esc") {
1164
+ if (this.opts.canClose && !this.opts.canClose()) return;
1165
+ e.preventDefault();
1166
+ e.stopPropagation();
1167
+ this.opts.close();
1168
+ return;
1169
+ }
1170
+ if (e.key !== "Tab" || e.altKey || e.ctrlKey || e.metaKey) return;
1171
+ const container = this.opts.container();
1172
+ if (!container) return;
1173
+ const items = tabbableIn(container);
1174
+ if (items.length === 0) {
1175
+ e.preventDefault();
1176
+ focusElement(container);
1177
+ return;
1178
+ }
1179
+ const active = deepActiveElement();
1180
+ const index = active ? items.indexOf(active) : -1;
1181
+ e.preventDefault();
1182
+ const count = items.length;
1183
+ const next = index === -1 ? e.shiftKey ? count - 1 : 0 : (index + (e.shiftKey ? -1 : 1) + count) % count;
1184
+ focusElement(items[next]);
1185
+ };
1186
+ /**
1187
+ * Focus moved somewhere. Inside the dialog: remember where. Into another
1188
+ * open Vio dialog: theirs. Onto the page: the browser walked out of the
1189
+ * dialog on its own (Safari leaving a widget iframe) — continue from the
1190
+ * element that had focus, backwards when the page element sits before
1191
+ * the host in document order.
1192
+ */
1193
+ this.onFocusIn = (e) => {
1194
+ if (!this.opts.host.isConnected) return;
1195
+ const path = e.composedPath();
1196
+ if (path.includes(this.opts.host)) {
1197
+ const active = deepActiveElement();
1198
+ this.lastInside = active instanceof HTMLElement ? active : null;
1199
+ return;
1200
+ }
1201
+ if (path.some((n) => isOpenVioDialog(n, this.opts.host))) return;
1202
+ const container = this.opts.container();
1203
+ if (!container) return;
1204
+ const items = tabbableIn(container);
1205
+ if (items.length === 0) {
1206
+ focusElement(container);
1207
+ return;
1208
+ }
1209
+ const target = e.target instanceof Node ? e.target : null;
1210
+ const backwards = !!target && precedes(target, this.opts.host);
1211
+ const index = this.lastInside ? items.indexOf(this.lastInside) : -1;
1212
+ const count = items.length;
1213
+ const next = backwards ? index <= 0 ? count - 1 : index - 1 : index === -1 || index === count - 1 ? 0 : index + 1;
1214
+ focusElement(items[next]);
1215
+ };
1216
+ /** Tab pressed while focus is on the page (or the body): enter the dialog. */
1217
+ this.onDocumentKeydown = (e) => {
1218
+ if (!this.opts.host.isConnected) return;
1219
+ if (e.defaultPrevented || e.key !== "Tab" || e.altKey || e.ctrlKey || e.metaKey) return;
1220
+ const active = deepActiveElement();
1221
+ if (active && isWithin(active, this.opts.host)) return;
1222
+ if (active && hasOpenVioDialogAbove(active, this.opts.host)) return;
1223
+ const container = this.opts.container();
1224
+ if (!container) return;
1225
+ e.preventDefault();
1226
+ e.stopPropagation();
1227
+ const items = tabbableIn(container);
1228
+ focusElement(items.length === 0 ? container : e.shiftKey ? items[items.length - 1] : items[0]);
1229
+ };
1230
+ }
1231
+ /**
1232
+ * Call from `updated()` when `open` changed. Lit reports `open` as changed
1233
+ * on the first render too (from undefined to its initial value): with the
1234
+ * same value twice this is a no-op, so that first call does nothing.
1235
+ */
1236
+ update(open) {
1237
+ if (open === this.active) return;
1238
+ this.active = open;
1239
+ if (open) this.opened();
1240
+ else this.closed();
1241
+ }
1242
+ opened() {
1243
+ this.opener = deepActiveElement();
1244
+ this.lastInside = null;
1245
+ this.opts.host.addEventListener("keydown", this.onKeydown);
1246
+ document.addEventListener("focusin", this.onFocusIn, true);
1247
+ document.addEventListener("keydown", this.onDocumentKeydown, true);
1248
+ const container = this.opts.container();
1249
+ if (container) focusInto(container);
1250
+ }
1251
+ closed() {
1252
+ this.opts.host.removeEventListener("keydown", this.onKeydown);
1253
+ document.removeEventListener("focusin", this.onFocusIn, true);
1254
+ document.removeEventListener("keydown", this.onDocumentKeydown, true);
1255
+ const opener = this.opener;
1256
+ this.opener = null;
1257
+ const active = deepActiveElement();
1258
+ if (!active || !isWithin(active, this.opts.host)) return;
1259
+ if (opener && opener.isConnected && isRestorable(opener)) {
1260
+ focusElement(opener);
1261
+ } else if (active instanceof HTMLElement) {
1262
+ active.blur();
1263
+ }
1264
+ }
1265
+ };
1266
+ function isOpenVioDialog(node, host) {
1267
+ return node instanceof Element && node !== host && node.tagName.startsWith("VIO-") && node.hasAttribute("open");
1268
+ }
1269
+ function hasOpenVioDialogAbove(node, host) {
1270
+ let n = node;
1271
+ while (n) {
1272
+ if (isOpenVioDialog(n, host)) return true;
1273
+ n = n.parentNode ?? (n instanceof ShadowRoot ? n.host : null);
1274
+ }
1275
+ return false;
1276
+ }
1277
+ function precedes(a, b) {
1278
+ return (a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING) !== 0;
1279
+ }
1280
+ function deepActiveElement() {
1281
+ if (typeof document === "undefined") return null;
1282
+ let el = document.activeElement;
1283
+ while (el?.shadowRoot?.activeElement) el = el.shadowRoot.activeElement;
1284
+ return el;
1285
+ }
1286
+ var TABBABLE_CANDIDATES = "a[href], area[href], button, input, select, textarea, iframe, summary, [tabindex], [contenteditable]";
1287
+ function tabbableIn(container) {
1288
+ const out = [];
1289
+ walkChildren(container, out);
1290
+ return out;
1291
+ }
1292
+ function walkChildren(parent, out) {
1293
+ for (const child of Array.from(parent.children)) {
1294
+ if (child instanceof HTMLSlotElement) {
1295
+ const assigned = child.assignedElements({ flatten: true });
1296
+ if (assigned.length > 0) {
1297
+ for (const el of assigned) visit(el, out);
1298
+ continue;
1299
+ }
1300
+ }
1301
+ visit(child, out);
1302
+ }
1303
+ }
1304
+ function visit(el, out) {
1305
+ if (isTabbable(el)) out.push(el);
1306
+ if (el.shadowRoot) walkChildren(el.shadowRoot, out);
1307
+ else walkChildren(el, out);
1308
+ }
1309
+ function isTabbable(el) {
1310
+ if (!(el instanceof HTMLElement) || !el.matches(TABBABLE_CANDIDATES)) return false;
1311
+ const tabindex = el.getAttribute("tabindex");
1312
+ if (tabindex !== null && Number(tabindex) < 0) return false;
1313
+ if (el.hasAttribute("disabled")) return false;
1314
+ if (el instanceof HTMLInputElement && el.type === "hidden") return false;
1315
+ if (el.getAttribute("contenteditable") === "false" && !el.matches("a[href], button, input, select, textarea, iframe, [tabindex]")) return false;
1316
+ if (el.hidden || el.getAttribute("aria-hidden") === "true") return false;
1317
+ if (typeof el.checkVisibility === "function" && !el.checkVisibility()) return false;
1318
+ return true;
1319
+ }
1320
+ function focusInto(container) {
1321
+ const first = tabbableIn(container)[0];
1322
+ focusElement(first ?? container);
1323
+ }
1324
+ function focusElement(el) {
1325
+ el.focus({ preventScroll: true });
1326
+ }
1327
+ function isRestorable(el) {
1328
+ return el instanceof HTMLElement && el !== document.body && el !== document.documentElement;
1329
+ }
1330
+ function isWithin(node, ancestor) {
1331
+ let n = node;
1332
+ while (n) {
1333
+ if (n === ancestor) return true;
1334
+ n = n.parentNode ?? (n instanceof ShadowRoot ? n.host : null);
1335
+ }
1336
+ return false;
1337
+ }
1338
+
888
1339
  // src/ui/components/vio-product-detail.ts
889
1340
  var STRIPE_LOGO_SRC = "data:image/svg+xml;utf8," + encodeURIComponent(
890
1341
  '<svg width="360" height="150" viewBox="0 0 360 150" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M360 77.4001C360 51.8001 347.6 31.6001 323.9 31.6001C300.1 31.6001 285.7 51.8001 285.7 77.2001C285.7 107.3 302.7 122.5 327.1 122.5C339 122.5 348 119.8 354.8 116V96.0001C348 99.4001 340.2 101.5 330.3 101.5C320.6 101.5 312 98.1001 310.9 86.3001H359.8C359.8 85.0001 360 79.8001 360 77.4001ZM310.6 67.9001C310.6 56.6001 317.5 51.9001 323.8 51.9001C329.9 51.9001 336.4 56.6001 336.4 67.9001H310.6Z" fill="#533AFD"/><path fill-rule="evenodd" clip-rule="evenodd" d="M247.1 31.6001C237.3 31.6001 231 36.2001 227.5 39.4001L226.2 33.2001H204.2V149.8L229.2 144.5L229.3 116.2C232.9 118.8 238.2 122.5 247 122.5C264.9 122.5 281.2 108.1 281.2 76.4001C281.1 47.4001 264.6 31.6001 247.1 31.6001ZM241.1 100.5C235.2 100.5 231.7 98.4001 229.3 95.8001L229.2 58.7001C231.8 55.8001 235.4 53.8001 241.1 53.8001C250.2 53.8001 256.5 64.0001 256.5 77.1001C256.5 90.5001 250.3 100.5 241.1 100.5Z" fill="#533AFD"/><path fill-rule="evenodd" clip-rule="evenodd" d="M169.8 25.7L194.9 20.3V0L169.8 5.3V25.7Z" fill="#533AFD"/><path d="M194.9 33.3H169.8V120.8H194.9V33.3Z" fill="#533AFD"/><path fill-rule="evenodd" clip-rule="evenodd" d="M142.9 40.7L141.3 33.3H119.7V120.8H144.7V61.5C150.6 53.8 160.6 55.2 163.7 56.3V33.3C160.5 32.1 148.8 29.9 142.9 40.7Z" fill="#533AFD"/><path fill-rule="evenodd" clip-rule="evenodd" d="M92.8999 11.6001L68.4999 16.8001L68.3999 96.9001C68.3999 111.7 79.4999 122.6 94.2999 122.6C102.5 122.6 108.5 121.1 111.8 119.3V99.0001C108.6 100.3 92.7999 104.9 92.7999 90.1001V54.6001H111.8V33.3001H92.7999L92.8999 11.6001Z" fill="#533AFD"/><path fill-rule="evenodd" clip-rule="evenodd" d="M25.3 58.7001C25.3 54.8001 28.5 53.3001 33.8 53.3001C41.4 53.3001 51 55.6001 58.6 59.7001V36.2001C50.3 32.9001 42.1 31.6001 33.8 31.6001C13.5 31.6001 0 42.2001 0 59.9001C0 87.5001 38 83.1001 38 95.0001C38 99.6001 34 101.1 28.4 101.1C20.1 101.1 9.5 97.7001 1.1 93.1001V116.9C10.4 120.9 19.8 122.6 28.4 122.6C49.2 122.6 63.5 112.3 63.5 94.4001C63.4 64.6001 25.3 69.9001 25.3 58.7001Z" fill="#533AFD"/></svg>'
@@ -897,6 +1348,17 @@ var VioProductDetail = class extends LitElement {
897
1348
  this.sponsorId = 0;
898
1349
  this.autoOpen = true;
899
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 = "";
900
1362
  this.product = null;
901
1363
  this.sellerName = null;
902
1364
  this.sellerSettled = false;
@@ -916,13 +1378,27 @@ var VioProductDetail = class extends LitElement {
916
1378
  this.optMapCache = /* @__PURE__ */ new WeakMap();
917
1379
  /** Pre-prepared Apple Pay handle — show() fires synchronously on tap. */
918
1380
  this.applePayHandle = null;
1381
+ /** Keyboard and focus while open: Escape closes, Tab stays inside, focus returns to the opener. */
1382
+ this.dialogFocus = new DialogFocus({
1383
+ host: this,
1384
+ container: () => this.renderRoot.querySelector(".modal"),
1385
+ close: () => this.close()
1386
+ });
919
1387
  this.boundProductClick = (e) => {
920
1388
  if (!this.autoOpen) return;
921
1389
  const ev = e;
922
1390
  const productIdNum = parseInt(ev.detail.productId, 10);
923
1391
  if (Number.isNaN(productIdNum)) return;
924
- const sponsorIdNum = parseInt(ev.detail.sponsorId, 10);
925
- 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() : "";
926
1402
  this.productId = ev.detail.productId;
927
1403
  this.sponsorId = sponsorIdNum;
928
1404
  this.currency = ev.detail.currency || getGlobalCurrency();
@@ -930,6 +1406,33 @@ var VioProductDetail = class extends LitElement {
930
1406
  void this.fetchProduct();
931
1407
  };
932
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
+ }
933
1436
  static {
934
1437
  this.styles = css`
935
1438
  :host { display: contents; }
@@ -959,6 +1462,8 @@ var VioProductDetail = class extends LitElement {
959
1462
  overflow-y: auto;
960
1463
  }
961
1464
  :host([open]) .modal { transform: translateY(0); }
1465
+ /* Focus lands on the panel itself only as a fallback — no ring for it. */
1466
+ .modal:focus { outline: none; }
962
1467
 
963
1468
  .handle { display: none; }
964
1469
 
@@ -967,7 +1472,7 @@ var VioProductDetail = class extends LitElement {
967
1472
  .modal {
968
1473
  top: auto;
969
1474
  height: 92vh;
970
- border-radius: 16px 16px 0 0;
1475
+ border-radius: var(--vio-radius-xl, 16px) var(--vio-radius-xl, 16px) 0 0;
971
1476
  box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.08);
972
1477
  }
973
1478
  .handle {
@@ -981,7 +1486,7 @@ var VioProductDetail = class extends LitElement {
981
1486
  width: 40px;
982
1487
  height: 4px;
983
1488
  background: var(--vio-color-border, #e5e5e5);
984
- border-radius: 2px;
1489
+ border-radius: var(--vio-radius-sm, 2px);
985
1490
  }
986
1491
  }
987
1492
 
@@ -1346,9 +1851,9 @@ var VioProductDetail = class extends LitElement {
1346
1851
  );
1347
1852
  background-size: 200% 100%;
1348
1853
  animation: vio-skeleton-shimmer 1.4s ease-in-out infinite;
1349
- border-radius: 8px;
1854
+ border-radius: var(--vio-radius-lg, 8px);
1350
1855
  }
1351
- .sk-image { width: 100%; aspect-ratio: 1; border-radius: 12px; }
1856
+ .sk-image { width: 100%; aspect-ratio: 1; border-radius: var(--vio-radius-lg, 12px); }
1352
1857
  .sk-line { height: 14px; }
1353
1858
  .sk-brand { width: 38%; height: 10px; margin-top: 6px; }
1354
1859
  .sk-title { width: 85%; height: 26px; }
@@ -1375,13 +1880,21 @@ var VioProductDetail = class extends LitElement {
1375
1880
  }
1376
1881
  connectedCallback() {
1377
1882
  super.connectedCallback();
1883
+ this.slotRegistration.connect(this.slotAttributes());
1378
1884
  void this.loadVippsButton();
1379
1885
  document.addEventListener("vio:product-click", this.boundProductClick);
1380
1886
  }
1381
1887
  disconnectedCallback() {
1382
1888
  document.removeEventListener("vio:product-click", this.boundProductClick);
1889
+ this.slotRegistration.clear();
1383
1890
  super.disconnectedCallback();
1384
1891
  }
1892
+ updated(changed) {
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
+ }
1897
+ }
1385
1898
  show() {
1386
1899
  this.open = true;
1387
1900
  }
@@ -1447,7 +1960,8 @@ var VioProductDetail = class extends LitElement {
1447
1960
  Vio.analytics.track("view_item", {
1448
1961
  context: {
1449
1962
  sponsorId: this.sponsorId > 0 ? Number(this.sponsorId) : void 0,
1450
- componentTemplateId: "product-detail"
1963
+ componentTemplateId: "product-detail",
1964
+ ...this.attribution()
1451
1965
  },
1452
1966
  commerce: {
1453
1967
  items: [
@@ -1487,7 +2001,7 @@ var VioProductDetail = class extends LitElement {
1487
2001
  }
1488
2002
  /** Which buy buttons to render — backend-configured per sponsor. */
1489
2003
  async loadAvailablePaymentMethods() {
1490
- 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__;
1491
2005
  if (!spId) return;
1492
2006
  try {
1493
2007
  const methods = await Vio.checkout.getAvailablePaymentMethods(spId);
@@ -1769,12 +2283,14 @@ var VioProductDetail = class extends LitElement {
1769
2283
  const sponsor = Vio.bootstrapCache?.primarySponsor;
1770
2284
  const secondaries = Vio.bootstrapCache?.secondarySponsors ?? [];
1771
2285
  const sponsorName = sponsor?.id === this.sponsorId ? sponsor.name : secondaries.find((s) => s.id === this.sponsorId)?.name;
2286
+ const { campaignId, slot, contentId } = this.attribution();
1772
2287
  Vio.cart.addProduct({
1773
2288
  product: this.product,
1774
2289
  sponsorId: this.sponsorId,
1775
2290
  sponsorName,
1776
2291
  quantity: this.quantity,
1777
- variantId: variant?.id
2292
+ variantId: variant?.id,
2293
+ origin: { campaignId, slot, contentId }
1778
2294
  });
1779
2295
  return variant?.id;
1780
2296
  }
@@ -1791,7 +2307,8 @@ var VioProductDetail = class extends LitElement {
1791
2307
  productId: this.product.id,
1792
2308
  sponsorId: this.sponsorId,
1793
2309
  quantity: this.quantity,
1794
- variantId
2310
+ variantId,
2311
+ ...this.attribution()
1795
2312
  }
1796
2313
  })
1797
2314
  );
@@ -1816,7 +2333,7 @@ var VioProductDetail = class extends LitElement {
1816
2333
  new CustomEvent("vio:checkout-open", {
1817
2334
  bubbles: true,
1818
2335
  composed: true,
1819
- detail: { sponsorId: this.sponsorId, paymentMethod: "klarna", express: true }
2336
+ detail: { sponsorId: this.sponsorId, paymentMethod: "klarna", express: true, ...this.attribution() }
1820
2337
  })
1821
2338
  );
1822
2339
  } finally {
@@ -1846,7 +2363,7 @@ var VioProductDetail = class extends LitElement {
1846
2363
  new CustomEvent("vio:checkout-open", {
1847
2364
  bubbles: true,
1848
2365
  composed: true,
1849
- detail: { sponsorId: this.sponsorId, express: false }
2366
+ detail: { sponsorId: this.sponsorId, express: false, ...this.attribution() }
1850
2367
  })
1851
2368
  );
1852
2369
  }
@@ -1882,7 +2399,7 @@ var VioProductDetail = class extends LitElement {
1882
2399
  new CustomEvent("vio:checkout-open", {
1883
2400
  bubbles: true,
1884
2401
  composed: true,
1885
- detail: { sponsorId: this.sponsorId, paymentMethod: "vipps", express: false }
2402
+ detail: { sponsorId: this.sponsorId, paymentMethod: "vipps", express: false, ...this.attribution() }
1886
2403
  })
1887
2404
  );
1888
2405
  }
@@ -1895,7 +2412,7 @@ var VioProductDetail = class extends LitElement {
1895
2412
  new CustomEvent("vio:checkout-open", {
1896
2413
  bubbles: true,
1897
2414
  composed: true,
1898
- detail: { sponsorId: this.sponsorId, paymentMethod: "stripe", express: false }
2415
+ detail: { sponsorId: this.sponsorId, paymentMethod: "stripe", express: false, ...this.attribution() }
1899
2416
  })
1900
2417
  );
1901
2418
  }
@@ -1938,7 +2455,7 @@ var VioProductDetail = class extends LitElement {
1938
2455
  render() {
1939
2456
  return html`
1940
2457
  <div class="backdrop" @click=${this.close}></div>
1941
- <div class="modal" role="dialog" aria-label="Produkt">
2458
+ <div class="modal" role="dialog" aria-modal="true" aria-label="Produkt" tabindex="-1">
1942
2459
  <div class="handle" @click=${this.close} aria-hidden="true"></div>
1943
2460
  <div class="topbar">
1944
2461
  <span class="topbar-brand">${this.headerName()}</span>
@@ -2178,6 +2695,24 @@ __decorateClass([
2178
2695
  __decorateClass([
2179
2696
  property({ type: String })
2180
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);
2181
2716
  __decorateClass([
2182
2717
  state()
2183
2718
  ], VioProductDetail.prototype, "product", 2);
@@ -2247,6 +2782,12 @@ var VioCart = class extends LitElement {
2247
2782
  this.preparedTotal = -1;
2248
2783
  /** In-flight guard for loadAvailablePaymentMethods. */
2249
2784
  this.loadingPaymentMethods = false;
2785
+ /** Keyboard and focus while open: Escape closes, Tab stays inside, focus returns to the opener. */
2786
+ this.dialogFocus = new DialogFocus({
2787
+ host: this,
2788
+ container: () => this.renderRoot.querySelector(".drawer"),
2789
+ close: () => this.close()
2790
+ });
2250
2791
  this.boundOnCartChange = (e) => {
2251
2792
  const detail = e.detail;
2252
2793
  this.carts = detail.cartsBySponsor;
@@ -2289,6 +2830,8 @@ var VioCart = class extends LitElement {
2289
2830
  box-shadow: -8px 0 24px rgba(0, 0, 0, 0.06);
2290
2831
  }
2291
2832
  :host([open]) .drawer { transform: translateX(0); }
2833
+ /* Focus lands on the drawer itself only as a fallback — no ring for it. */
2834
+ .drawer:focus { outline: none; }
2292
2835
 
2293
2836
  .handle { display: none; }
2294
2837
 
@@ -2301,7 +2844,7 @@ var VioCart = class extends LitElement {
2301
2844
  bottom: 0;
2302
2845
  width: 100%;
2303
2846
  height: 88vh;
2304
- border-radius: 16px 16px 0 0;
2847
+ border-radius: var(--vio-radius-xl, 16px) var(--vio-radius-xl, 16px) 0 0;
2305
2848
  transform: translateY(100%);
2306
2849
  box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.08);
2307
2850
  }
@@ -2316,7 +2859,7 @@ var VioCart = class extends LitElement {
2316
2859
  width: 40px;
2317
2860
  height: 4px;
2318
2861
  background: var(--vio-color-border, #e5e5e5);
2319
- border-radius: 2px;
2862
+ border-radius: var(--vio-radius-sm, 2px);
2320
2863
  }
2321
2864
  }
2322
2865
 
@@ -2409,7 +2952,7 @@ var VioCart = class extends LitElement {
2409
2952
  height: 24px;
2410
2953
  border: 1px solid var(--vio-color-border, #e5e5e5);
2411
2954
  background: #fff;
2412
- border-radius: 2px;
2955
+ border-radius: var(--vio-radius-sm, 2px);
2413
2956
  cursor: pointer;
2414
2957
  font-size: 12px;
2415
2958
  color: var(--vio-color-text, #0a0a0a);
@@ -2466,7 +3009,7 @@ var VioCart = class extends LitElement {
2466
3009
  background: #000;
2467
3010
  color: #fff;
2468
3011
  border: none;
2469
- border-radius: 8px;
3012
+ border-radius: var(--vio-radius-lg, 8px);
2470
3013
  font-size: 19px;
2471
3014
  font-weight: 500;
2472
3015
  line-height: 1;
@@ -2488,7 +3031,7 @@ var VioCart = class extends LitElement {
2488
3031
  background: #ffb3c7;
2489
3032
  color: #0a0a0a;
2490
3033
  border: none;
2491
- border-radius: 8px;
3034
+ border-radius: var(--vio-radius-lg, 8px);
2492
3035
  font-size: 16px;
2493
3036
  font-weight: 600;
2494
3037
  cursor: pointer;
@@ -2509,7 +3052,7 @@ var VioCart = class extends LitElement {
2509
3052
  background: #ff5b24;
2510
3053
  color: #fff;
2511
3054
  border: none;
2512
- border-radius: 8px;
3055
+ border-radius: var(--vio-radius-lg, 8px);
2513
3056
  cursor: pointer;
2514
3057
  display: flex;
2515
3058
  align-items: center;
@@ -2533,7 +3076,7 @@ var VioCart = class extends LitElement {
2533
3076
  background: #635bff;
2534
3077
  color: #fff;
2535
3078
  border: none;
2536
- border-radius: 8px;
3079
+ border-radius: var(--vio-radius-lg, 8px);
2537
3080
  font-size: 16px;
2538
3081
  font-weight: 600;
2539
3082
  cursor: pointer;
@@ -2671,6 +3214,7 @@ var VioCart = class extends LitElement {
2671
3214
  return isMethodEnabled(this.availableMethods, ...names);
2672
3215
  }
2673
3216
  updated(changed) {
3217
+ if (changed.has("open")) this.dialogFocus.update(this.open);
2674
3218
  if (changed.has("open") && this.open) {
2675
3219
  this.confirmedOrder = null;
2676
3220
  this.preparedTotal = -1;
@@ -2795,6 +3339,7 @@ var VioCart = class extends LitElement {
2795
3339
  async startVippsExpressOrCheckout(sponsorId) {
2796
3340
  let fallback = true;
2797
3341
  try {
3342
+ await Vio.cart.waitForCartMutations(sponsorId);
2798
3343
  if (!await Vio.checkout.getVippsExpressEnabled(sponsorId)) throw new Error("express off");
2799
3344
  const res = await Vio.checkout.startVippsExpress(sponsorId);
2800
3345
  fallback = !res?.payment_url;
@@ -2836,11 +3381,14 @@ var VioCart = class extends LitElement {
2836
3381
  for (const cart of this.carts.values()) return cart.currency;
2837
3382
  return "NOK";
2838
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).
2839
3387
  onIncrement(item) {
2840
- Vio.cart.updateQuantity(item.id, item.sponsorId, item.quantity + 1);
3388
+ Vio.cart.adjustQuantity(item.id, item.sponsorId, 1);
2841
3389
  }
2842
3390
  onDecrement(item) {
2843
- Vio.cart.updateQuantity(item.id, item.sponsorId, item.quantity - 1);
3391
+ Vio.cart.adjustQuantity(item.id, item.sponsorId, -1);
2844
3392
  }
2845
3393
  onRemove(item) {
2846
3394
  Vio.cart.removeItem(item.id, item.sponsorId);
@@ -2850,7 +3398,7 @@ var VioCart = class extends LitElement {
2850
3398
  const sponsorCarts = [...this.carts.values()].filter((c) => c.items.length > 0);
2851
3399
  return html`
2852
3400
  <div class="backdrop" @click=${this.close}></div>
2853
- <aside class="drawer" role="dialog" aria-label=${this.heading}>
3401
+ <aside class="drawer" role="dialog" aria-modal="true" aria-label=${this.heading} tabindex="-1">
2854
3402
  <div class="handle" @click=${this.close} aria-hidden="true"></div>
2855
3403
  <div class="header">
2856
3404
  <h2 class="heading">${this.confirmedOrder ? "Takk!" : this.heading}</h2>
@@ -3044,6 +3592,38 @@ __decorateClass([
3044
3592
  __decorateClass([
3045
3593
  state()
3046
3594
  ], VioCart.prototype, "confirmedOrder", 2);
3595
+
3596
+ // src/ui/checkout-errors.ts
3597
+ var EMPTY_CART_MESSAGE = "Handlekurven er tom. Legg til et produkt og pr\xF8v igjen.";
3598
+ var CART_CHANGED_MESSAGE = "Handlekurven ble endret i en annen fane. Last siden p\xE5 nytt og pr\xF8v igjen.";
3599
+ var CART_PAID_MESSAGE = "Denne handlekurven er allerede betalt. Start en ny handlekurv.";
3600
+ var PAYMENT_START_FAILED_MESSAGE = "Betalingen kunne ikke startes. Pr\xF8v igjen, eller velg en annen betalingsm\xE5te.";
3601
+ var SHIPS_APART = "kan ikke sendes sammen";
3602
+ function friendlyPaymentError(raw, method) {
3603
+ const text = String(raw ?? "");
3604
+ const lower = text.toLowerCase();
3605
+ if (lower.includes("got 0") || lower.includes("amount must be a positive")) {
3606
+ return EMPTY_CART_MESSAGE;
3607
+ }
3608
+ if (lower.includes("replaced by a newer") || text.includes("CHECKOUT_SUPERSEDED")) {
3609
+ return CART_CHANGED_MESSAGE;
3610
+ }
3611
+ if (lower.includes("already been paid") || text.includes("CART_ALREADY_PAID")) {
3612
+ return CART_PAID_MESSAGE;
3613
+ }
3614
+ if (text.includes(SHIPS_APART)) return norwegianSentence(text);
3615
+ return PAYMENT_START_FAILED_MESSAGE;
3616
+ }
3617
+ function norwegianSentence(text) {
3618
+ const at = text.indexOf(SHIPS_APART);
3619
+ const prefixEnd = text.lastIndexOf(": ", at);
3620
+ return (prefixEnd === -1 ? text : text.slice(prefixEnd + 2)).trim();
3621
+ }
3622
+ function errorText(err) {
3623
+ return err instanceof Error ? err.message : String(err);
3624
+ }
3625
+
3626
+ // src/ui/components/vio-checkout.ts
3047
3627
  var STRIPE_LOGO_SRC3 = "data:image/svg+xml;utf8," + encodeURIComponent(
3048
3628
  '<svg width="360" height="150" viewBox="0 0 360 150" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M360 77.4001C360 51.8001 347.6 31.6001 323.9 31.6001C300.1 31.6001 285.7 51.8001 285.7 77.2001C285.7 107.3 302.7 122.5 327.1 122.5C339 122.5 348 119.8 354.8 116V96.0001C348 99.4001 340.2 101.5 330.3 101.5C320.6 101.5 312 98.1001 310.9 86.3001H359.8C359.8 85.0001 360 79.8001 360 77.4001ZM310.6 67.9001C310.6 56.6001 317.5 51.9001 323.8 51.9001C329.9 51.9001 336.4 56.6001 336.4 67.9001H310.6Z" fill="#533AFD"/><path fill-rule="evenodd" clip-rule="evenodd" d="M247.1 31.6001C237.3 31.6001 231 36.2001 227.5 39.4001L226.2 33.2001H204.2V149.8L229.2 144.5L229.3 116.2C232.9 118.8 238.2 122.5 247 122.5C264.9 122.5 281.2 108.1 281.2 76.4001C281.1 47.4001 264.6 31.6001 247.1 31.6001ZM241.1 100.5C235.2 100.5 231.7 98.4001 229.3 95.8001L229.2 58.7001C231.8 55.8001 235.4 53.8001 241.1 53.8001C250.2 53.8001 256.5 64.0001 256.5 77.1001C256.5 90.5001 250.3 100.5 241.1 100.5Z" fill="#533AFD"/><path fill-rule="evenodd" clip-rule="evenodd" d="M169.8 25.7L194.9 20.3V0L169.8 5.3V25.7Z" fill="#533AFD"/><path d="M194.9 33.3H169.8V120.8H194.9V33.3Z" fill="#533AFD"/><path fill-rule="evenodd" clip-rule="evenodd" d="M142.9 40.7L141.3 33.3H119.7V120.8H144.7V61.5C150.6 53.8 160.6 55.2 163.7 56.3V33.3C160.5 32.1 148.8 29.9 142.9 40.7Z" fill="#533AFD"/><path fill-rule="evenodd" clip-rule="evenodd" d="M92.8999 11.6001L68.4999 16.8001L68.3999 96.9001C68.3999 111.7 79.4999 122.6 94.2999 122.6C102.5 122.6 108.5 121.1 111.8 119.3V99.0001C108.6 100.3 92.7999 104.9 92.7999 90.1001V54.6001H111.8V33.3001H92.7999L92.8999 11.6001Z" fill="#533AFD"/><path fill-rule="evenodd" clip-rule="evenodd" d="M25.3 58.7001C25.3 54.8001 28.5 53.3001 33.8 53.3001C41.4 53.3001 51 55.6001 58.6 59.7001V36.2001C50.3 32.9001 42.1 31.6001 33.8 31.6001C13.5 31.6001 0 42.2001 0 59.9001C0 87.5001 38 83.1001 38 95.0001C38 99.6001 34 101.1 28.4 101.1C20.1 101.1 9.5 97.7001 1.1 93.1001V116.9C10.4 120.9 19.8 122.6 28.4 122.6C49.2 122.6 63.5 112.3 63.5 94.4001C63.4 64.6001 25.3 69.9001 25.3 58.7001Z" fill="#533AFD"/></svg>'
3049
3629
  );
@@ -3155,6 +3735,16 @@ var _VioCheckout = class _VioCheckout extends LitElement {
3155
3735
  };
3156
3736
  /** In-flight guard for loadAvailablePaymentMethods. */
3157
3737
  this.loadingPaymentMethods = false;
3738
+ /**
3739
+ * Keyboard and focus while open: Escape closes — not while a payment is
3740
+ * in flight — Tab stays inside, focus returns to the opener.
3741
+ */
3742
+ this.dialogFocus = new DialogFocus({
3743
+ host: this,
3744
+ container: () => this.renderRoot.querySelector(".modal"),
3745
+ close: () => this.close(),
3746
+ canClose: () => !this.paymentInFlight
3747
+ });
3158
3748
  this.paymentMethodsResolved = false;
3159
3749
  /** Guards `autoSelectSoleMethod` to ONE attempt per opening — see there. */
3160
3750
  this.autoSelectAttempted = false;
@@ -3233,10 +3823,17 @@ var _VioCheckout = class _VioCheckout extends LitElement {
3233
3823
  };
3234
3824
  this.boundOnPaymentError = (e) => {
3235
3825
  const detail = e.detail;
3236
- this.paymentError = detail.method === "klarna" ? `Klarna-betalingen kunne ikke fullf\xF8res: ${detail.error}` : detail.error;
3826
+ if (typeof console !== "undefined") {
3827
+ console.warn("[VioCheckout] payment-error:", detail.method, detail.error);
3828
+ }
3829
+ this.paymentError = detail.method === "klarna" ? `Klarna-betalingen kunne ikke fullf\xF8res: ${detail.error}` : friendlyPaymentError(detail.error, detail.method);
3237
3830
  this.open = true;
3238
3831
  };
3239
3832
  }
3833
+ /** A payment is being started or confirmed right now: the overlay must not close under it. */
3834
+ get paymentInFlight() {
3835
+ return this.stripePaying || this.stripeLoading || this.vippsLoading || this.klarnaAuthorizing || this.applePayInProgress;
3836
+ }
3240
3837
  static {
3241
3838
  this.styles = css`
3242
3839
  :host { display: contents; }
@@ -3265,6 +3862,8 @@ var _VioCheckout = class _VioCheckout extends LitElement {
3265
3862
  overflow-y: auto;
3266
3863
  }
3267
3864
  :host([open]) .modal { transform: translateY(0); }
3865
+ /* Focus lands on the panel itself only as a fallback — no ring for it. */
3866
+ .modal:focus { outline: none; }
3268
3867
 
3269
3868
  /* Confirmation drawer — compact bottom sheet, not the full-screen overlay. */
3270
3869
  .modal.as-drawer {
@@ -3894,6 +4493,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
3894
4493
  const vippsStatus = await Vio.checkout.getVippsStatus(checkoutId, sponsorId || void 0);
3895
4494
  const state6 = String(vippsStatus?.state ?? "").toUpperCase();
3896
4495
  sawStatus = true;
4496
+ if (vippsStatus?.duplicate_of) return { outcome: "duplicate", sawStatus };
3897
4497
  if (state6 === "AUTHORIZED") return { outcome: "paid", sawStatus };
3898
4498
  if (state6 === "CREATED") continue;
3899
4499
  return { outcome: "failed", sawStatus };
@@ -3926,6 +4526,9 @@ var _VioCheckout = class _VioCheckout extends LitElement {
3926
4526
  detail: { method: vioMethod, sponsorId, orderId: checkoutId }
3927
4527
  })
3928
4528
  );
4529
+ } else if (outcome === "duplicate") {
4530
+ this.paymentError = CART_PAID_MESSAGE;
4531
+ this.open = true;
3929
4532
  } else if (outcome === "failed") {
3930
4533
  this.paymentError = "Betalingen ble avbrutt eller feilet. Vennligst pr\xF8v igjen.";
3931
4534
  this.open = true;
@@ -3996,7 +4599,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
3996
4599
  this.verifyingReturn = null;
3997
4600
  if (order?.status === "checkout_complete" && order.html_snippet) {
3998
4601
  const spId = this.checkoutState?.sponsorId ?? 0;
3999
- this.showProviderReceipt("kustom", spId, order);
4602
+ this.showProviderReceipt("kustom", spId, order, kustomReturn.checkoutId);
4000
4603
  await this.updateComplete;
4001
4604
  const container = this.lightContainer("vio-kustom-checkout-container", "vio-kustom");
4002
4605
  Vio.checkout.renderKustomSnippet(container, order.html_snippet);
@@ -4042,12 +4645,12 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4042
4645
  const result = { order: { orderId: order.order_id }, chargedTotal: order.total_price };
4043
4646
  this.qliroMountedOrderId = order.order_id;
4044
4647
  if (order.html_snippet) {
4045
- this.showProviderReceipt("qliro", spId, result);
4648
+ this.showProviderReceipt("qliro", spId, result, checkoutId);
4046
4649
  await this.updateComplete;
4047
4650
  const container = this.lightContainer("vio-qliro-checkout-container", "vio-qliro");
4048
4651
  Vio.checkout.renderKustomSnippet(container, order.html_snippet);
4049
4652
  } else {
4050
- this.confirmOrder("qliro", spId, result);
4653
+ this.confirmOrder("qliro", spId, result, checkoutId);
4051
4654
  }
4052
4655
  } else if (order?.html_snippet) {
4053
4656
  this.qliroMountedOrderId = order.order_id;
@@ -4098,9 +4701,12 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4098
4701
  Vio.checkout.renderKustomSnippet(container, order.html_snippet);
4099
4702
  }
4100
4703
  if (order.status === "PurchaseCompleted") {
4101
- this.showProviderReceipt("walley", this.checkoutState?.sponsorId ?? 0, {
4102
- orderId: order.order_id
4103
- });
4704
+ this.showProviderReceipt(
4705
+ "walley",
4706
+ this.checkoutState?.sponsorId ?? 0,
4707
+ { orderId: order.order_id },
4708
+ checkoutId
4709
+ );
4104
4710
  }
4105
4711
  }
4106
4712
  } catch (err) {
@@ -4196,6 +4802,10 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4196
4802
  const checkout = await Vio.checkout.getCheckout(checkoutId, sponsorId || void 0);
4197
4803
  const status = String(checkout?.status ?? "").toLowerCase();
4198
4804
  sawStatus = true;
4805
+ if (checkout?.duplicate_of) {
4806
+ outcome = "duplicate";
4807
+ break;
4808
+ }
4199
4809
  if (_VioCheckout.FAILED_STATUSES.has(status)) {
4200
4810
  outcome = "failed";
4201
4811
  break;
@@ -4260,6 +4870,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4260
4870
  super.disconnectedCallback();
4261
4871
  }
4262
4872
  updated(changed) {
4873
+ if (changed.has("open")) this.dialogFocus.update(this.open);
4263
4874
  if (changed.has("open") && this.open && this.items.length > 0 && this.availableShippingsList.length === 0) {
4264
4875
  void this.loadAvailableShippings();
4265
4876
  }
@@ -4518,7 +5129,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4518
5129
  if (typeof console !== "undefined") {
4519
5130
  console.warn("[VioCheckout] Klarna Payments mount failed:", err);
4520
5131
  }
4521
- this.paymentError = `Kunne ikke laste Klarna: ${err instanceof Error ? err.message : String(err)}`;
5132
+ this.paymentError = friendlyPaymentError(errorText(err));
4522
5133
  } finally {
4523
5134
  this.klarnaMounting = false;
4524
5135
  }
@@ -4701,7 +5312,10 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4701
5312
  }
4702
5313
  this.stripeLoading = true;
4703
5314
  void Vio.checkout.startStripePayment(this.checkoutState?.sponsorId, this.form).catch((err) => {
4704
- this.paymentError = `Kunne ikke starte Stripe-betaling: ${err instanceof Error ? err.message : String(err)}`;
5315
+ if (typeof console !== "undefined") {
5316
+ console.warn("[VioCheckout] Stripe payment could not start:", err);
5317
+ }
5318
+ this.paymentError = friendlyPaymentError(errorText(err));
4705
5319
  }).finally(() => {
4706
5320
  this.stripeLoading = false;
4707
5321
  });
@@ -4709,7 +5323,10 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4709
5323
  if (method === "vipps") {
4710
5324
  this.vippsLoading = true;
4711
5325
  void this.startVippsFromCheckout().catch((err) => {
4712
- this.paymentError = `Kunne ikke starte Vipps-betaling: ${err instanceof Error ? err.message : String(err)}`;
5326
+ if (typeof console !== "undefined") {
5327
+ console.warn("[VioCheckout] Vipps payment could not start:", err);
5328
+ }
5329
+ this.paymentError = friendlyPaymentError(errorText(err));
4713
5330
  }).finally(() => {
4714
5331
  this.vippsLoading = false;
4715
5332
  });
@@ -4744,11 +5361,11 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4744
5361
  * the confirmation screen, emits `vio:payment-success`, and clears the
4745
5362
  * paid sponsor's cart so the badge + drawer reflect the purchase.
4746
5363
  */
4747
- confirmOrder(method, sponsorId, result) {
5364
+ confirmOrder(method, sponsorId, result, returnCheckoutId) {
4748
5365
  const r = result ?? {};
4749
5366
  const shippingPrice = this.shippingMajor();
4750
5367
  const email = String(this.form.email ?? "").trim();
4751
- const checkoutId = this.checkoutState?.checkoutId;
5368
+ const checkoutId = returnCheckoutId || this.checkoutState?.checkoutId;
4752
5369
  const providerOrderId = r.order?.orderId != null ? String(r.order.orderId) : "";
4753
5370
  this.confirmedOrder = {
4754
5371
  items: [...this.items],
@@ -4765,7 +5382,10 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4765
5382
  new CustomEvent("vio:payment-success", {
4766
5383
  bubbles: true,
4767
5384
  composed: true,
4768
- detail: { method, sponsorId, result }
5385
+ // `checkoutId` is Vio's own reference for the purchase — what the
5386
+ // redirect path already sends as `orderId`; analytics falls back to it
5387
+ // when the provider's result carries no order id.
5388
+ detail: { method, sponsorId, result, checkoutId }
4769
5389
  })
4770
5390
  );
4771
5391
  Vio.cart.clearSponsorCart(sponsorId);
@@ -4877,7 +5497,9 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4877
5497
  <div
4878
5498
  class="modal ${this.express ? "as-side" : this.orderConfirmed ? "as-confirm" : ""}"
4879
5499
  role="dialog"
5500
+ aria-modal="true"
4880
5501
  aria-label=${this.heading}
5502
+ tabindex="-1"
4881
5503
  >
4882
5504
  <div class="handle" @click=${this.close} aria-hidden="true"></div>
4883
5505
  <div class="topbar">
@@ -4913,15 +5535,16 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4913
5535
  * sponsor's cart cleared — but the screen is the provider's receipt, not our
4914
5536
  * "Takk!": two receipts for one purchase is one too many.
4915
5537
  */
4916
- showProviderReceipt(method, sponsorId, result) {
5538
+ showProviderReceipt(method, sponsorId, result, returnCheckoutId) {
4917
5539
  this.providerReceipt = method;
4918
5540
  this.confirmedMethod = method;
4919
5541
  this.open = true;
5542
+ const checkoutId = returnCheckoutId || this.checkoutState?.checkoutId;
4920
5543
  this.dispatchEvent(
4921
5544
  new CustomEvent("vio:payment-success", {
4922
5545
  bubbles: true,
4923
5546
  composed: true,
4924
- detail: { method, sponsorId, result }
5547
+ detail: { method, sponsorId, result, checkoutId }
4925
5548
  })
4926
5549
  );
4927
5550
  if (sponsorId) {
@@ -5090,7 +5713,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
5090
5713
  if (typeof console !== "undefined") {
5091
5714
  console.warn("[VioCheckout] Kustom mount failed:", err);
5092
5715
  }
5093
- this.paymentError = `Kunne ikke laste Kustom: ${err instanceof Error ? err.message : String(err)}`;
5716
+ this.paymentError = friendlyPaymentError(errorText(err));
5094
5717
  Vio.checkout.selectPaymentMethod("");
5095
5718
  } finally {
5096
5719
  this.kustomMounting = false;
@@ -5210,7 +5833,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
5210
5833
  if (typeof console !== "undefined") {
5211
5834
  console.warn("[VioCheckout] Qliro mount failed:", err);
5212
5835
  }
5213
- this.paymentError = `Kunne ikke laste Qliro: ${err instanceof Error ? err.message : String(err)}`;
5836
+ this.paymentError = friendlyPaymentError(errorText(err));
5214
5837
  Vio.checkout.selectPaymentMethod("");
5215
5838
  } finally {
5216
5839
  this.qliroMounting = false;
@@ -5267,7 +5890,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
5267
5890
  if (typeof console !== "undefined") {
5268
5891
  console.warn("[VioCheckout] Walley mount failed:", err);
5269
5892
  }
5270
- this.paymentError = `Kunne ikke laste Walley: ${err instanceof Error ? err.message : String(err)}`;
5893
+ this.paymentError = friendlyPaymentError(errorText(err));
5271
5894
  Vio.checkout.selectPaymentMethod("");
5272
5895
  } finally {
5273
5896
  this.walleyMounting = false;
@@ -5342,7 +5965,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
5342
5965
  if (typeof console !== "undefined") {
5343
5966
  console.warn("[VioCheckout] Nexi mount failed:", err);
5344
5967
  }
5345
- this.paymentError = `Kunne ikke laste Nexi: ${err instanceof Error ? err.message : String(err)}`;
5968
+ this.paymentError = friendlyPaymentError(errorText(err));
5346
5969
  Vio.checkout.selectPaymentMethod("");
5347
5970
  } finally {
5348
5971
  this.nexiMounting = false;
@@ -5529,7 +6152,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
5529
6152
  this.adyenMountedKey = this.embedPurchaseKey();
5530
6153
  } catch (err) {
5531
6154
  this.unmountAdyen();
5532
- this.paymentError = err instanceof AdyenOriginError ? "Betaling er ikke aktivert for denne siden enn\xE5. Ta kontakt med butikken." : `Kunne ikke laste betaling: ${err instanceof Error ? err.message : String(err)}`;
6155
+ this.paymentError = err instanceof AdyenOriginError ? "Betaling er ikke aktivert for denne siden enn\xE5. Ta kontakt med butikken." : friendlyPaymentError(errorText(err));
5533
6156
  if (typeof console !== "undefined") console.warn("[VioCheckout] Adyen mount failed:", err);
5534
6157
  } finally {
5535
6158
  this.adyenMounting = false;
@@ -5639,7 +6262,10 @@ var _VioCheckout = class _VioCheckout extends LitElement {
5639
6262
  },
5640
6263
  onLoadError: (message) => {
5641
6264
  this.unmountStripe();
5642
- this.paymentError = `Kunne ikke laste betaling: ${message}`;
6265
+ if (typeof console !== "undefined") {
6266
+ console.warn("[VioCheckout] Stripe Payment Element failed to load:", message);
6267
+ }
6268
+ this.paymentError = friendlyPaymentError(message, "stripe");
5643
6269
  }
5644
6270
  });
5645
6271
  if (this.embedSession() !== mountedFor || this.checkoutState?.paymentMethod !== "stripe" || !this.open) {
@@ -5658,7 +6284,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
5658
6284
  this.stripeMountedKey = this.embedPurchaseKey();
5659
6285
  } catch (err) {
5660
6286
  this.unmountStripe();
5661
- this.paymentError = `Kunne ikke laste betaling: ${err instanceof Error ? err.message : String(err)}`;
6287
+ this.paymentError = friendlyPaymentError(errorText(err));
5662
6288
  if (typeof console !== "undefined") console.warn("[VioCheckout] Stripe mount failed:", err);
5663
6289
  } finally {
5664
6290
  this.stripeMounting = false;
@@ -5687,6 +6313,9 @@ var _VioCheckout = class _VioCheckout extends LitElement {
5687
6313
  const settled = checkoutId ? await this.waitForStripeOrder(checkoutId, spId) : "pending";
5688
6314
  if (settled === "paid") {
5689
6315
  this.confirmOrder("stripe", spId, { chargedTotal: charged });
6316
+ } else if (settled === "duplicate") {
6317
+ this.stripeSettled = false;
6318
+ this.paymentError = CART_PAID_MESSAGE;
5690
6319
  } else if (settled === "failed") {
5691
6320
  this.stripeSettled = false;
5692
6321
  this.paymentError = "Betalingen ble avbrutt eller feilet. Vennligst pr\xF8v igjen.";
@@ -5712,6 +6341,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
5712
6341
  try {
5713
6342
  const checkout = await Vio.checkout.getCheckout(checkoutId, sponsorId || void 0);
5714
6343
  const status = String(checkout?.status ?? "").toLowerCase();
6344
+ if (checkout?.duplicate_of) return "duplicate";
5715
6345
  if (_VioCheckout.FAILED_STATUSES.has(status)) return "failed";
5716
6346
  if (_VioCheckout.PAID_STATUSES.has(status)) return "paid";
5717
6347
  } catch (err) {
@@ -6377,5 +7007,5 @@ function registerVioElements() {
6377
7007
  }
6378
7008
 
6379
7009
  export { VioCart, VioCheckout, VioProduct, VioProductCarousel, VioProductDetail, VioTokens, applyVioTheme, registerVioElements };
6380
- //# sourceMappingURL=chunk-NUNGU6LL.js.map
6381
- //# sourceMappingURL=chunk-NUNGU6LL.js.map
7010
+ //# sourceMappingURL=chunk-COCHGJ2K.js.map
7011
+ //# sourceMappingURL=chunk-COCHGJ2K.js.map