@vio-live/web-sdk 0.19.0 → 0.21.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 } from './chunk-WTVGVDZX.js';
2
2
  import { LitElement, css, html } from 'lit';
3
3
  import { property, state, query } from 'lit/decorators.js';
4
4
 
@@ -885,6 +885,192 @@ function isMethodEnabled(available, ...names) {
885
885
  );
886
886
  }
887
887
 
888
+ // src/ui/dialog-focus.ts
889
+ var DialogFocus = class {
890
+ constructor(opts) {
891
+ this.opts = opts;
892
+ this.opener = null;
893
+ this.active = false;
894
+ /** The element inside the dialog that had focus last — where a pull-back continues from. */
895
+ this.lastInside = null;
896
+ this.onKeydown = (e) => {
897
+ if (e.defaultPrevented) return;
898
+ if (e.key === "Escape" || e.key === "Esc") {
899
+ if (this.opts.canClose && !this.opts.canClose()) return;
900
+ e.preventDefault();
901
+ e.stopPropagation();
902
+ this.opts.close();
903
+ return;
904
+ }
905
+ if (e.key !== "Tab" || e.altKey || e.ctrlKey || e.metaKey) return;
906
+ const container = this.opts.container();
907
+ if (!container) return;
908
+ const items = tabbableIn(container);
909
+ if (items.length === 0) {
910
+ e.preventDefault();
911
+ focusElement(container);
912
+ return;
913
+ }
914
+ const active = deepActiveElement();
915
+ const index = active ? items.indexOf(active) : -1;
916
+ e.preventDefault();
917
+ const count = items.length;
918
+ const next = index === -1 ? e.shiftKey ? count - 1 : 0 : (index + (e.shiftKey ? -1 : 1) + count) % count;
919
+ focusElement(items[next]);
920
+ };
921
+ /**
922
+ * Focus moved somewhere. Inside the dialog: remember where. Into another
923
+ * open Vio dialog: theirs. Onto the page: the browser walked out of the
924
+ * dialog on its own (Safari leaving a widget iframe) — continue from the
925
+ * element that had focus, backwards when the page element sits before
926
+ * the host in document order.
927
+ */
928
+ this.onFocusIn = (e) => {
929
+ if (!this.opts.host.isConnected) return;
930
+ const path = e.composedPath();
931
+ if (path.includes(this.opts.host)) {
932
+ const active = deepActiveElement();
933
+ this.lastInside = active instanceof HTMLElement ? active : null;
934
+ return;
935
+ }
936
+ if (path.some((n) => isOpenVioDialog(n, this.opts.host))) return;
937
+ const container = this.opts.container();
938
+ if (!container) return;
939
+ const items = tabbableIn(container);
940
+ if (items.length === 0) {
941
+ focusElement(container);
942
+ return;
943
+ }
944
+ const target = e.target instanceof Node ? e.target : null;
945
+ const backwards = !!target && precedes(target, this.opts.host);
946
+ const index = this.lastInside ? items.indexOf(this.lastInside) : -1;
947
+ const count = items.length;
948
+ const next = backwards ? index <= 0 ? count - 1 : index - 1 : index === -1 || index === count - 1 ? 0 : index + 1;
949
+ focusElement(items[next]);
950
+ };
951
+ /** Tab pressed while focus is on the page (or the body): enter the dialog. */
952
+ this.onDocumentKeydown = (e) => {
953
+ if (!this.opts.host.isConnected) return;
954
+ if (e.defaultPrevented || e.key !== "Tab" || e.altKey || e.ctrlKey || e.metaKey) return;
955
+ const active = deepActiveElement();
956
+ if (active && isWithin(active, this.opts.host)) return;
957
+ if (active && hasOpenVioDialogAbove(active, this.opts.host)) return;
958
+ const container = this.opts.container();
959
+ if (!container) return;
960
+ e.preventDefault();
961
+ e.stopPropagation();
962
+ const items = tabbableIn(container);
963
+ focusElement(items.length === 0 ? container : e.shiftKey ? items[items.length - 1] : items[0]);
964
+ };
965
+ }
966
+ /**
967
+ * Call from `updated()` when `open` changed. Lit reports `open` as changed
968
+ * on the first render too (from undefined to its initial value): with the
969
+ * same value twice this is a no-op, so that first call does nothing.
970
+ */
971
+ update(open) {
972
+ if (open === this.active) return;
973
+ this.active = open;
974
+ if (open) this.opened();
975
+ else this.closed();
976
+ }
977
+ opened() {
978
+ this.opener = deepActiveElement();
979
+ this.lastInside = null;
980
+ this.opts.host.addEventListener("keydown", this.onKeydown);
981
+ document.addEventListener("focusin", this.onFocusIn, true);
982
+ document.addEventListener("keydown", this.onDocumentKeydown, true);
983
+ const container = this.opts.container();
984
+ if (container) focusInto(container);
985
+ }
986
+ closed() {
987
+ this.opts.host.removeEventListener("keydown", this.onKeydown);
988
+ document.removeEventListener("focusin", this.onFocusIn, true);
989
+ document.removeEventListener("keydown", this.onDocumentKeydown, true);
990
+ const opener = this.opener;
991
+ this.opener = null;
992
+ const active = deepActiveElement();
993
+ if (!active || !isWithin(active, this.opts.host)) return;
994
+ if (opener && opener.isConnected && isRestorable(opener)) {
995
+ focusElement(opener);
996
+ } else if (active instanceof HTMLElement) {
997
+ active.blur();
998
+ }
999
+ }
1000
+ };
1001
+ function isOpenVioDialog(node, host) {
1002
+ return node instanceof Element && node !== host && node.tagName.startsWith("VIO-") && node.hasAttribute("open");
1003
+ }
1004
+ function hasOpenVioDialogAbove(node, host) {
1005
+ let n = node;
1006
+ while (n) {
1007
+ if (isOpenVioDialog(n, host)) return true;
1008
+ n = n.parentNode ?? (n instanceof ShadowRoot ? n.host : null);
1009
+ }
1010
+ return false;
1011
+ }
1012
+ function precedes(a, b) {
1013
+ return (a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING) !== 0;
1014
+ }
1015
+ function deepActiveElement() {
1016
+ if (typeof document === "undefined") return null;
1017
+ let el = document.activeElement;
1018
+ while (el?.shadowRoot?.activeElement) el = el.shadowRoot.activeElement;
1019
+ return el;
1020
+ }
1021
+ var TABBABLE_CANDIDATES = "a[href], area[href], button, input, select, textarea, iframe, summary, [tabindex], [contenteditable]";
1022
+ function tabbableIn(container) {
1023
+ const out = [];
1024
+ walkChildren(container, out);
1025
+ return out;
1026
+ }
1027
+ function walkChildren(parent, out) {
1028
+ for (const child of Array.from(parent.children)) {
1029
+ if (child instanceof HTMLSlotElement) {
1030
+ const assigned = child.assignedElements({ flatten: true });
1031
+ if (assigned.length > 0) {
1032
+ for (const el of assigned) visit(el, out);
1033
+ continue;
1034
+ }
1035
+ }
1036
+ visit(child, out);
1037
+ }
1038
+ }
1039
+ function visit(el, out) {
1040
+ if (isTabbable(el)) out.push(el);
1041
+ if (el.shadowRoot) walkChildren(el.shadowRoot, out);
1042
+ else walkChildren(el, out);
1043
+ }
1044
+ function isTabbable(el) {
1045
+ if (!(el instanceof HTMLElement) || !el.matches(TABBABLE_CANDIDATES)) return false;
1046
+ const tabindex = el.getAttribute("tabindex");
1047
+ if (tabindex !== null && Number(tabindex) < 0) return false;
1048
+ if (el.hasAttribute("disabled")) return false;
1049
+ if (el instanceof HTMLInputElement && el.type === "hidden") return false;
1050
+ if (el.getAttribute("contenteditable") === "false" && !el.matches("a[href], button, input, select, textarea, iframe, [tabindex]")) return false;
1051
+ if (el.hidden || el.getAttribute("aria-hidden") === "true") return false;
1052
+ if (typeof el.checkVisibility === "function" && !el.checkVisibility()) return false;
1053
+ return true;
1054
+ }
1055
+ function focusInto(container) {
1056
+ const first = tabbableIn(container)[0];
1057
+ focusElement(first ?? container);
1058
+ }
1059
+ function focusElement(el) {
1060
+ el.focus({ preventScroll: true });
1061
+ }
1062
+ function isRestorable(el) {
1063
+ return el instanceof HTMLElement && el !== document.body && el !== document.documentElement;
1064
+ }
1065
+ function isWithin(node, ancestor) {
1066
+ let n = node;
1067
+ while (n) {
1068
+ if (n === ancestor) return true;
1069
+ n = n.parentNode ?? (n instanceof ShadowRoot ? n.host : null);
1070
+ }
1071
+ return false;
1072
+ }
1073
+
888
1074
  // src/ui/components/vio-product-detail.ts
889
1075
  var STRIPE_LOGO_SRC = "data:image/svg+xml;utf8," + encodeURIComponent(
890
1076
  '<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>'
@@ -916,6 +1102,12 @@ var VioProductDetail = class extends LitElement {
916
1102
  this.optMapCache = /* @__PURE__ */ new WeakMap();
917
1103
  /** Pre-prepared Apple Pay handle — show() fires synchronously on tap. */
918
1104
  this.applePayHandle = null;
1105
+ /** Keyboard and focus while open: Escape closes, Tab stays inside, focus returns to the opener. */
1106
+ this.dialogFocus = new DialogFocus({
1107
+ host: this,
1108
+ container: () => this.renderRoot.querySelector(".modal"),
1109
+ close: () => this.close()
1110
+ });
919
1111
  this.boundProductClick = (e) => {
920
1112
  if (!this.autoOpen) return;
921
1113
  const ev = e;
@@ -959,6 +1151,8 @@ var VioProductDetail = class extends LitElement {
959
1151
  overflow-y: auto;
960
1152
  }
961
1153
  :host([open]) .modal { transform: translateY(0); }
1154
+ /* Focus lands on the panel itself only as a fallback — no ring for it. */
1155
+ .modal:focus { outline: none; }
962
1156
 
963
1157
  .handle { display: none; }
964
1158
 
@@ -967,7 +1161,7 @@ var VioProductDetail = class extends LitElement {
967
1161
  .modal {
968
1162
  top: auto;
969
1163
  height: 92vh;
970
- border-radius: 16px 16px 0 0;
1164
+ border-radius: var(--vio-radius-xl, 16px) var(--vio-radius-xl, 16px) 0 0;
971
1165
  box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.08);
972
1166
  }
973
1167
  .handle {
@@ -981,7 +1175,7 @@ var VioProductDetail = class extends LitElement {
981
1175
  width: 40px;
982
1176
  height: 4px;
983
1177
  background: var(--vio-color-border, #e5e5e5);
984
- border-radius: 2px;
1178
+ border-radius: var(--vio-radius-sm, 2px);
985
1179
  }
986
1180
  }
987
1181
 
@@ -1346,9 +1540,9 @@ var VioProductDetail = class extends LitElement {
1346
1540
  );
1347
1541
  background-size: 200% 100%;
1348
1542
  animation: vio-skeleton-shimmer 1.4s ease-in-out infinite;
1349
- border-radius: 8px;
1543
+ border-radius: var(--vio-radius-lg, 8px);
1350
1544
  }
1351
- .sk-image { width: 100%; aspect-ratio: 1; border-radius: 12px; }
1545
+ .sk-image { width: 100%; aspect-ratio: 1; border-radius: var(--vio-radius-lg, 12px); }
1352
1546
  .sk-line { height: 14px; }
1353
1547
  .sk-brand { width: 38%; height: 10px; margin-top: 6px; }
1354
1548
  .sk-title { width: 85%; height: 26px; }
@@ -1382,6 +1576,9 @@ var VioProductDetail = class extends LitElement {
1382
1576
  document.removeEventListener("vio:product-click", this.boundProductClick);
1383
1577
  super.disconnectedCallback();
1384
1578
  }
1579
+ updated(changed) {
1580
+ if (changed.has("open")) this.dialogFocus.update(this.open);
1581
+ }
1385
1582
  show() {
1386
1583
  this.open = true;
1387
1584
  }
@@ -1938,7 +2135,7 @@ var VioProductDetail = class extends LitElement {
1938
2135
  render() {
1939
2136
  return html`
1940
2137
  <div class="backdrop" @click=${this.close}></div>
1941
- <div class="modal" role="dialog" aria-label="Produkt">
2138
+ <div class="modal" role="dialog" aria-modal="true" aria-label="Produkt" tabindex="-1">
1942
2139
  <div class="handle" @click=${this.close} aria-hidden="true"></div>
1943
2140
  <div class="topbar">
1944
2141
  <span class="topbar-brand">${this.headerName()}</span>
@@ -2247,6 +2444,12 @@ var VioCart = class extends LitElement {
2247
2444
  this.preparedTotal = -1;
2248
2445
  /** In-flight guard for loadAvailablePaymentMethods. */
2249
2446
  this.loadingPaymentMethods = false;
2447
+ /** Keyboard and focus while open: Escape closes, Tab stays inside, focus returns to the opener. */
2448
+ this.dialogFocus = new DialogFocus({
2449
+ host: this,
2450
+ container: () => this.renderRoot.querySelector(".drawer"),
2451
+ close: () => this.close()
2452
+ });
2250
2453
  this.boundOnCartChange = (e) => {
2251
2454
  const detail = e.detail;
2252
2455
  this.carts = detail.cartsBySponsor;
@@ -2289,6 +2492,8 @@ var VioCart = class extends LitElement {
2289
2492
  box-shadow: -8px 0 24px rgba(0, 0, 0, 0.06);
2290
2493
  }
2291
2494
  :host([open]) .drawer { transform: translateX(0); }
2495
+ /* Focus lands on the drawer itself only as a fallback — no ring for it. */
2496
+ .drawer:focus { outline: none; }
2292
2497
 
2293
2498
  .handle { display: none; }
2294
2499
 
@@ -2301,7 +2506,7 @@ var VioCart = class extends LitElement {
2301
2506
  bottom: 0;
2302
2507
  width: 100%;
2303
2508
  height: 88vh;
2304
- border-radius: 16px 16px 0 0;
2509
+ border-radius: var(--vio-radius-xl, 16px) var(--vio-radius-xl, 16px) 0 0;
2305
2510
  transform: translateY(100%);
2306
2511
  box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.08);
2307
2512
  }
@@ -2316,7 +2521,7 @@ var VioCart = class extends LitElement {
2316
2521
  width: 40px;
2317
2522
  height: 4px;
2318
2523
  background: var(--vio-color-border, #e5e5e5);
2319
- border-radius: 2px;
2524
+ border-radius: var(--vio-radius-sm, 2px);
2320
2525
  }
2321
2526
  }
2322
2527
 
@@ -2409,7 +2614,7 @@ var VioCart = class extends LitElement {
2409
2614
  height: 24px;
2410
2615
  border: 1px solid var(--vio-color-border, #e5e5e5);
2411
2616
  background: #fff;
2412
- border-radius: 2px;
2617
+ border-radius: var(--vio-radius-sm, 2px);
2413
2618
  cursor: pointer;
2414
2619
  font-size: 12px;
2415
2620
  color: var(--vio-color-text, #0a0a0a);
@@ -2466,7 +2671,7 @@ var VioCart = class extends LitElement {
2466
2671
  background: #000;
2467
2672
  color: #fff;
2468
2673
  border: none;
2469
- border-radius: 8px;
2674
+ border-radius: var(--vio-radius-lg, 8px);
2470
2675
  font-size: 19px;
2471
2676
  font-weight: 500;
2472
2677
  line-height: 1;
@@ -2488,7 +2693,7 @@ var VioCart = class extends LitElement {
2488
2693
  background: #ffb3c7;
2489
2694
  color: #0a0a0a;
2490
2695
  border: none;
2491
- border-radius: 8px;
2696
+ border-radius: var(--vio-radius-lg, 8px);
2492
2697
  font-size: 16px;
2493
2698
  font-weight: 600;
2494
2699
  cursor: pointer;
@@ -2509,7 +2714,7 @@ var VioCart = class extends LitElement {
2509
2714
  background: #ff5b24;
2510
2715
  color: #fff;
2511
2716
  border: none;
2512
- border-radius: 8px;
2717
+ border-radius: var(--vio-radius-lg, 8px);
2513
2718
  cursor: pointer;
2514
2719
  display: flex;
2515
2720
  align-items: center;
@@ -2533,7 +2738,7 @@ var VioCart = class extends LitElement {
2533
2738
  background: #635bff;
2534
2739
  color: #fff;
2535
2740
  border: none;
2536
- border-radius: 8px;
2741
+ border-radius: var(--vio-radius-lg, 8px);
2537
2742
  font-size: 16px;
2538
2743
  font-weight: 600;
2539
2744
  cursor: pointer;
@@ -2671,6 +2876,7 @@ var VioCart = class extends LitElement {
2671
2876
  return isMethodEnabled(this.availableMethods, ...names);
2672
2877
  }
2673
2878
  updated(changed) {
2879
+ if (changed.has("open")) this.dialogFocus.update(this.open);
2674
2880
  if (changed.has("open") && this.open) {
2675
2881
  this.confirmedOrder = null;
2676
2882
  this.preparedTotal = -1;
@@ -2850,7 +3056,7 @@ var VioCart = class extends LitElement {
2850
3056
  const sponsorCarts = [...this.carts.values()].filter((c) => c.items.length > 0);
2851
3057
  return html`
2852
3058
  <div class="backdrop" @click=${this.close}></div>
2853
- <aside class="drawer" role="dialog" aria-label=${this.heading}>
3059
+ <aside class="drawer" role="dialog" aria-modal="true" aria-label=${this.heading} tabindex="-1">
2854
3060
  <div class="handle" @click=${this.close} aria-hidden="true"></div>
2855
3061
  <div class="header">
2856
3062
  <h2 class="heading">${this.confirmedOrder ? "Takk!" : this.heading}</h2>
@@ -3044,6 +3250,38 @@ __decorateClass([
3044
3250
  __decorateClass([
3045
3251
  state()
3046
3252
  ], VioCart.prototype, "confirmedOrder", 2);
3253
+
3254
+ // src/ui/checkout-errors.ts
3255
+ var EMPTY_CART_MESSAGE = "Handlekurven er tom. Legg til et produkt og pr\xF8v igjen.";
3256
+ var CART_CHANGED_MESSAGE = "Handlekurven ble endret i en annen fane. Last siden p\xE5 nytt og pr\xF8v igjen.";
3257
+ var CART_PAID_MESSAGE = "Denne handlekurven er allerede betalt. Start en ny handlekurv.";
3258
+ var PAYMENT_START_FAILED_MESSAGE = "Betalingen kunne ikke startes. Pr\xF8v igjen, eller velg en annen betalingsm\xE5te.";
3259
+ var SHIPS_APART = "kan ikke sendes sammen";
3260
+ function friendlyPaymentError(raw, method) {
3261
+ const text = String(raw ?? "");
3262
+ const lower = text.toLowerCase();
3263
+ if (lower.includes("got 0") || lower.includes("amount must be a positive")) {
3264
+ return EMPTY_CART_MESSAGE;
3265
+ }
3266
+ if (lower.includes("replaced by a newer") || text.includes("CHECKOUT_SUPERSEDED")) {
3267
+ return CART_CHANGED_MESSAGE;
3268
+ }
3269
+ if (lower.includes("already been paid") || text.includes("CART_ALREADY_PAID")) {
3270
+ return CART_PAID_MESSAGE;
3271
+ }
3272
+ if (text.includes(SHIPS_APART)) return norwegianSentence(text);
3273
+ return PAYMENT_START_FAILED_MESSAGE;
3274
+ }
3275
+ function norwegianSentence(text) {
3276
+ const at = text.indexOf(SHIPS_APART);
3277
+ const prefixEnd = text.lastIndexOf(": ", at);
3278
+ return (prefixEnd === -1 ? text : text.slice(prefixEnd + 2)).trim();
3279
+ }
3280
+ function errorText(err) {
3281
+ return err instanceof Error ? err.message : String(err);
3282
+ }
3283
+
3284
+ // src/ui/components/vio-checkout.ts
3047
3285
  var STRIPE_LOGO_SRC3 = "data:image/svg+xml;utf8," + encodeURIComponent(
3048
3286
  '<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
3287
  );
@@ -3155,6 +3393,16 @@ var _VioCheckout = class _VioCheckout extends LitElement {
3155
3393
  };
3156
3394
  /** In-flight guard for loadAvailablePaymentMethods. */
3157
3395
  this.loadingPaymentMethods = false;
3396
+ /**
3397
+ * Keyboard and focus while open: Escape closes — not while a payment is
3398
+ * in flight — Tab stays inside, focus returns to the opener.
3399
+ */
3400
+ this.dialogFocus = new DialogFocus({
3401
+ host: this,
3402
+ container: () => this.renderRoot.querySelector(".modal"),
3403
+ close: () => this.close(),
3404
+ canClose: () => !this.paymentInFlight
3405
+ });
3158
3406
  this.paymentMethodsResolved = false;
3159
3407
  /** Guards `autoSelectSoleMethod` to ONE attempt per opening — see there. */
3160
3408
  this.autoSelectAttempted = false;
@@ -3233,10 +3481,17 @@ var _VioCheckout = class _VioCheckout extends LitElement {
3233
3481
  };
3234
3482
  this.boundOnPaymentError = (e) => {
3235
3483
  const detail = e.detail;
3236
- this.paymentError = detail.method === "klarna" ? `Klarna-betalingen kunne ikke fullf\xF8res: ${detail.error}` : detail.error;
3484
+ if (typeof console !== "undefined") {
3485
+ console.warn("[VioCheckout] payment-error:", detail.method, detail.error);
3486
+ }
3487
+ this.paymentError = detail.method === "klarna" ? `Klarna-betalingen kunne ikke fullf\xF8res: ${detail.error}` : friendlyPaymentError(detail.error, detail.method);
3237
3488
  this.open = true;
3238
3489
  };
3239
3490
  }
3491
+ /** A payment is being started or confirmed right now: the overlay must not close under it. */
3492
+ get paymentInFlight() {
3493
+ return this.stripePaying || this.stripeLoading || this.vippsLoading || this.klarnaAuthorizing || this.applePayInProgress;
3494
+ }
3240
3495
  static {
3241
3496
  this.styles = css`
3242
3497
  :host { display: contents; }
@@ -3265,6 +3520,8 @@ var _VioCheckout = class _VioCheckout extends LitElement {
3265
3520
  overflow-y: auto;
3266
3521
  }
3267
3522
  :host([open]) .modal { transform: translateY(0); }
3523
+ /* Focus lands on the panel itself only as a fallback — no ring for it. */
3524
+ .modal:focus { outline: none; }
3268
3525
 
3269
3526
  /* Confirmation drawer — compact bottom sheet, not the full-screen overlay. */
3270
3527
  .modal.as-drawer {
@@ -3894,6 +4151,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
3894
4151
  const vippsStatus = await Vio.checkout.getVippsStatus(checkoutId, sponsorId || void 0);
3895
4152
  const state6 = String(vippsStatus?.state ?? "").toUpperCase();
3896
4153
  sawStatus = true;
4154
+ if (vippsStatus?.duplicate_of) return { outcome: "duplicate", sawStatus };
3897
4155
  if (state6 === "AUTHORIZED") return { outcome: "paid", sawStatus };
3898
4156
  if (state6 === "CREATED") continue;
3899
4157
  return { outcome: "failed", sawStatus };
@@ -3926,6 +4184,9 @@ var _VioCheckout = class _VioCheckout extends LitElement {
3926
4184
  detail: { method: vioMethod, sponsorId, orderId: checkoutId }
3927
4185
  })
3928
4186
  );
4187
+ } else if (outcome === "duplicate") {
4188
+ this.paymentError = CART_PAID_MESSAGE;
4189
+ this.open = true;
3929
4190
  } else if (outcome === "failed") {
3930
4191
  this.paymentError = "Betalingen ble avbrutt eller feilet. Vennligst pr\xF8v igjen.";
3931
4192
  this.open = true;
@@ -3996,7 +4257,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
3996
4257
  this.verifyingReturn = null;
3997
4258
  if (order?.status === "checkout_complete" && order.html_snippet) {
3998
4259
  const spId = this.checkoutState?.sponsorId ?? 0;
3999
- this.showProviderReceipt("kustom", spId, order);
4260
+ this.showProviderReceipt("kustom", spId, order, kustomReturn.checkoutId);
4000
4261
  await this.updateComplete;
4001
4262
  const container = this.lightContainer("vio-kustom-checkout-container", "vio-kustom");
4002
4263
  Vio.checkout.renderKustomSnippet(container, order.html_snippet);
@@ -4042,12 +4303,12 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4042
4303
  const result = { order: { orderId: order.order_id }, chargedTotal: order.total_price };
4043
4304
  this.qliroMountedOrderId = order.order_id;
4044
4305
  if (order.html_snippet) {
4045
- this.showProviderReceipt("qliro", spId, result);
4306
+ this.showProviderReceipt("qliro", spId, result, checkoutId);
4046
4307
  await this.updateComplete;
4047
4308
  const container = this.lightContainer("vio-qliro-checkout-container", "vio-qliro");
4048
4309
  Vio.checkout.renderKustomSnippet(container, order.html_snippet);
4049
4310
  } else {
4050
- this.confirmOrder("qliro", spId, result);
4311
+ this.confirmOrder("qliro", spId, result, checkoutId);
4051
4312
  }
4052
4313
  } else if (order?.html_snippet) {
4053
4314
  this.qliroMountedOrderId = order.order_id;
@@ -4098,9 +4359,12 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4098
4359
  Vio.checkout.renderKustomSnippet(container, order.html_snippet);
4099
4360
  }
4100
4361
  if (order.status === "PurchaseCompleted") {
4101
- this.showProviderReceipt("walley", this.checkoutState?.sponsorId ?? 0, {
4102
- orderId: order.order_id
4103
- });
4362
+ this.showProviderReceipt(
4363
+ "walley",
4364
+ this.checkoutState?.sponsorId ?? 0,
4365
+ { orderId: order.order_id },
4366
+ checkoutId
4367
+ );
4104
4368
  }
4105
4369
  }
4106
4370
  } catch (err) {
@@ -4196,6 +4460,10 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4196
4460
  const checkout = await Vio.checkout.getCheckout(checkoutId, sponsorId || void 0);
4197
4461
  const status = String(checkout?.status ?? "").toLowerCase();
4198
4462
  sawStatus = true;
4463
+ if (checkout?.duplicate_of) {
4464
+ outcome = "duplicate";
4465
+ break;
4466
+ }
4199
4467
  if (_VioCheckout.FAILED_STATUSES.has(status)) {
4200
4468
  outcome = "failed";
4201
4469
  break;
@@ -4260,6 +4528,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4260
4528
  super.disconnectedCallback();
4261
4529
  }
4262
4530
  updated(changed) {
4531
+ if (changed.has("open")) this.dialogFocus.update(this.open);
4263
4532
  if (changed.has("open") && this.open && this.items.length > 0 && this.availableShippingsList.length === 0) {
4264
4533
  void this.loadAvailableShippings();
4265
4534
  }
@@ -4518,7 +4787,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4518
4787
  if (typeof console !== "undefined") {
4519
4788
  console.warn("[VioCheckout] Klarna Payments mount failed:", err);
4520
4789
  }
4521
- this.paymentError = `Kunne ikke laste Klarna: ${err instanceof Error ? err.message : String(err)}`;
4790
+ this.paymentError = friendlyPaymentError(errorText(err));
4522
4791
  } finally {
4523
4792
  this.klarnaMounting = false;
4524
4793
  }
@@ -4701,7 +4970,10 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4701
4970
  }
4702
4971
  this.stripeLoading = true;
4703
4972
  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)}`;
4973
+ if (typeof console !== "undefined") {
4974
+ console.warn("[VioCheckout] Stripe payment could not start:", err);
4975
+ }
4976
+ this.paymentError = friendlyPaymentError(errorText(err));
4705
4977
  }).finally(() => {
4706
4978
  this.stripeLoading = false;
4707
4979
  });
@@ -4709,7 +4981,10 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4709
4981
  if (method === "vipps") {
4710
4982
  this.vippsLoading = true;
4711
4983
  void this.startVippsFromCheckout().catch((err) => {
4712
- this.paymentError = `Kunne ikke starte Vipps-betaling: ${err instanceof Error ? err.message : String(err)}`;
4984
+ if (typeof console !== "undefined") {
4985
+ console.warn("[VioCheckout] Vipps payment could not start:", err);
4986
+ }
4987
+ this.paymentError = friendlyPaymentError(errorText(err));
4713
4988
  }).finally(() => {
4714
4989
  this.vippsLoading = false;
4715
4990
  });
@@ -4744,11 +5019,11 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4744
5019
  * the confirmation screen, emits `vio:payment-success`, and clears the
4745
5020
  * paid sponsor's cart so the badge + drawer reflect the purchase.
4746
5021
  */
4747
- confirmOrder(method, sponsorId, result) {
5022
+ confirmOrder(method, sponsorId, result, returnCheckoutId) {
4748
5023
  const r = result ?? {};
4749
5024
  const shippingPrice = this.shippingMajor();
4750
5025
  const email = String(this.form.email ?? "").trim();
4751
- const checkoutId = this.checkoutState?.checkoutId;
5026
+ const checkoutId = returnCheckoutId || this.checkoutState?.checkoutId;
4752
5027
  const providerOrderId = r.order?.orderId != null ? String(r.order.orderId) : "";
4753
5028
  this.confirmedOrder = {
4754
5029
  items: [...this.items],
@@ -4765,7 +5040,10 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4765
5040
  new CustomEvent("vio:payment-success", {
4766
5041
  bubbles: true,
4767
5042
  composed: true,
4768
- detail: { method, sponsorId, result }
5043
+ // `checkoutId` is Vio's own reference for the purchase — what the
5044
+ // redirect path already sends as `orderId`; analytics falls back to it
5045
+ // when the provider's result carries no order id.
5046
+ detail: { method, sponsorId, result, checkoutId }
4769
5047
  })
4770
5048
  );
4771
5049
  Vio.cart.clearSponsorCart(sponsorId);
@@ -4877,7 +5155,9 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4877
5155
  <div
4878
5156
  class="modal ${this.express ? "as-side" : this.orderConfirmed ? "as-confirm" : ""}"
4879
5157
  role="dialog"
5158
+ aria-modal="true"
4880
5159
  aria-label=${this.heading}
5160
+ tabindex="-1"
4881
5161
  >
4882
5162
  <div class="handle" @click=${this.close} aria-hidden="true"></div>
4883
5163
  <div class="topbar">
@@ -4913,15 +5193,16 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4913
5193
  * sponsor's cart cleared — but the screen is the provider's receipt, not our
4914
5194
  * "Takk!": two receipts for one purchase is one too many.
4915
5195
  */
4916
- showProviderReceipt(method, sponsorId, result) {
5196
+ showProviderReceipt(method, sponsorId, result, returnCheckoutId) {
4917
5197
  this.providerReceipt = method;
4918
5198
  this.confirmedMethod = method;
4919
5199
  this.open = true;
5200
+ const checkoutId = returnCheckoutId || this.checkoutState?.checkoutId;
4920
5201
  this.dispatchEvent(
4921
5202
  new CustomEvent("vio:payment-success", {
4922
5203
  bubbles: true,
4923
5204
  composed: true,
4924
- detail: { method, sponsorId, result }
5205
+ detail: { method, sponsorId, result, checkoutId }
4925
5206
  })
4926
5207
  );
4927
5208
  if (sponsorId) {
@@ -5090,7 +5371,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
5090
5371
  if (typeof console !== "undefined") {
5091
5372
  console.warn("[VioCheckout] Kustom mount failed:", err);
5092
5373
  }
5093
- this.paymentError = `Kunne ikke laste Kustom: ${err instanceof Error ? err.message : String(err)}`;
5374
+ this.paymentError = friendlyPaymentError(errorText(err));
5094
5375
  Vio.checkout.selectPaymentMethod("");
5095
5376
  } finally {
5096
5377
  this.kustomMounting = false;
@@ -5210,7 +5491,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
5210
5491
  if (typeof console !== "undefined") {
5211
5492
  console.warn("[VioCheckout] Qliro mount failed:", err);
5212
5493
  }
5213
- this.paymentError = `Kunne ikke laste Qliro: ${err instanceof Error ? err.message : String(err)}`;
5494
+ this.paymentError = friendlyPaymentError(errorText(err));
5214
5495
  Vio.checkout.selectPaymentMethod("");
5215
5496
  } finally {
5216
5497
  this.qliroMounting = false;
@@ -5267,7 +5548,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
5267
5548
  if (typeof console !== "undefined") {
5268
5549
  console.warn("[VioCheckout] Walley mount failed:", err);
5269
5550
  }
5270
- this.paymentError = `Kunne ikke laste Walley: ${err instanceof Error ? err.message : String(err)}`;
5551
+ this.paymentError = friendlyPaymentError(errorText(err));
5271
5552
  Vio.checkout.selectPaymentMethod("");
5272
5553
  } finally {
5273
5554
  this.walleyMounting = false;
@@ -5342,7 +5623,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
5342
5623
  if (typeof console !== "undefined") {
5343
5624
  console.warn("[VioCheckout] Nexi mount failed:", err);
5344
5625
  }
5345
- this.paymentError = `Kunne ikke laste Nexi: ${err instanceof Error ? err.message : String(err)}`;
5626
+ this.paymentError = friendlyPaymentError(errorText(err));
5346
5627
  Vio.checkout.selectPaymentMethod("");
5347
5628
  } finally {
5348
5629
  this.nexiMounting = false;
@@ -5529,7 +5810,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
5529
5810
  this.adyenMountedKey = this.embedPurchaseKey();
5530
5811
  } catch (err) {
5531
5812
  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)}`;
5813
+ this.paymentError = err instanceof AdyenOriginError ? "Betaling er ikke aktivert for denne siden enn\xE5. Ta kontakt med butikken." : friendlyPaymentError(errorText(err));
5533
5814
  if (typeof console !== "undefined") console.warn("[VioCheckout] Adyen mount failed:", err);
5534
5815
  } finally {
5535
5816
  this.adyenMounting = false;
@@ -5639,7 +5920,10 @@ var _VioCheckout = class _VioCheckout extends LitElement {
5639
5920
  },
5640
5921
  onLoadError: (message) => {
5641
5922
  this.unmountStripe();
5642
- this.paymentError = `Kunne ikke laste betaling: ${message}`;
5923
+ if (typeof console !== "undefined") {
5924
+ console.warn("[VioCheckout] Stripe Payment Element failed to load:", message);
5925
+ }
5926
+ this.paymentError = friendlyPaymentError(message, "stripe");
5643
5927
  }
5644
5928
  });
5645
5929
  if (this.embedSession() !== mountedFor || this.checkoutState?.paymentMethod !== "stripe" || !this.open) {
@@ -5658,7 +5942,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
5658
5942
  this.stripeMountedKey = this.embedPurchaseKey();
5659
5943
  } catch (err) {
5660
5944
  this.unmountStripe();
5661
- this.paymentError = `Kunne ikke laste betaling: ${err instanceof Error ? err.message : String(err)}`;
5945
+ this.paymentError = friendlyPaymentError(errorText(err));
5662
5946
  if (typeof console !== "undefined") console.warn("[VioCheckout] Stripe mount failed:", err);
5663
5947
  } finally {
5664
5948
  this.stripeMounting = false;
@@ -5687,6 +5971,9 @@ var _VioCheckout = class _VioCheckout extends LitElement {
5687
5971
  const settled = checkoutId ? await this.waitForStripeOrder(checkoutId, spId) : "pending";
5688
5972
  if (settled === "paid") {
5689
5973
  this.confirmOrder("stripe", spId, { chargedTotal: charged });
5974
+ } else if (settled === "duplicate") {
5975
+ this.stripeSettled = false;
5976
+ this.paymentError = CART_PAID_MESSAGE;
5690
5977
  } else if (settled === "failed") {
5691
5978
  this.stripeSettled = false;
5692
5979
  this.paymentError = "Betalingen ble avbrutt eller feilet. Vennligst pr\xF8v igjen.";
@@ -5712,6 +5999,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
5712
5999
  try {
5713
6000
  const checkout = await Vio.checkout.getCheckout(checkoutId, sponsorId || void 0);
5714
6001
  const status = String(checkout?.status ?? "").toLowerCase();
6002
+ if (checkout?.duplicate_of) return "duplicate";
5715
6003
  if (_VioCheckout.FAILED_STATUSES.has(status)) return "failed";
5716
6004
  if (_VioCheckout.PAID_STATUSES.has(status)) return "paid";
5717
6005
  } catch (err) {
@@ -6377,5 +6665,5 @@ function registerVioElements() {
6377
6665
  }
6378
6666
 
6379
6667
  export { VioCart, VioCheckout, VioProduct, VioProductCarousel, VioProductDetail, VioTokens, applyVioTheme, registerVioElements };
6380
- //# sourceMappingURL=chunk-NUNGU6LL.js.map
6381
- //# sourceMappingURL=chunk-NUNGU6LL.js.map
6668
+ //# sourceMappingURL=chunk-MRJR6DG5.js.map
6669
+ //# sourceMappingURL=chunk-MRJR6DG5.js.map