@vio-live/web-sdk 0.11.1 → 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, formatPrice } from './chunk-TJ43TE7H.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
 
@@ -863,6 +863,215 @@ function sponsorNameFromBootstrap(sponsorId) {
863
863
  if (boot.primarySponsor?.id === sponsorId) return boot.primarySponsor.name;
864
864
  return boot.secondarySponsors?.find((s) => s.id === sponsorId)?.name;
865
865
  }
866
+
867
+ // src/core/checkout/method-taxonomy.ts
868
+ function normalizeMethodName(name) {
869
+ return String(name ?? "").toLowerCase().replace(/[^a-z]/g, "");
870
+ }
871
+ var EMBEDDED_METHODS = ["kustom", "qliro", "walley", "nexi"];
872
+ var FORM_FIRST_WIDGET_METHODS = ["adyen"];
873
+ var NO_EXPRESS_BUTTON_METHODS = [
874
+ ...EMBEDDED_METHODS,
875
+ ...FORM_FIRST_WIDGET_METHODS
876
+ ];
877
+ var COLLECTS_OWN_ADDRESS = [...EMBEDDED_METHODS, "vipps"];
878
+ var isEmbeddedMethod = (name) => EMBEDDED_METHODS.includes(normalizeMethodName(name));
879
+ var isFormFirstWidgetMethod = (name) => FORM_FIRST_WIDGET_METHODS.includes(normalizeMethodName(name));
880
+ var collectsOwnAddress = (name) => COLLECTS_OWN_ADDRESS.includes(normalizeMethodName(name));
881
+ function isMethodEnabled(available, ...names) {
882
+ if (available === null) return true;
883
+ return available.some(
884
+ (m) => names.some((n) => normalizeMethodName(m) === normalizeMethodName(n))
885
+ );
886
+ }
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
+
1074
+ // src/ui/components/vio-product-detail.ts
866
1075
  var STRIPE_LOGO_SRC = "data:image/svg+xml;utf8," + encodeURIComponent(
867
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>'
868
1077
  );
@@ -875,6 +1084,8 @@ var VioProductDetail = class extends LitElement {
875
1084
  this.autoOpen = true;
876
1085
  this.currency = "NOK";
877
1086
  this.product = null;
1087
+ this.sellerName = null;
1088
+ this.sellerSettled = false;
878
1089
  this.isLoading = false;
879
1090
  this.fetchError = "";
880
1091
  this.selectedOptions = {};
@@ -883,6 +1094,7 @@ var VioProductDetail = class extends LitElement {
883
1094
  this.adding = false;
884
1095
  this.applePayOk = false;
885
1096
  this.availableMethods = null;
1097
+ this.vippsOfficial = false;
886
1098
  /** Race guard: only the latest fetch may write state (two quick taps on
887
1099
  * different cards would otherwise let the slow response win). */
888
1100
  this.fetchSeq = 0;
@@ -890,6 +1102,12 @@ var VioProductDetail = class extends LitElement {
890
1102
  this.optMapCache = /* @__PURE__ */ new WeakMap();
891
1103
  /** Pre-prepared Apple Pay handle — show() fires synchronously on tap. */
892
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
+ });
893
1111
  this.boundProductClick = (e) => {
894
1112
  if (!this.autoOpen) return;
895
1113
  const ev = e;
@@ -933,6 +1151,8 @@ var VioProductDetail = class extends LitElement {
933
1151
  overflow-y: auto;
934
1152
  }
935
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; }
936
1156
 
937
1157
  .handle { display: none; }
938
1158
 
@@ -941,7 +1161,7 @@ var VioProductDetail = class extends LitElement {
941
1161
  .modal {
942
1162
  top: auto;
943
1163
  height: 92vh;
944
- border-radius: 16px 16px 0 0;
1164
+ border-radius: var(--vio-radius-xl, 16px) var(--vio-radius-xl, 16px) 0 0;
945
1165
  box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.08);
946
1166
  }
947
1167
  .handle {
@@ -955,7 +1175,7 @@ var VioProductDetail = class extends LitElement {
955
1175
  width: 40px;
956
1176
  height: 4px;
957
1177
  background: var(--vio-color-border, #e5e5e5);
958
- border-radius: 2px;
1178
+ border-radius: var(--vio-radius-sm, 2px);
959
1179
  }
960
1180
  }
961
1181
 
@@ -1193,6 +1413,25 @@ var VioProductDetail = class extends LitElement {
1193
1413
  cursor: not-allowed;
1194
1414
  }
1195
1415
  /* Klarna express button — sits full-width below the add-to-cart row. */
1416
+ /* Generic buy-now for the embedded checkouts, which have no branded
1417
+ express button of their own. */
1418
+ .buy-now {
1419
+ width: 100%;
1420
+ margin-top: 10px;
1421
+ padding: 14px 24px;
1422
+ background: var(--vio-color-text, #0a0a0a);
1423
+ color: var(--vio-color-text-on-primary, #fff);
1424
+ border: none;
1425
+ border-radius: var(--vio-radius-md, 4px);
1426
+ font-family: var(--vio-font-sans, inherit);
1427
+ font-size: 12px;
1428
+ text-transform: uppercase;
1429
+ letter-spacing: var(--vio-tracking-label, 0.1em);
1430
+ font-weight: 700;
1431
+ cursor: pointer;
1432
+ }
1433
+ .buy-now:hover:not(:disabled) { opacity: 0.9; }
1434
+ .buy-now:disabled { opacity: 0.5; cursor: not-allowed; }
1196
1435
  .buy-klarna {
1197
1436
  width: 100%;
1198
1437
  margin-top: 10px;
@@ -1244,6 +1483,8 @@ var VioProductDetail = class extends LitElement {
1244
1483
  }
1245
1484
  .buy-vipps:hover:not(:disabled) { background: #ec4f1c; }
1246
1485
  .buy-vipps:disabled { opacity: 0.5; cursor: not-allowed; }
1486
+ /* Vipps' own button (web component) — same slot, their drawing. */
1487
+ .buy-vipps-official { display: block; width: 100%; margin-top: 10px; cursor: pointer; }
1247
1488
  /* Apple Pay express button — black, per Apple's button guidelines. Only
1248
1489
  shown in Safari (canApplePay). The glyph is the Apple logo (Safari). */
1249
1490
  .buy-applepay {
@@ -1299,9 +1540,9 @@ var VioProductDetail = class extends LitElement {
1299
1540
  );
1300
1541
  background-size: 200% 100%;
1301
1542
  animation: vio-skeleton-shimmer 1.4s ease-in-out infinite;
1302
- border-radius: 8px;
1543
+ border-radius: var(--vio-radius-lg, 8px);
1303
1544
  }
1304
- .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); }
1305
1546
  .sk-line { height: 14px; }
1306
1547
  .sk-brand { width: 38%; height: 10px; margin-top: 6px; }
1307
1548
  .sk-title { width: 85%; height: 26px; }
@@ -1328,12 +1569,16 @@ var VioProductDetail = class extends LitElement {
1328
1569
  }
1329
1570
  connectedCallback() {
1330
1571
  super.connectedCallback();
1572
+ void this.loadVippsButton();
1331
1573
  document.addEventListener("vio:product-click", this.boundProductClick);
1332
1574
  }
1333
1575
  disconnectedCallback() {
1334
1576
  document.removeEventListener("vio:product-click", this.boundProductClick);
1335
1577
  super.disconnectedCallback();
1336
1578
  }
1579
+ updated(changed) {
1580
+ if (changed.has("open")) this.dialogFocus.update(this.open);
1581
+ }
1337
1582
  show() {
1338
1583
  this.open = true;
1339
1584
  }
@@ -1342,6 +1587,8 @@ var VioProductDetail = class extends LitElement {
1342
1587
  setTimeout(() => {
1343
1588
  if (!this.open) {
1344
1589
  this.product = null;
1590
+ this.sellerName = null;
1591
+ this.sellerSettled = false;
1345
1592
  this.selectedOptions = {};
1346
1593
  this.quantity = 1;
1347
1594
  this.activeImageIndex = 0;
@@ -1358,6 +1605,8 @@ var VioProductDetail = class extends LitElement {
1358
1605
  const seq = ++this.fetchSeq;
1359
1606
  this.isLoading = true;
1360
1607
  this.fetchError = "";
1608
+ this.sellerName = null;
1609
+ this.sellerSettled = false;
1361
1610
  let products = null;
1362
1611
  let lastError = null;
1363
1612
  for (let attempt = 0; attempt < 4; attempt++) {
@@ -1386,6 +1635,7 @@ var VioProductDetail = class extends LitElement {
1386
1635
  }
1387
1636
  this.optMapCache = /* @__PURE__ */ new WeakMap();
1388
1637
  this.product = products?.[0] ?? null;
1638
+ if (this.product) void this.loadSeller(productIdNum, seq);
1389
1639
  if (this.product) {
1390
1640
  try {
1391
1641
  const price = Number(
@@ -1452,9 +1702,7 @@ var VioProductDetail = class extends LitElement {
1452
1702
  }
1453
1703
  /** Backend names arrive as e.g. "Apple Pay" — compare letters only. */
1454
1704
  methodEnabled(...names) {
1455
- if (this.availableMethods === null) return true;
1456
- const norm = (s) => s.toLowerCase().replace(/[^a-z]/g, "");
1457
- return this.availableMethods.some((m) => names.some((n) => norm(m) === norm(n)));
1705
+ return isMethodEnabled(this.availableMethods, ...names);
1458
1706
  }
1459
1707
  /* eslint-disable @typescript-eslint/no-explicit-any */
1460
1708
  /**
@@ -1773,10 +2021,60 @@ var VioProductDetail = class extends LitElement {
1773
2021
  }
1774
2022
  }
1775
2023
  /** Buy with Vipps — opens the checkout with Vipps preselected. */
2024
+ /**
2025
+ * Buy now with an embedded checkout (Kustom, Qliro, Walley).
2026
+ *
2027
+ * Those three have no express path of their own, so the product page showed
2028
+ * a "Kjøp nå" for Apple Pay, Stripe, Klarna and Vipps and nothing at all for
2029
+ * them — the same structural gap the cart footer had. Opens the checkout
2030
+ * without preselecting: with a single method the checkout selects it itself,
2031
+ * with several the shopper picks.
2032
+ *
2033
+ * Like every other "Kjøp nå" here, it adds to the cart first — so a cart
2034
+ * that already had items buys those too. Isolating a single-product purchase
2035
+ * needs a checkout that is not bound to the sponsor's cart, which is a
2036
+ * separate piece of work and deliberately not done here.
2037
+ */
2038
+ buyNow() {
2039
+ if (!this.product || this.availableQuantity <= 0 || this.adding) return;
2040
+ this.addCurrentToCart();
2041
+ this.close();
2042
+ this.dispatchEvent(
2043
+ new CustomEvent("vio:checkout-open", {
2044
+ bubbles: true,
2045
+ composed: true,
2046
+ detail: { sponsorId: this.sponsorId, express: false }
2047
+ })
2048
+ );
2049
+ }
2050
+ /**
2051
+ * Buy with Vipps — Express: the app opens with address and delivery
2052
+ * already there, nothing to fill in here (Vipps' own recommendation,
2053
+ * 2026-09-29). When this cart cannot have Express, or Vipps cannot be
2054
+ * started, the checkout opens with Vipps preselected, as before.
2055
+ */
2056
+ /** Vipps' own button when its script answers; our orange one otherwise. */
2057
+ async loadVippsButton() {
2058
+ this.vippsOfficial = await ensureVippsButton();
2059
+ }
1776
2060
  buyWithVipps() {
1777
2061
  if (!this.product || this.availableQuantity <= 0 || this.adding) return;
1778
2062
  this.addCurrentToCart();
1779
2063
  this.close();
2064
+ void this.startVippsExpressOrCheckout();
2065
+ }
2066
+ async startVippsExpressOrCheckout() {
2067
+ let fallback = true;
2068
+ try {
2069
+ if (!await Vio.checkout.getVippsExpressEnabled(this.sponsorId)) throw new Error("express off");
2070
+ const res = await Vio.checkout.startVippsExpress(this.sponsorId);
2071
+ fallback = !res?.payment_url;
2072
+ } catch (err) {
2073
+ if (typeof console !== "undefined") {
2074
+ console.warn("[VioProductDetail] Vipps Express not started:", err);
2075
+ }
2076
+ }
2077
+ if (!fallback) return;
1780
2078
  this.dispatchEvent(
1781
2079
  new CustomEvent("vio:checkout-open", {
1782
2080
  bubbles: true,
@@ -1818,13 +2116,29 @@ var VioProductDetail = class extends LitElement {
1818
2116
  void this.applePayHandle.show();
1819
2117
  this.close();
1820
2118
  }
2119
+ /** The seller's Company name; the brand when there is none; nothing while asking. */
2120
+ headerName() {
2121
+ if (this.sellerName) return this.sellerName;
2122
+ return this.sellerSettled ? this.product?.brand ?? "" : "";
2123
+ }
2124
+ async loadSeller(productId, seq) {
2125
+ let name = null;
2126
+ try {
2127
+ name = await Vio.commerceFor(this.sponsorId).channel.product.getSellerCompany(productId);
2128
+ } catch {
2129
+ name = null;
2130
+ }
2131
+ if (seq !== this.fetchSeq) return;
2132
+ this.sellerName = name;
2133
+ this.sellerSettled = true;
2134
+ }
1821
2135
  render() {
1822
2136
  return html`
1823
2137
  <div class="backdrop" @click=${this.close}></div>
1824
- <div class="modal" role="dialog" aria-label="Produkt">
2138
+ <div class="modal" role="dialog" aria-modal="true" aria-label="Produkt" tabindex="-1">
1825
2139
  <div class="handle" @click=${this.close} aria-hidden="true"></div>
1826
2140
  <div class="topbar">
1827
- <span class="topbar-brand">${this.product?.brand ?? ""}</span>
2141
+ <span class="topbar-brand">${this.headerName()}</span>
1828
2142
  <button class="close" @click=${this.close} aria-label="Lukk">×</button>
1829
2143
  </div>
1830
2144
 
@@ -1922,6 +2236,17 @@ var VioProductDetail = class extends LitElement {
1922
2236
  </button>
1923
2237
  </div>
1924
2238
 
2239
+ ${this.availableQuantity > 0 && this.methodEnabled(...NO_EXPRESS_BUTTON_METHODS) ? html`
2240
+ <button
2241
+ class="buy-now"
2242
+ @click=${this.buyNow}
2243
+ ?disabled=${this.adding}
2244
+ aria-label="Kjøp nå"
2245
+ >
2246
+ Kjøp nå
2247
+ </button>
2248
+ ` : ""}
2249
+
1925
2250
  ${this.availableQuantity > 0 && this.applePayOk && this.methodEnabled("apple-pay", "applepay") ? html`
1926
2251
  <button
1927
2252
  class="buy-applepay"
@@ -1965,17 +2290,30 @@ var VioProductDetail = class extends LitElement {
1965
2290
  </button>
1966
2291
  ` : ""}
1967
2292
 
1968
- ${this.availableQuantity > 0 && this.methodEnabled("vipps") ? html`
1969
- <button
1970
- class="buy-vipps"
1971
- @click=${this.buyWithVipps}
1972
- ?disabled=${this.adding}
1973
- aria-label="Kjøp nå med Vipps"
1974
- >
1975
- <span>Kjøp nå med</span>
1976
- <span class="vipps-badge">vipps</span>
1977
- </button>
1978
- ` : ""}
2293
+ ${this.availableQuantity > 0 && this.methodEnabled("vipps") ? this.vippsOfficial ? html`
2294
+ <vipps-mobilepay-button
2295
+ class="buy-vipps-official"
2296
+ brand="vipps"
2297
+ language="no"
2298
+ verb="buy"
2299
+ variant="primary"
2300
+ rounded="true"
2301
+ stretched="true"
2302
+ role="button"
2303
+ aria-label="Kjøp nå med Vipps"
2304
+ @click=${this.buyWithVipps}
2305
+ ></vipps-mobilepay-button>
2306
+ ` : html`
2307
+ <button
2308
+ class="buy-vipps"
2309
+ @click=${this.buyWithVipps}
2310
+ ?disabled=${this.adding}
2311
+ aria-label="Kjøp nå med Vipps"
2312
+ >
2313
+ <span>Kjøp nå med</span>
2314
+ <span class="vipps-badge">vipps</span>
2315
+ </button>
2316
+ ` : ""}
1979
2317
 
1980
2318
  ${p.description ? html`
1981
2319
  <div>
@@ -2040,6 +2378,12 @@ __decorateClass([
2040
2378
  __decorateClass([
2041
2379
  state()
2042
2380
  ], VioProductDetail.prototype, "product", 2);
2381
+ __decorateClass([
2382
+ state()
2383
+ ], VioProductDetail.prototype, "sellerName", 2);
2384
+ __decorateClass([
2385
+ state()
2386
+ ], VioProductDetail.prototype, "sellerSettled", 2);
2043
2387
  __decorateClass([
2044
2388
  state()
2045
2389
  ], VioProductDetail.prototype, "isLoading", 2);
@@ -2064,6 +2408,9 @@ __decorateClass([
2064
2408
  __decorateClass([
2065
2409
  state()
2066
2410
  ], VioProductDetail.prototype, "availableMethods", 2);
2411
+ __decorateClass([
2412
+ state()
2413
+ ], VioProductDetail.prototype, "vippsOfficial", 2);
2067
2414
  function normalizeValues(raw) {
2068
2415
  if (Array.isArray(raw)) {
2069
2416
  return raw.map((v) => String(v).trim()).filter((v) => v.length > 0);
@@ -2087,6 +2434,9 @@ var VioCart = class extends LitElement {
2087
2434
  this.itemCount = 0;
2088
2435
  this.applePayAvailable = false;
2089
2436
  this.availableMethods = null;
2437
+ this.vippsOfficial = false;
2438
+ /** Whether the method lookup has been attempted — see boundOnCartChange. */
2439
+ this.paymentMethodsAttempted = false;
2090
2440
  this.confirmedOrder = null;
2091
2441
  /** Pre-resolved Apple Pay handle; `show()` fires the sheet synchronously. */
2092
2442
  this.applePayHandle = null;
@@ -2094,11 +2444,18 @@ var VioCart = class extends LitElement {
2094
2444
  this.preparedTotal = -1;
2095
2445
  /** In-flight guard for loadAvailablePaymentMethods. */
2096
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
+ });
2097
2453
  this.boundOnCartChange = (e) => {
2098
2454
  const detail = e.detail;
2099
2455
  this.carts = detail.cartsBySponsor;
2100
2456
  this.itemCount = detail.itemCount;
2101
- if (this.availableMethods === null) {
2457
+ if (!this.paymentMethodsAttempted) {
2458
+ this.paymentMethodsAttempted = true;
2102
2459
  void this.loadAvailablePaymentMethods();
2103
2460
  }
2104
2461
  };
@@ -2135,6 +2492,8 @@ var VioCart = class extends LitElement {
2135
2492
  box-shadow: -8px 0 24px rgba(0, 0, 0, 0.06);
2136
2493
  }
2137
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; }
2138
2497
 
2139
2498
  .handle { display: none; }
2140
2499
 
@@ -2147,7 +2506,7 @@ var VioCart = class extends LitElement {
2147
2506
  bottom: 0;
2148
2507
  width: 100%;
2149
2508
  height: 88vh;
2150
- border-radius: 16px 16px 0 0;
2509
+ border-radius: var(--vio-radius-xl, 16px) var(--vio-radius-xl, 16px) 0 0;
2151
2510
  transform: translateY(100%);
2152
2511
  box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.08);
2153
2512
  }
@@ -2162,7 +2521,7 @@ var VioCart = class extends LitElement {
2162
2521
  width: 40px;
2163
2522
  height: 4px;
2164
2523
  background: var(--vio-color-border, #e5e5e5);
2165
- border-radius: 2px;
2524
+ border-radius: var(--vio-radius-sm, 2px);
2166
2525
  }
2167
2526
  }
2168
2527
 
@@ -2255,7 +2614,7 @@ var VioCart = class extends LitElement {
2255
2614
  height: 24px;
2256
2615
  border: 1px solid var(--vio-color-border, #e5e5e5);
2257
2616
  background: #fff;
2258
- border-radius: 2px;
2617
+ border-radius: var(--vio-radius-sm, 2px);
2259
2618
  cursor: pointer;
2260
2619
  font-size: 12px;
2261
2620
  color: var(--vio-color-text, #0a0a0a);
@@ -2273,6 +2632,23 @@ var VioCart = class extends LitElement {
2273
2632
  font-size: 14px;
2274
2633
  }
2275
2634
 
2635
+ /* Always present: the express buttons below are shortcuts, not the only
2636
+ way out of the cart. Without this a channel offering only an embedded
2637
+ checkout (Kustom, Qliro, Walley) had no button at all. */
2638
+ .checkout-btn {
2639
+ width: 100%;
2640
+ padding: 14px;
2641
+ margin-bottom: var(--vio-space-sm, 8px);
2642
+ background: var(--vio-color-text, #0a0a0a);
2643
+ color: var(--vio-color-text-on-primary, #fff);
2644
+ border: none;
2645
+ border-radius: var(--vio-radius-md, 4px);
2646
+ font-family: var(--vio-font-sans, inherit);
2647
+ font-size: var(--vio-size-sm, 14px);
2648
+ font-weight: 600;
2649
+ cursor: pointer;
2650
+ }
2651
+ .checkout-btn:hover { opacity: 0.9; }
2276
2652
  .footer {
2277
2653
  padding: var(--vio-space-lg, 24px);
2278
2654
  border-top: 1px solid var(--vio-color-border, #e5e5e5);
@@ -2295,7 +2671,7 @@ var VioCart = class extends LitElement {
2295
2671
  background: #000;
2296
2672
  color: #fff;
2297
2673
  border: none;
2298
- border-radius: 8px;
2674
+ border-radius: var(--vio-radius-lg, 8px);
2299
2675
  font-size: 19px;
2300
2676
  font-weight: 500;
2301
2677
  line-height: 1;
@@ -2317,7 +2693,7 @@ var VioCart = class extends LitElement {
2317
2693
  background: #ffb3c7;
2318
2694
  color: #0a0a0a;
2319
2695
  border: none;
2320
- border-radius: 8px;
2696
+ border-radius: var(--vio-radius-lg, 8px);
2321
2697
  font-size: 16px;
2322
2698
  font-weight: 600;
2323
2699
  cursor: pointer;
@@ -2338,7 +2714,7 @@ var VioCart = class extends LitElement {
2338
2714
  background: #ff5b24;
2339
2715
  color: #fff;
2340
2716
  border: none;
2341
- border-radius: 8px;
2717
+ border-radius: var(--vio-radius-lg, 8px);
2342
2718
  cursor: pointer;
2343
2719
  display: flex;
2344
2720
  align-items: center;
@@ -2347,6 +2723,7 @@ var VioCart = class extends LitElement {
2347
2723
  transition: background 0.15s;
2348
2724
  }
2349
2725
  .vipps-btn:hover { background: #ec4f1c; }
2726
+ .vipps-btn-official { display: block; width: 100%; margin-bottom: 10px; cursor: pointer; }
2350
2727
  .vipps-btn .vipps-logo {
2351
2728
  font-size: 20px;
2352
2729
  font-weight: 800;
@@ -2361,7 +2738,7 @@ var VioCart = class extends LitElement {
2361
2738
  background: #635bff;
2362
2739
  color: #fff;
2363
2740
  border: none;
2364
- border-radius: 8px;
2741
+ border-radius: var(--vio-radius-lg, 8px);
2365
2742
  font-size: 16px;
2366
2743
  font-weight: 600;
2367
2744
  cursor: pointer;
@@ -2445,6 +2822,7 @@ var VioCart = class extends LitElement {
2445
2822
  }
2446
2823
  connectedCallback() {
2447
2824
  super.connectedCallback();
2825
+ void this.loadVippsButton();
2448
2826
  this.carts = Vio.cart.getAllCarts();
2449
2827
  this.itemCount = Vio.cart.itemCount;
2450
2828
  Vio.cart.addEventListener("change", this.boundOnCartChange);
@@ -2494,12 +2872,11 @@ var VioCart = class extends LitElement {
2494
2872
  }
2495
2873
  }
2496
2874
  /** Backend names arrive as e.g. "Apple Pay" — compare letters only. */
2497
- methodEnabled(name) {
2498
- if (this.availableMethods === null) return true;
2499
- const norm = (s) => s.toLowerCase().replace(/[^a-z]/g, "");
2500
- return this.availableMethods.some((m) => norm(m) === norm(name));
2875
+ methodEnabled(...names) {
2876
+ return isMethodEnabled(this.availableMethods, ...names);
2501
2877
  }
2502
2878
  updated(changed) {
2879
+ if (changed.has("open")) this.dialogFocus.update(this.open);
2503
2880
  if (changed.has("open") && this.open) {
2504
2881
  this.confirmedOrder = null;
2505
2882
  this.preparedTotal = -1;
@@ -2585,15 +2962,59 @@ var VioCart = class extends LitElement {
2585
2962
  );
2586
2963
  }
2587
2964
  /** Vipps — opens the checkout with Vipps preselected. */
2965
+ /**
2966
+ * Open the checkout without preselecting a method.
2967
+ *
2968
+ * The footer used to be express-only — Apple Pay, Stripe, Klarna, Vipps —
2969
+ * so a channel offering just an embedded checkout (Kustom, Qliro, Walley)
2970
+ * had NO button at all and the cart was a dead end. Even with an express
2971
+ * method present the others were unreachable from here.
2972
+ */
2973
+ onCheckout() {
2974
+ if (this.itemCount === 0) return;
2975
+ const firstSponsorId = [...this.carts.keys()][0];
2976
+ if (firstSponsorId === void 0) return;
2977
+ this.dispatchEvent(
2978
+ new CustomEvent("vio:checkout-open", {
2979
+ bubbles: true,
2980
+ composed: true,
2981
+ detail: { sponsorId: firstSponsorId, express: false }
2982
+ })
2983
+ );
2984
+ }
2985
+ /**
2986
+ * Vipps — Express: the app collects address and delivery, so the cart
2987
+ * goes straight to Vipps. When this cart cannot have Express (several
2988
+ * suppliers, digital goods, no rate) or Vipps cannot be started, the
2989
+ * checkout opens with Vipps preselected, as before.
2990
+ */
2588
2991
  onVipps() {
2589
2992
  if (this.itemCount === 0) return;
2590
2993
  const firstSponsorId = [...this.carts.keys()][0];
2591
2994
  if (firstSponsorId === void 0) return;
2995
+ void this.startVippsExpressOrCheckout(firstSponsorId);
2996
+ }
2997
+ /** Vipps' own button when its script answers; our orange one otherwise. */
2998
+ async loadVippsButton() {
2999
+ this.vippsOfficial = await ensureVippsButton();
3000
+ }
3001
+ async startVippsExpressOrCheckout(sponsorId) {
3002
+ let fallback = true;
3003
+ try {
3004
+ if (!await Vio.checkout.getVippsExpressEnabled(sponsorId)) throw new Error("express off");
3005
+ const res = await Vio.checkout.startVippsExpress(sponsorId);
3006
+ fallback = !res?.payment_url;
3007
+ } catch (err) {
3008
+ if (typeof console !== "undefined") {
3009
+ console.warn("[VioCart] Vipps Express not started:", err);
3010
+ }
3011
+ }
3012
+ if (!fallback) return;
2592
3013
  this.dispatchEvent(
2593
3014
  new CustomEvent("vio:checkout-open", {
2594
3015
  bubbles: true,
2595
3016
  composed: true,
2596
- detail: { sponsorId: firstSponsorId, paymentMethod: "vipps", express: false }
3017
+ detail: { sponsorId, paymentMethod: "vipps", express: false }
2597
3018
  })
2598
3019
  );
2599
3020
  }
@@ -2635,7 +3056,7 @@ var VioCart = class extends LitElement {
2635
3056
  const sponsorCarts = [...this.carts.values()].filter((c) => c.items.length > 0);
2636
3057
  return html`
2637
3058
  <div class="backdrop" @click=${this.close}></div>
2638
- <aside class="drawer" role="dialog" aria-label=${this.heading}>
3059
+ <aside class="drawer" role="dialog" aria-modal="true" aria-label=${this.heading} tabindex="-1">
2639
3060
  <div class="handle" @click=${this.close} aria-hidden="true"></div>
2640
3061
  <div class="header">
2641
3062
  <h2 class="heading">${this.confirmedOrder ? "Takk!" : this.heading}</h2>
@@ -2705,6 +3126,13 @@ var VioCart = class extends LitElement {
2705
3126
  ${formatPrice(this.totalAcrossSponsors, this.primaryCurrency)}
2706
3127
  </span>
2707
3128
  </div>
3129
+ <button
3130
+ class="checkout-btn"
3131
+ @click=${this.onCheckout}
3132
+ aria-label="Til kassen"
3133
+ >
3134
+ Til kassen
3135
+ </button>
2708
3136
  ${this.methodEnabled("apple-pay") && this.applePayAvailable ? html`
2709
3137
  <button
2710
3138
  class="applepay-btn"
@@ -2738,15 +3166,28 @@ var VioCart = class extends LitElement {
2738
3166
  />
2739
3167
  </button>
2740
3168
  ` : ""}
2741
- ${this.methodEnabled("vipps") ? html`
2742
- <button
2743
- class="vipps-btn"
2744
- @click=${this.onVipps}
2745
- aria-label="Betal med Vipps"
2746
- >
2747
- <span class="vipps-logo">vipps</span>
2748
- </button>
2749
- ` : ""}
3169
+ ${this.methodEnabled("vipps") ? this.vippsOfficial ? html`
3170
+ <vipps-mobilepay-button
3171
+ class="vipps-btn-official"
3172
+ brand="vipps"
3173
+ language="no"
3174
+ verb="pay"
3175
+ variant="primary"
3176
+ rounded="true"
3177
+ stretched="true"
3178
+ role="button"
3179
+ aria-label="Betal med Vipps"
3180
+ @click=${this.onVipps}
3181
+ ></vipps-mobilepay-button>
3182
+ ` : html`
3183
+ <button
3184
+ class="vipps-btn"
3185
+ @click=${this.onVipps}
3186
+ aria-label="Betal med Vipps"
3187
+ >
3188
+ <span class="vipps-logo">vipps</span>
3189
+ </button>
3190
+ ` : ""}
2750
3191
  </div>
2751
3192
  `}
2752
3193
  </aside>
@@ -2803,9 +3244,44 @@ __decorateClass([
2803
3244
  __decorateClass([
2804
3245
  state()
2805
3246
  ], VioCart.prototype, "availableMethods", 2);
3247
+ __decorateClass([
3248
+ state()
3249
+ ], VioCart.prototype, "vippsOfficial", 2);
2806
3250
  __decorateClass([
2807
3251
  state()
2808
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
2809
3285
  var STRIPE_LOGO_SRC3 = "data:image/svg+xml;utf8," + encodeURIComponent(
2810
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>'
2811
3287
  );
@@ -2822,9 +3298,13 @@ var _VioCheckout = class _VioCheckout extends LitElement {
2822
3298
  this.applePayInProgress = false;
2823
3299
  this.klarnaAvailable = false;
2824
3300
  this.orderConfirmed = false;
3301
+ this.providerReceipt = null;
2825
3302
  this.confirmedMethod = null;
2826
3303
  this.paymentError = null;
2827
3304
  this.paymentNotice = null;
3305
+ this.verifyingReturn = null;
3306
+ /** The checkout whose confirmation is on screen — see fillOrderNumber. */
3307
+ this.confirmedCheckoutId = null;
2828
3308
  this.confirmedOrder = null;
2829
3309
  /** Active Klarna Payments widget handle + the subtotal it was mounted for. */
2830
3310
  this.klarnaHandle = null;
@@ -2837,12 +3317,70 @@ var _VioCheckout = class _VioCheckout extends LitElement {
2837
3317
  this.selectedShipping = "";
2838
3318
  this.availableShippingsList = [];
2839
3319
  this.availableMethods = null;
3320
+ this.vippsOfficial = false;
2840
3321
  this.kustomMounting = false;
3322
+ this.kustomSyncing = false;
3323
+ this.kustomShipping = null;
3324
+ this.kustomTotal = null;
3325
+ this.kustomNotice = null;
3326
+ /** The cart the mounted Kustom order describes — see kustomCartKey. */
3327
+ this.kustomMountedKey = null;
2841
3328
  this.kustomMountedOrderId = null;
3329
+ /** The checkout session (see embedSession) each embedded widget's order belongs to. */
3330
+ this.kustomMountedFor = null;
3331
+ this.qliroMountedFor = null;
3332
+ this.walleyMountedFor = null;
3333
+ this.nexiMountedFor = null;
2842
3334
  this.qliroMounting = false;
2843
3335
  this.qliroMountedOrderId = null;
2844
3336
  this.walleyMounting = false;
2845
3337
  this.walleyMountedOrderId = null;
3338
+ this.nexiMounting = false;
3339
+ this.nexiMountedOrderId = null;
3340
+ this.nexiShippingNotice = null;
3341
+ this.nexiShipping = null;
3342
+ this.nexiPicking = false;
3343
+ /** The Nexi payment to resume on the next mount — set only by the ?paymentId= return. */
3344
+ this.nexiResumePaymentId = null;
3345
+ this.adyenMounting = false;
3346
+ this.adyenRequested = false;
3347
+ this.adyenMounted = false;
3348
+ this.adyenStale = false;
3349
+ this.adyenSettled = false;
3350
+ /** The checkout session (see embedSession) and the purchase the mounted photo is of. */
3351
+ this.adyenMountedFor = null;
3352
+ this.adyenMountedKey = null;
3353
+ this.stripeMode = null;
3354
+ this.stripeRequested = false;
3355
+ this.stripeMounting = false;
3356
+ this.stripeMounted = false;
3357
+ this.stripeStale = false;
3358
+ this.stripeSettled = false;
3359
+ this.stripePaying = false;
3360
+ this.stripeReady = false;
3361
+ this.stripeMountedFor = null;
3362
+ this.stripeMountedKey = null;
3363
+ /** The checkout the mounted PaymentIntent pays for. */
3364
+ this.stripeCheckoutId = null;
3365
+ this.stripeElement = null;
3366
+ this.qliroShipping = null;
3367
+ /**
3368
+ * Set while this page load is showing the outcome of an embedded checkout's
3369
+ * confirmation redirect (Kustom, Qliro, Walley). While set, nothing may start
3370
+ * a new payment.
3371
+ *
3372
+ * The return handler selects the method so the panel has something to show,
3373
+ * and only THEN asks the provider whether the order was paid. Selecting is a
3374
+ * state change; the render it schedules runs `updated()` → `mount*IfNeeded()`
3375
+ * long before the provider answers, and in a fresh page load there is no
3376
+ * mounted order to stop it — so every return created a new backend checkout
3377
+ * and a new provider order for a cart that had just been paid. Not a race
3378
+ * that needs a slow network: a microtask always beats a round trip.
3379
+ * Reproduced in `vio-checkout.return.dom.test.ts` before this existed.
3380
+ *
3381
+ * Cleared when the checkout closes: reopening it is a fresh checkout.
3382
+ */
3383
+ this.returningFrom = null;
2846
3384
  this.stripeLoading = false;
2847
3385
  this.vippsLoading = false;
2848
3386
  this.form = {
@@ -2855,10 +3393,41 @@ var _VioCheckout = class _VioCheckout extends LitElement {
2855
3393
  };
2856
3394
  /** In-flight guard for loadAvailablePaymentMethods. */
2857
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
+ });
3406
+ this.paymentMethodsResolved = false;
3407
+ /** Guards `autoSelectSoleMethod` to ONE attempt per opening — see there. */
3408
+ this.autoSelectAttempted = false;
2858
3409
  /** In-flight + attempted guards for loadAvailableShippings (change events
2859
3410
  * fire per keystroke — without these, typing an address hammers the API). */
2860
3411
  this.loadingShippings = false;
2861
3412
  this.shippingsAttempted = false;
3413
+ this.shippingBlocked = false;
3414
+ this.shippingsReadyFor = null;
3415
+ this.boundOnQliroEvent = (e) => {
3416
+ const event = e.detail;
3417
+ if (!event) return;
3418
+ if (event.type === "shipping-price-changed" && typeof event.price === "number") {
3419
+ this.qliroShipping = { ...this.qliroShipping ?? {}, price: event.price };
3420
+ } else if (event.type === "shipping-changed") {
3421
+ const shipping = event.shipping ?? {};
3422
+ const ref = shipping.method ?? shipping.merchantReference ?? shipping.MerchantReference;
3423
+ const price = typeof shipping.price === "number" ? shipping.price : this.availableShippingsList.find((o) => o.id === String(ref))?.priceMajor;
3424
+ this.qliroShipping = {
3425
+ ...this.qliroShipping ?? {},
3426
+ ...ref != null ? { ref: String(ref) } : {},
3427
+ ...typeof price === "number" ? { price } : {}
3428
+ };
3429
+ }
3430
+ };
2862
3431
  this.boundOnCheckoutChange = (e) => {
2863
3432
  const detail = e.detail;
2864
3433
  const prevSponsorId = this.checkoutState?.sponsorId;
@@ -2866,7 +3435,8 @@ var _VioCheckout = class _VioCheckout extends LitElement {
2866
3435
  this.checkoutState = detail.state;
2867
3436
  this.items = Vio.checkout.items;
2868
3437
  if (this.checkoutState) {
2869
- if (sponsorChanged || this.availableMethods === null) {
3438
+ if (sponsorChanged || !this.paymentMethodsResolved) {
3439
+ if (sponsorChanged) this.paymentMethodsResolved = false;
2870
3440
  void this.refreshApplePay();
2871
3441
  void this.refreshKlarna();
2872
3442
  void this.loadAvailablePaymentMethods();
@@ -2878,13 +3448,28 @@ var _VioCheckout = class _VioCheckout extends LitElement {
2878
3448
  this.applePayAvailable = false;
2879
3449
  this.klarnaAvailable = false;
2880
3450
  this.orderConfirmed = false;
3451
+ this.providerReceipt = null;
2881
3452
  this.confirmedMethod = null;
2882
3453
  this.confirmedOrder = null;
2883
3454
  this.paymentError = null;
2884
3455
  this.paymentNotice = null;
3456
+ this.verifyingReturn = null;
3457
+ this.confirmedCheckoutId = null;
2885
3458
  this.express = false;
2886
3459
  this.availableMethods = null;
3460
+ this.paymentMethodsResolved = false;
3461
+ this.autoSelectAttempted = false;
3462
+ this.returningFrom = null;
3463
+ this.shippingBlocked = false;
2887
3464
  this.unmountKlarna();
3465
+ this.unmountKustom();
3466
+ this.unmountQliro();
3467
+ this.unmountWalley();
3468
+ this.unmountNexi();
3469
+ this.unmountAdyen();
3470
+ this.adyenSettled = false;
3471
+ this.unmountStripe();
3472
+ this.stripeSettled = false;
2888
3473
  }
2889
3474
  };
2890
3475
  this.boundOnPaymentComplete = (e) => {
@@ -2896,10 +3481,17 @@ var _VioCheckout = class _VioCheckout extends LitElement {
2896
3481
  };
2897
3482
  this.boundOnPaymentError = (e) => {
2898
3483
  const detail = e.detail;
2899
- 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);
2900
3488
  this.open = true;
2901
3489
  };
2902
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
+ }
2903
3495
  static {
2904
3496
  this.styles = css`
2905
3497
  :host { display: contents; }
@@ -2928,6 +3520,8 @@ var _VioCheckout = class _VioCheckout extends LitElement {
2928
3520
  overflow-y: auto;
2929
3521
  }
2930
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; }
2931
3525
 
2932
3526
  /* Confirmation drawer — compact bottom sheet, not the full-screen overlay. */
2933
3527
  .modal.as-drawer {
@@ -2966,10 +3560,12 @@ var _VioCheckout = class _VioCheckout extends LitElement {
2966
3560
  :host([open]) .modal.as-side { transform: translateX(0); }
2967
3561
 
2968
3562
  /* Desktop: the checkout ALWAYS lives as a right-side panel (like the cart,
2969
- product detail and express drawer) — never a full-width overlay. Only the
2970
- compact confirmation card stays centered. */
3563
+ product detail and express drawer) — never a full-width overlay. The
3564
+ confirmation stays in that panel too: a centered card jumped out of the
3565
+ panel the shopper was in and looked like the phone layout (Angelo,
3566
+ 2026-09-16). */
2971
3567
  @media (min-width: 601px) {
2972
- .modal:not(.as-confirm) {
3568
+ .modal {
2973
3569
  inset: 0 0 0 auto;
2974
3570
  width: min(440px, 100%);
2975
3571
  max-width: 440px;
@@ -2977,7 +3573,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
2977
3573
  border-radius: 0;
2978
3574
  box-shadow: -8px 0 24px rgba(0, 0, 0, 0.1);
2979
3575
  }
2980
- :host([open]) .modal:not(.as-confirm) { transform: translateX(0); }
3576
+ :host([open]) .modal { transform: translateX(0); }
2981
3577
  }
2982
3578
  @media (max-width: 600px) {
2983
3579
  /* On phones, keep it a bottom sheet for reachability. */
@@ -2992,37 +3588,12 @@ var _VioCheckout = class _VioCheckout extends LitElement {
2992
3588
  :host([open]) .modal.as-side { transform: translateY(0); }
2993
3589
  }
2994
3590
 
2995
- /* Confirmation: a centered desktop dialog (the bottom-sheet looked like
2996
- mobile on desktop). Falls back to a bottom sheet on phones. */
2997
- .modal.as-confirm {
2998
- inset: auto;
2999
- top: 50%;
3000
- left: 50%;
3001
- width: min(92vw, 440px);
3002
- max-width: 440px;
3003
- height: auto;
3004
- max-height: 88vh;
3005
- border-radius: var(--vio-radius-xl, 16px);
3006
- box-shadow: 0 30px 80px -24px rgba(0, 0, 0, 0.4);
3007
- transform: translate(-50%, -50%) scale(0.96);
3008
- opacity: 0;
3009
- }
3010
- :host([open]) .modal.as-confirm {
3011
- transform: translate(-50%, -50%) scale(1);
3012
- opacity: 1;
3013
- }
3591
+ /* Phones: the confirmation is a compact bottom sheet, as tall as it needs. */
3014
3592
  @media (max-width: 600px) {
3015
3593
  .modal.as-confirm {
3016
- inset: auto 0 0 0;
3017
- top: auto;
3018
- left: 0;
3019
- width: 100%;
3020
- max-width: none;
3021
- border-radius: var(--vio-radius-xl, 16px) var(--vio-radius-xl, 16px) 0 0;
3022
- transform: translateY(100%) scale(1);
3023
- opacity: 1;
3594
+ height: auto;
3595
+ max-height: 88vh;
3024
3596
  }
3025
- :host([open]) .modal.as-confirm { transform: translateY(0); }
3026
3597
  }
3027
3598
 
3028
3599
  .handle { display: none; }
@@ -3139,6 +3710,61 @@ var _VioCheckout = class _VioCheckout extends LitElement {
3139
3710
  grid-template-columns: 1fr 1fr;
3140
3711
  gap: var(--vio-space-md, 16px);
3141
3712
  }
3713
+ /* Shown until we know which methods the channel offers. Rendering every
3714
+ button meanwhile would let a shopper click one that is not enabled. */
3715
+ .payment-loading {
3716
+ display: flex;
3717
+ align-items: center;
3718
+ gap: 10px;
3719
+ padding: 20px 0;
3720
+ font-size: 13px;
3721
+ color: var(--vio-color-text-secondary, #666);
3722
+ }
3723
+ .payment-spinner {
3724
+ width: 14px;
3725
+ height: 14px;
3726
+ border: 2px solid var(--vio-color-border, #e5e5e5);
3727
+ border-top-color: var(--vio-color-text-secondary, #666);
3728
+ border-radius: 50%;
3729
+ animation: vio-payment-spin 0.7s linear infinite;
3730
+ }
3731
+ @keyframes vio-payment-spin {
3732
+ to { transform: rotate(360deg); }
3733
+ }
3734
+ @media (prefers-reduced-motion: reduce) {
3735
+ .payment-spinner { animation: none; }
3736
+ }
3737
+ /* Back from a payment page: only this until we know. */
3738
+ .verifying {
3739
+ display: flex;
3740
+ flex-direction: column;
3741
+ align-items: center;
3742
+ text-align: center;
3743
+ gap: 10px;
3744
+ padding: 56px 16px;
3745
+ }
3746
+ .verifying-spinner {
3747
+ width: 28px;
3748
+ height: 28px;
3749
+ border: 3px solid var(--vio-color-border, #e5e5e5);
3750
+ border-top-color: var(--vio-color-accent, #c14a3b);
3751
+ border-radius: 50%;
3752
+ animation: vio-payment-spin 0.8s linear infinite;
3753
+ }
3754
+ .verifying-text {
3755
+ margin: 6px 0 0;
3756
+ font-size: 15px;
3757
+ font-weight: 600;
3758
+ color: var(--vio-color-text, #1a1a1a);
3759
+ }
3760
+ .verifying-hint {
3761
+ margin: 0;
3762
+ font-size: 13px;
3763
+ color: var(--vio-color-text-secondary, #666);
3764
+ }
3765
+ @media (prefers-reduced-motion: reduce) {
3766
+ .verifying-spinner { animation: none; }
3767
+ }
3142
3768
  .payment-btn {
3143
3769
  padding: 20px;
3144
3770
  background: #fff;
@@ -3175,13 +3801,6 @@ var _VioCheckout = class _VioCheckout extends LitElement {
3175
3801
  .payment-btn[aria-pressed='true'] {
3176
3802
  border-color: var(--vio-color-accent, #c14a3b);
3177
3803
  }
3178
- .apple-pay-note {
3179
- grid-column: 1 / -1;
3180
- font-size: 12px;
3181
- color: var(--vio-color-text-tertiary, #999);
3182
- text-align: center;
3183
- margin-top: 4px;
3184
- }
3185
3804
  /* Klarna Payments — inline widget panel, shown once Klarna is selected. */
3186
3805
  .klarna-panel {
3187
3806
  grid-column: 1 / -1;
@@ -3223,6 +3842,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
3223
3842
  }
3224
3843
  /* Shipping selector (express) — Standard / Express options. */
3225
3844
  .ship-select { display: flex; flex-direction: column; gap: 8px; }
3845
+ .nexi-ship { margin-bottom: 12px; }
3226
3846
  .ship-opt {
3227
3847
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
3228
3848
  padding: 12px 14px; border: 1px solid var(--vio-color-border, #ddd);
@@ -3272,58 +3892,103 @@ var _VioCheckout = class _VioCheckout extends LitElement {
3272
3892
 
3273
3893
  /* Order confirmation — compact, shown inside the drawer. */
3274
3894
  .confirmation {
3275
- text-align: center;
3276
- padding: 24px 24px 28px;
3895
+ padding: 8px 0 28px;
3277
3896
  display: flex;
3278
3897
  flex-direction: column;
3279
- align-items: center;
3280
- gap: 14px;
3898
+ gap: 20px;
3281
3899
  }
3282
- .confirm-check {
3283
- width: 64px;
3284
- height: 64px;
3285
- border-radius: 50%;
3286
- background: #2e7d32;
3287
- color: #fff;
3900
+ .confirm-head {
3288
3901
  display: flex;
3289
3902
  align-items: center;
3290
- justify-content: center;
3291
- font-size: 32px;
3292
- line-height: 1;
3293
- }
3294
- .confirm-title {
3295
- font-family: var(--vio-font-serif, Georgia, serif);
3296
- font-size: 28px;
3297
- font-weight: 400;
3298
- margin: 0;
3903
+ gap: 16px;
3299
3904
  }
3300
- .confirm-text {
3301
- font-size: 14px;
3302
- color: var(--vio-color-text-secondary, #666);
3303
- max-width: 360px;
3304
- line-height: 1.6;
3905
+ .confirm-details {
3305
3906
  margin: 0;
3907
+ display: flex;
3908
+ flex-direction: column;
3909
+ border: 1px solid var(--vio-color-border, #eee);
3910
+ border-radius: var(--vio-radius-lg, 8px);
3306
3911
  }
3307
- .confirm-order-id {
3912
+ .confirm-details > div {
3913
+ display: flex;
3914
+ justify-content: space-between;
3915
+ gap: 16px;
3916
+ padding: 10px 14px;
3308
3917
  font-size: 13px;
3309
- color: var(--vio-color-text-secondary, #666);
3310
3918
  }
3311
- .confirm-order-id b {
3312
- color: var(--vio-color-text, #0a0a0a);
3313
- font-variant-numeric: tabular-nums;
3919
+ .confirm-details > div + div { border-top: 1px solid var(--vio-color-border, #eee); }
3920
+ .confirm-details dt { color: var(--vio-color-text-secondary, #666); white-space: nowrap; }
3921
+ .confirm-details dd.ref { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-weight: 500; font-size: 12px; }
3922
+ .confirm-details dd {
3923
+ margin: 0;
3924
+ font-weight: 600;
3925
+ text-align: right;
3926
+ font-variant-numeric: tabular-nums;
3927
+ word-break: break-all;
3928
+ }
3929
+ .confirm-label {
3930
+ font-size: 11px;
3931
+ text-transform: uppercase;
3932
+ letter-spacing: var(--vio-tracking-label, 0.1em);
3933
+ color: var(--vio-color-text-secondary, #666);
3934
+ }
3935
+ .confirm-line {
3936
+ display: grid;
3937
+ grid-template-columns: 56px 1fr auto;
3938
+ gap: 12px;
3939
+ align-items: center;
3940
+ }
3941
+ .confirm-thumb {
3942
+ width: 56px;
3943
+ height: 56px;
3944
+ object-fit: cover;
3945
+ background: var(--vio-color-surface-muted, #f2f2f2);
3946
+ display: block;
3947
+ }
3948
+ .confirm-line-name { display: flex; flex-direction: column; gap: 2px; font-size: 14px; }
3949
+ .confirm-line-name small { font-size: 11px; color: var(--vio-color-text-secondary, #666); }
3950
+ .confirm-line-price { font-size: 14px; font-weight: 600; }
3951
+ .confirm-check {
3952
+ flex-shrink: 0;
3953
+ width: 48px;
3954
+ height: 48px;
3955
+ border-radius: 50%;
3956
+ background: #2e7d32;
3957
+ color: #fff;
3958
+ display: flex;
3959
+ align-items: center;
3960
+ justify-content: center;
3961
+ font-size: 24px;
3962
+ line-height: 1;
3963
+ }
3964
+ .confirm-title {
3965
+ font-family: var(--vio-font-serif, Georgia, serif);
3966
+ font-size: 24px;
3967
+ font-weight: 400;
3968
+ margin: 0;
3969
+ }
3970
+ .confirm-text {
3971
+ font-size: 14px;
3972
+ color: var(--vio-color-text-secondary, #666);
3973
+ line-height: 1.5;
3974
+ margin: 4px 0 0;
3975
+ }
3976
+ .confirm-order-id {
3977
+ font-size: 13px;
3978
+ color: var(--vio-color-text-secondary, #666);
3979
+ }
3980
+ .confirm-order-id b {
3981
+ color: var(--vio-color-text, #0a0a0a);
3982
+ font-variant-numeric: tabular-nums;
3314
3983
  }
3315
3984
  .confirm-summary {
3316
- width: 100%;
3317
- max-width: 360px;
3318
- border-top: 1px solid var(--vio-color-border, #eee);
3319
- padding-top: 14px;
3320
3985
  display: flex;
3321
3986
  flex-direction: column;
3322
- gap: 8px;
3323
- text-align: left;
3987
+ gap: 12px;
3324
3988
  font-size: 13px;
3325
3989
  }
3326
3990
  .confirm-close {
3991
+ width: 100%;
3327
3992
  border: none;
3328
3993
  background: var(--vio-color-text, #0a0a0a);
3329
3994
  color: var(--vio-color-text-on-primary, #fff);
@@ -3388,11 +4053,13 @@ var _VioCheckout = class _VioCheckout extends LitElement {
3388
4053
  }
3389
4054
  connectedCallback() {
3390
4055
  super.connectedCallback();
4056
+ void this.loadVippsButton();
3391
4057
  this.checkoutState = Vio.checkout.currentState;
3392
4058
  this.items = Vio.checkout.items;
3393
4059
  Vio.checkout.addEventListener("change", this.boundOnCheckoutChange);
3394
4060
  Vio.checkout.addEventListener("payment-complete", this.boundOnPaymentComplete);
3395
4061
  Vio.checkout.addEventListener("payment-error", this.boundOnPaymentError);
4062
+ Vio.checkout.addEventListener("qliro-event", this.boundOnQliroEvent);
3396
4063
  if (this.checkoutState) {
3397
4064
  void this.refreshApplePay();
3398
4065
  void this.refreshKlarna();
@@ -3400,6 +4067,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
3400
4067
  }
3401
4068
  void Vio.checkout.resumeKlarnaReturn();
3402
4069
  void this.checkReturnPaymentStatus();
4070
+ this.requestUpdate();
3403
4071
  }
3404
4072
  static {
3405
4073
  /**
@@ -3435,6 +4103,18 @@ var _VioCheckout = class _VioCheckout extends LitElement {
3435
4103
  "unpaid"
3436
4104
  ]);
3437
4105
  }
4106
+ static {
4107
+ /**
4108
+ * Shown instead of the payment step when the cart's products cannot ship
4109
+ * together. Angelo, 2026-09-16: block the purchase rather than sell it
4110
+ * without shipping, or with a rate that does not fit every product.
4111
+ */
4112
+ this.SHIPPING_BLOCKED_MESSAGE = "Produktene i handlekurven kan ikke sendes sammen. Fjern ett av dem for \xE5 fullf\xF8re kj\xF8pet.";
4113
+ }
4114
+ static {
4115
+ /** The payment may or may not have gone through, and we could not find out. */
4116
+ this.RETURN_UNVERIFIED_MESSAGE = "Vi kunne ikke bekrefte betalingen. Handlekurven er uendret \u2014 pr\xF8v igjen, eller kontakt support hvis du ble belastet.";
4117
+ }
3438
4118
  static {
3439
4119
  /** Waits between GetCheckout attempts — PSP webhooks (Vipps especially) can
3440
4120
  * land seconds after the shopper is redirected back, so a single immediate
@@ -3457,6 +4137,10 @@ var _VioCheckout = class _VioCheckout extends LitElement {
3457
4137
  * anything else (including an unrecognized state) is treated as failed —
3458
4138
  * fail-closed by default rather than guessing.
3459
4139
  */
4140
+ /** Vipps' own button when its script answers; our orange one otherwise. */
4141
+ async loadVippsButton() {
4142
+ this.vippsOfficial = await ensureVippsButton();
4143
+ }
3460
4144
  async pollVippsStatus(checkoutId, sponsorId) {
3461
4145
  let sawStatus = false;
3462
4146
  for (let attempt = 0; attempt < _VioCheckout.VIPPS_STATUS_MAX_ATTEMPTS; attempt++) {
@@ -3467,6 +4151,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
3467
4151
  const vippsStatus = await Vio.checkout.getVippsStatus(checkoutId, sponsorId || void 0);
3468
4152
  const state6 = String(vippsStatus?.state ?? "").toUpperCase();
3469
4153
  sawStatus = true;
4154
+ if (vippsStatus?.duplicate_of) return { outcome: "duplicate", sawStatus };
3470
4155
  if (state6 === "AUTHORIZED") return { outcome: "paid", sawStatus };
3471
4156
  if (state6 === "CREATED") continue;
3472
4157
  return { outcome: "failed", sawStatus };
@@ -3486,12 +4171,12 @@ var _VioCheckout = class _VioCheckout extends LitElement {
3486
4171
  this.confirmedOrder = {
3487
4172
  items: [],
3488
4173
  currency: getGlobalCurrency(),
3489
- total: 0,
3490
- orderId: checkoutId || void 0
4174
+ total: 0
3491
4175
  };
3492
4176
  this.confirmedMethod = vioMethod;
3493
4177
  this.orderConfirmed = true;
3494
4178
  this.open = true;
4179
+ void this.fillOrderNumber(checkoutId, sponsorId);
3495
4180
  this.dispatchEvent(
3496
4181
  new CustomEvent("vio:payment-success", {
3497
4182
  bubbles: true,
@@ -3499,6 +4184,9 @@ var _VioCheckout = class _VioCheckout extends LitElement {
3499
4184
  detail: { method: vioMethod, sponsorId, orderId: checkoutId }
3500
4185
  })
3501
4186
  );
4187
+ } else if (outcome === "duplicate") {
4188
+ this.paymentError = CART_PAID_MESSAGE;
4189
+ this.open = true;
3502
4190
  } else if (outcome === "failed") {
3503
4191
  this.paymentError = "Betalingen ble avbrutt eller feilet. Vennligst pr\xF8v igjen.";
3504
4192
  this.open = true;
@@ -3506,7 +4194,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
3506
4194
  this.paymentNotice = "Betalingen behandles fortsatt. Du f\xE5r ordrebekreftelse p\xE5 e-post n\xE5r den er gjennomf\xF8rt. Handlekurven er uendret.";
3507
4195
  this.open = true;
3508
4196
  } else {
3509
- this.paymentError = "Vi kunne ikke bekrefte betalingen. Handlekurven er uendret \u2014 pr\xF8v igjen, eller kontakt support hvis du ble belastet.";
4197
+ this.paymentError = _VioCheckout.RETURN_UNVERIFIED_MESSAGE;
3510
4198
  this.open = true;
3511
4199
  }
3512
4200
  }
@@ -3518,9 +4206,33 @@ var _VioCheckout = class _VioCheckout extends LitElement {
3518
4206
  const vioMethod = urlParams.get("vio_method") || "stripe";
3519
4207
  const sponsorId = Number(urlParams.get("vio_sponsor") || 0);
3520
4208
  const checkoutId = urlParams.get("checkout_id") || "";
3521
- if (!vioPayment && !checkoutId) return;
3522
- const kustomOrderId = urlParams.get("order_id") || "";
3523
- if (urlParams.get("payment_processor") === "KUSTOM" && kustomOrderId) {
4209
+ const nexiPaymentId = nexiReturnPaymentId();
4210
+ const nexiPending = readNexiPending();
4211
+ if (nexiPaymentId && nexiPending && nexiPending.paymentId === nexiPaymentId) {
4212
+ this.nexiResumePaymentId = nexiPaymentId;
4213
+ this.cleanReturnQueryParams(["paymentId"]);
4214
+ if (!this.checkoutState && nexiPending.sponsorId) {
4215
+ try {
4216
+ Vio.checkout.open(nexiPending.sponsorId);
4217
+ } catch {
4218
+ }
4219
+ }
4220
+ try {
4221
+ Vio.checkout.selectPaymentMethod("nexi");
4222
+ } catch {
4223
+ }
4224
+ this.open = true;
4225
+ return;
4226
+ }
4227
+ const kustomReturn = kustomReturnOutcome(urlParams);
4228
+ if (kustomReturn?.kind === "rejected") {
4229
+ this.cleanReturnQueryParams([...KUSTOM_RETURN_QUERY_KEYS]);
4230
+ this.kustomNotice = "Bestillingen ble endret. Betalingen ble ikke gjennomf\xF8rt \u2014 pr\xF8v igjen.";
4231
+ return;
4232
+ }
4233
+ if (kustomReturn?.kind === "confirmation") {
4234
+ const kustomOrderId = kustomReturn.orderId;
4235
+ this.returningFrom = "kustom";
3524
4236
  if (!this.checkoutState) {
3525
4237
  const spIdToUse = sponsorId || [...Vio.cart.getAllCarts().keys()][0] || 0;
3526
4238
  if (spIdToUse) {
@@ -3535,49 +4247,35 @@ var _VioCheckout = class _VioCheckout extends LitElement {
3535
4247
  } catch {
3536
4248
  }
3537
4249
  this.open = true;
3538
- this.paymentNotice = "Bekrefter betalingen\u2026";
4250
+ this.verifyingReturn = "Bekrefter betalingen\u2026";
4251
+ this.cleanReturnQueryParams([...KUSTOM_RETURN_QUERY_KEYS]);
3539
4252
  try {
3540
4253
  const order = await Vio.checkout.getKustomOrder(
3541
4254
  kustomOrderId,
3542
4255
  this.checkoutState?.sponsorId
3543
4256
  );
3544
- this.paymentNotice = null;
3545
- if (order?.html_snippet) {
3546
- this.kustomMountedOrderId = order.order_id;
4257
+ this.verifyingReturn = null;
4258
+ if (order?.status === "checkout_complete" && order.html_snippet) {
4259
+ const spId = this.checkoutState?.sponsorId ?? 0;
4260
+ this.showProviderReceipt("kustom", spId, order, kustomReturn.checkoutId);
3547
4261
  await this.updateComplete;
3548
- const container = this.renderRoot?.querySelector(
3549
- "#vio-kustom-checkout-container"
3550
- );
3551
- if (container) {
3552
- Vio.checkout.renderKustomSnippet(container, order.html_snippet);
3553
- }
3554
- if (order.status === "checkout_complete") {
3555
- this.dispatchEvent(
3556
- new CustomEvent("vio:payment-success", {
3557
- detail: { method: "kustom", orderId: order.order_id },
3558
- bubbles: true,
3559
- composed: true
3560
- })
3561
- );
3562
- const spId = this.checkoutState?.sponsorId;
3563
- if (spId) {
3564
- try {
3565
- Vio.cart.clearSponsorCart(spId);
3566
- } catch {
3567
- }
3568
- }
3569
- }
4262
+ const container = this.lightContainer("vio-kustom-checkout-container", "vio-kustom");
4263
+ Vio.checkout.renderKustomSnippet(container, order.html_snippet);
4264
+ } else {
4265
+ this.paymentError = _VioCheckout.RETURN_UNVERIFIED_MESSAGE;
3570
4266
  }
3571
4267
  } catch (err) {
3572
- this.paymentNotice = null;
4268
+ this.verifyingReturn = null;
4269
+ this.paymentError = _VioCheckout.RETURN_UNVERIFIED_MESSAGE;
3573
4270
  if (typeof console !== "undefined") {
3574
4271
  console.warn("[VioCheckout] Kustom confirmation read failed:", err);
3575
4272
  }
3576
4273
  }
3577
- this.cleanReturnQueryParams(["order_id", "payment_processor"]);
3578
4274
  return;
3579
4275
  }
4276
+ if (!vioPayment && !checkoutId) return;
3580
4277
  if (urlParams.get("payment_processor") === "QLIRO" && checkoutId) {
4278
+ this.returningFrom = "qliro";
3581
4279
  if (!this.checkoutState) {
3582
4280
  const spIdToUse = sponsorId || [...Vio.cart.getAllCarts().keys()][0] || 0;
3583
4281
  if (spIdToUse) {
@@ -3591,40 +4289,38 @@ var _VioCheckout = class _VioCheckout extends LitElement {
3591
4289
  Vio.checkout.selectPaymentMethod("qliro");
3592
4290
  } catch {
3593
4291
  }
4292
+ this.autoSelectAttempted = true;
3594
4293
  this.open = true;
3595
- this.paymentNotice = "Bekrefter betalingen\u2026";
4294
+ this.verifyingReturn = "Bekrefter betalingen\u2026";
3596
4295
  try {
3597
4296
  const order = await Vio.checkout.getQliroOrder(
3598
4297
  checkoutId,
3599
4298
  this.checkoutState?.sponsorId
3600
4299
  );
3601
- this.paymentNotice = null;
3602
- if (order?.html_snippet) {
4300
+ this.verifyingReturn = null;
4301
+ if (order?.status === "Completed") {
4302
+ const spId = this.checkoutState?.sponsorId ?? sponsorId ?? 0;
4303
+ const result = { order: { orderId: order.order_id }, chargedTotal: order.total_price };
4304
+ this.qliroMountedOrderId = order.order_id;
4305
+ if (order.html_snippet) {
4306
+ this.showProviderReceipt("qliro", spId, result, checkoutId);
4307
+ await this.updateComplete;
4308
+ const container = this.lightContainer("vio-qliro-checkout-container", "vio-qliro");
4309
+ Vio.checkout.renderKustomSnippet(container, order.html_snippet);
4310
+ } else {
4311
+ this.confirmOrder("qliro", spId, result, checkoutId);
4312
+ }
4313
+ } else if (order?.html_snippet) {
3603
4314
  this.qliroMountedOrderId = order.order_id;
3604
4315
  await this.updateComplete;
3605
4316
  const container = this.lightContainer("vio-qliro-checkout-container", "vio-qliro");
3606
4317
  if (container) {
3607
4318
  Vio.checkout.renderKustomSnippet(container, order.html_snippet);
3608
4319
  }
3609
- if (order.status === "Completed") {
3610
- this.dispatchEvent(
3611
- new CustomEvent("vio:payment-success", {
3612
- detail: { method: "qliro", orderId: order.order_id },
3613
- bubbles: true,
3614
- composed: true
3615
- })
3616
- );
3617
- const spId = this.checkoutState?.sponsorId;
3618
- if (spId) {
3619
- try {
3620
- Vio.cart.clearSponsorCart(spId);
3621
- } catch {
3622
- }
3623
- }
3624
- }
3625
4320
  }
3626
4321
  } catch (err) {
3627
- this.paymentNotice = null;
4322
+ this.verifyingReturn = null;
4323
+ this.paymentError = _VioCheckout.RETURN_UNVERIFIED_MESSAGE;
3628
4324
  if (typeof console !== "undefined") {
3629
4325
  console.warn("[VioCheckout] Qliro confirmation read failed:", err);
3630
4326
  }
@@ -3633,6 +4329,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
3633
4329
  return;
3634
4330
  }
3635
4331
  if (urlParams.get("payment_processor") === "WALLEY" && checkoutId) {
4332
+ this.returningFrom = "walley";
3636
4333
  if (!this.checkoutState) {
3637
4334
  const spIdToUse = sponsorId || [...Vio.cart.getAllCarts().keys()][0] || 0;
3638
4335
  if (spIdToUse) {
@@ -3647,13 +4344,13 @@ var _VioCheckout = class _VioCheckout extends LitElement {
3647
4344
  } catch {
3648
4345
  }
3649
4346
  this.open = true;
3650
- this.paymentNotice = "Bekrefter betalingen\u2026";
4347
+ this.verifyingReturn = "Bekrefter betalingen\u2026";
3651
4348
  try {
3652
4349
  const order = await Vio.checkout.getWalleyOrder(
3653
4350
  checkoutId,
3654
4351
  this.checkoutState?.sponsorId
3655
4352
  );
3656
- this.paymentNotice = null;
4353
+ this.verifyingReturn = null;
3657
4354
  if (order?.html_snippet) {
3658
4355
  this.walleyMountedOrderId = order.order_id;
3659
4356
  await this.updateComplete;
@@ -3662,24 +4359,17 @@ var _VioCheckout = class _VioCheckout extends LitElement {
3662
4359
  Vio.checkout.renderKustomSnippet(container, order.html_snippet);
3663
4360
  }
3664
4361
  if (order.status === "PurchaseCompleted") {
3665
- this.dispatchEvent(
3666
- new CustomEvent("vio:payment-success", {
3667
- detail: { method: "walley", orderId: order.order_id },
3668
- bubbles: true,
3669
- composed: true
3670
- })
4362
+ this.showProviderReceipt(
4363
+ "walley",
4364
+ this.checkoutState?.sponsorId ?? 0,
4365
+ { orderId: order.order_id },
4366
+ checkoutId
3671
4367
  );
3672
- const spId = this.checkoutState?.sponsorId;
3673
- if (spId) {
3674
- try {
3675
- Vio.cart.clearSponsorCart(spId);
3676
- } catch {
3677
- }
3678
- }
3679
4368
  }
3680
4369
  }
3681
4370
  } catch (err) {
3682
- this.paymentNotice = null;
4371
+ this.verifyingReturn = null;
4372
+ this.paymentError = _VioCheckout.RETURN_UNVERIFIED_MESSAGE;
3683
4373
  if (typeof console !== "undefined") {
3684
4374
  console.warn("[VioCheckout] Walley confirmation read failed:", err);
3685
4375
  }
@@ -3691,6 +4381,56 @@ var _VioCheckout = class _VioCheckout extends LitElement {
3691
4381
  this.cleanReturnQueryParams(["vio_payment", "vio_method", "vio_sponsor", "checkout_id"]);
3692
4382
  return;
3693
4383
  }
4384
+ if (vioMethod === ADYEN_RETURN_METHOD && checkoutId) {
4385
+ const adyenReturn = adyenReturnParams(window.location.search);
4386
+ this.cleanReturnQueryParams(ADYEN_RETURN_QUERY_KEYS);
4387
+ if (!adyenReturn) return;
4388
+ this.returningFrom = "adyen";
4389
+ this.adyenSettled = true;
4390
+ this.autoSelectAttempted = true;
4391
+ if (!this.checkoutState) {
4392
+ const spIdToUse = sponsorId || [...Vio.cart.getAllCarts().keys()][0] || 0;
4393
+ if (spIdToUse) {
4394
+ try {
4395
+ Vio.checkout.open(spIdToUse);
4396
+ Vio.checkout.selectPaymentMethod("adyen");
4397
+ } catch {
4398
+ }
4399
+ }
4400
+ }
4401
+ this.open = true;
4402
+ this.verifyingReturn = "Bekrefter betalingen\u2026";
4403
+ let payment = null;
4404
+ let sawStatus = false;
4405
+ try {
4406
+ payment = await Vio.checkout.getAdyenPayment(
4407
+ checkoutId,
4408
+ { redirectResult: adyenReturn.redirectResult },
4409
+ sponsorId || void 0
4410
+ );
4411
+ sawStatus = !!payment;
4412
+ } catch (err) {
4413
+ if (typeof console !== "undefined") {
4414
+ console.warn("[VioCheckout] Adyen return could not be verified:", err);
4415
+ }
4416
+ }
4417
+ this.verifyingReturn = null;
4418
+ const outcome = sawStatus ? adyenReturnOutcome(payment) : "pending";
4419
+ this.applyReturnOutcome(outcome, sawStatus, "adyen", sponsorId, checkoutId);
4420
+ if (outcome === "paid" && this.confirmedOrder) {
4421
+ this.confirmedOrder = {
4422
+ ...this.confirmedOrder,
4423
+ currency: payment?.purchase_currency ?? this.confirmedOrder.currency,
4424
+ total: payment?.total_price ?? this.confirmedOrder.total
4425
+ };
4426
+ this.addAdyenPaymentDetails(payment);
4427
+ }
4428
+ if (outcome === "failed") {
4429
+ this.returningFrom = null;
4430
+ this.adyenSettled = false;
4431
+ }
4432
+ return;
4433
+ }
3694
4434
  if (vioMethod === "vipps" && checkoutId) {
3695
4435
  if (!this.checkoutState) {
3696
4436
  const spIdToUse = sponsorId || [...Vio.cart.getAllCarts().keys()][0] || 0;
@@ -3703,9 +4443,9 @@ var _VioCheckout = class _VioCheckout extends LitElement {
3703
4443
  }
3704
4444
  }
3705
4445
  this.open = true;
3706
- this.paymentNotice = "Bekrefter betalingen med Vipps\u2026";
4446
+ this.verifyingReturn = "Bekrefter betalingen med Vipps\u2026";
3707
4447
  const { outcome, sawStatus } = await this.pollVippsStatus(checkoutId, sponsorId);
3708
- this.paymentNotice = null;
4448
+ this.verifyingReturn = null;
3709
4449
  this.applyReturnOutcome(outcome, sawStatus, vioMethod, sponsorId, checkoutId);
3710
4450
  this.cleanReturnQueryParams(["vio_payment", "vio_method", "vio_sponsor", "checkout_id"]);
3711
4451
  } else if (vioPayment === "success") {
@@ -3713,18 +4453,26 @@ var _VioCheckout = class _VioCheckout extends LitElement {
3713
4453
  let sawStatus = false;
3714
4454
  if (checkoutId) {
3715
4455
  this.open = true;
3716
- this.paymentNotice = "Bekrefter betalingen\u2026";
4456
+ this.verifyingReturn = "Bekrefter betalingen\u2026";
3717
4457
  for (const delayMs of _VioCheckout.RETURN_VERIFY_DELAYS_MS) {
3718
4458
  if (delayMs > 0) await new Promise((r) => setTimeout(r, delayMs));
3719
4459
  try {
3720
4460
  const checkout = await Vio.checkout.getCheckout(checkoutId, sponsorId || void 0);
3721
4461
  const status = String(checkout?.status ?? "").toLowerCase();
3722
4462
  sawStatus = true;
4463
+ if (checkout?.duplicate_of) {
4464
+ outcome = "duplicate";
4465
+ break;
4466
+ }
3723
4467
  if (_VioCheckout.FAILED_STATUSES.has(status)) {
3724
4468
  outcome = "failed";
3725
4469
  break;
3726
4470
  }
3727
- if (_VioCheckout.PAID_STATUSES.has(status) || Boolean(checkout?.origin_payment_id)) {
4471
+ if (_VioCheckout.PAID_STATUSES.has(status) || // A Stripe checkout carries `origin_payment_id` from the
4472
+ // moment the link or the intent is CREATED (shopcart), so for
4473
+ // Stripe that pointer says nothing about who paid. Only the
4474
+ // status does, and it is the webhook that sets it.
4475
+ vioMethod !== "stripe" && Boolean(checkout?.origin_payment_id)) {
3728
4476
  outcome = "paid";
3729
4477
  break;
3730
4478
  }
@@ -3734,7 +4482,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
3734
4482
  }
3735
4483
  }
3736
4484
  }
3737
- this.paymentNotice = null;
4485
+ this.verifyingReturn = null;
3738
4486
  }
3739
4487
  this.applyReturnOutcome(outcome, sawStatus, vioMethod, sponsorId, checkoutId);
3740
4488
  this.cleanReturnQueryParams(["vio_payment", "vio_method", "vio_sponsor", "checkout_id"]);
@@ -3773,17 +4521,45 @@ var _VioCheckout = class _VioCheckout extends LitElement {
3773
4521
  Vio.checkout.removeEventListener("change", this.boundOnCheckoutChange);
3774
4522
  Vio.checkout.removeEventListener("payment-complete", this.boundOnPaymentComplete);
3775
4523
  Vio.checkout.removeEventListener("payment-error", this.boundOnPaymentError);
4524
+ Vio.checkout.removeEventListener("qliro-event", this.boundOnQliroEvent);
3776
4525
  this.unmountKlarna();
4526
+ this.unmountNexi();
4527
+ this.unmountAdyen();
3777
4528
  super.disconnectedCallback();
3778
4529
  }
3779
4530
  updated(changed) {
4531
+ if (changed.has("open")) this.dialogFocus.update(this.open);
3780
4532
  if (changed.has("open") && this.open && this.items.length > 0 && this.availableShippingsList.length === 0) {
3781
4533
  void this.loadAvailableShippings();
3782
4534
  }
4535
+ if (changed.has("open") && this.open) {
4536
+ this.autoSelectSoleMethod();
4537
+ }
4538
+ if ((changed.has("klarnaAvailable") && this.klarnaAvailable || changed.has("applePayAvailable") && this.applePayAvailable) && this.paymentMethodsResolved && !this.checkoutState?.paymentMethod) {
4539
+ this.autoSelectAttempted = false;
4540
+ this.autoSelectSoleMethod();
4541
+ }
4542
+ if (changed.has("shippingBlocked") && this.shippingBlocked) {
4543
+ this.unmountKlarna();
4544
+ this.unmountKustom();
4545
+ this.unmountQliro();
4546
+ this.unmountWalley();
4547
+ this.unmountNexi();
4548
+ this.unmountAdyen();
4549
+ this.unmountStripe();
4550
+ this.paymentError = null;
4551
+ }
4552
+ if (!this.isConnected) return;
3783
4553
  void this.mountKlarnaIfNeeded();
3784
4554
  void this.mountKustomIfNeeded();
4555
+ this.syncKustomIfStale();
3785
4556
  void this.mountQliroIfNeeded();
3786
4557
  void this.mountWalleyIfNeeded();
4558
+ void this.mountNexiIfNeeded();
4559
+ this.dropStaleAdyen();
4560
+ void this.mountAdyenIfNeeded();
4561
+ this.dropStaleStripe();
4562
+ void this.mountStripeIfNeeded();
3787
4563
  }
3788
4564
  close() {
3789
4565
  this.open = false;
@@ -3796,17 +4572,30 @@ var _VioCheckout = class _VioCheckout extends LitElement {
3796
4572
  void this.loadAvailableShippings();
3797
4573
  }
3798
4574
  }
3799
- /** Fetch the real per-supplier shippings and preselect + persist the first. */
4575
+ /**
4576
+ * Fetch the real per-supplier shippings and preselect + persist the first.
4577
+ *
4578
+ * Also decides whether the cart can ship at all (`shippingBlocked`), and
4579
+ * records which opening the answer belongs to (`shippingsReadyFor`).
4580
+ */
3800
4581
  async loadAvailableShippings() {
3801
4582
  const spId = this.checkoutState?.sponsorId;
3802
4583
  if (!spId) return;
3803
4584
  if (this.items.length === 0) return;
3804
4585
  if (this.loadingShippings) return;
4586
+ const session = this.embedSession();
3805
4587
  this.loadingShippings = true;
3806
4588
  this.shippingsAttempted = true;
3807
4589
  try {
3808
4590
  const shippings = await Vio.checkout.fetchAvailableShippings(spId);
3809
- if (Array.isArray(shippings) && shippings.length > 0) {
4591
+ if (session !== this.embedSession()) return;
4592
+ if (!Array.isArray(shippings) || shippings.length === 0) {
4593
+ if (Vio.checkout.lastShippingsResolved) {
4594
+ this.availableShippingsList = [];
4595
+ this.shippingBlocked = this.someLineShipsOnItsOwn();
4596
+ }
4597
+ } else {
4598
+ this.shippingBlocked = false;
3810
4599
  this.availableShippingsList = shippings;
3811
4600
  if (!this.selectedShipping || !shippings.some((s) => s.id === this.selectedShipping)) {
3812
4601
  const firstOpt = shippings[0];
@@ -3825,8 +4614,24 @@ var _VioCheckout = class _VioCheckout extends LitElement {
3825
4614
  }
3826
4615
  } finally {
3827
4616
  this.loadingShippings = false;
4617
+ if (session === this.embedSession()) {
4618
+ this.shippingsReadyFor = session;
4619
+ } else if (this.checkoutState && this.items.length > 0) {
4620
+ void this.loadAvailableShippings();
4621
+ }
3828
4622
  }
3829
4623
  }
4624
+ /**
4625
+ * Whether some product in the cart has shipping rates of its own. With no
4626
+ * rate shared by all of them, that means they cannot ship TOGETHER. A cart
4627
+ * where no line has rates is left alone: digital products have none, and
4628
+ * shopcart refuses the physical case on its side.
4629
+ */
4630
+ someLineShipsOnItsOwn() {
4631
+ return this.items.some(
4632
+ (i) => Array.isArray(i.availableShippings) && i.availableShippings.length > 0
4633
+ );
4634
+ }
3830
4635
  /** Whether all mandatory Leveringsadresse fields are filled. */
3831
4636
  get isAddressValid() {
3832
4637
  const f = this.form;
@@ -3848,11 +4653,13 @@ var _VioCheckout = class _VioCheckout extends LitElement {
3848
4653
  if (this.loadingPaymentMethods) return;
3849
4654
  this.loadingPaymentMethods = true;
3850
4655
  try {
4656
+ const spId = this.checkoutState.sponsorId;
3851
4657
  const methods = await Vio.checkout.getAvailablePaymentMethods(
3852
- this.checkoutState.sponsorId
4658
+ spId
3853
4659
  );
3854
4660
  if (Array.isArray(methods)) {
3855
4661
  this.availableMethods = methods.map((m) => m.name);
4662
+ this.stripeMode = await Vio.checkout.getStripeMode(spId);
3856
4663
  } else {
3857
4664
  this.availableMethods = null;
3858
4665
  }
@@ -3863,13 +4670,70 @@ var _VioCheckout = class _VioCheckout extends LitElement {
3863
4670
  this.availableMethods = null;
3864
4671
  } finally {
3865
4672
  this.loadingPaymentMethods = false;
4673
+ this.paymentMethodsResolved = true;
4674
+ this.autoSelectSoleMethod();
3866
4675
  }
3867
4676
  }
4677
+ /**
4678
+ * With exactly one payment method there is no choice to make, so making the
4679
+ * shopper click it is a step that can only have one outcome. Select it —
4680
+ * and since the checkout asks for the method first (2026-09-22), this is
4681
+ * what skips that step: the form, or the provider's widget, comes straight
4682
+ * up.
4683
+ *
4684
+ * Deliberately narrow:
4685
+ * - never overrides a method the shopper already picked;
4686
+ * - only for methods usable RIGHT NOW — Apple Pay and Klarna depend on the
4687
+ * browser and on their script loading, and auto-selecting an unusable one
4688
+ * would strand the shopper on a panel that cannot pay (see `updated`: a
4689
+ * late "available" gets one more try);
4690
+ * - selecting never pays: Stripe and Vipps still wait for their own button.
4691
+ */
4692
+ autoSelectSoleMethod() {
4693
+ if (!this.open) return;
4694
+ if (!this.paymentMethodsResolved) return;
4695
+ if (this.autoSelectAttempted) return;
4696
+ this.autoSelectAttempted = true;
4697
+ if (this.checkoutState?.paymentMethod) return;
4698
+ const methods = this.offeredMethods();
4699
+ if (!Array.isArray(methods) || methods.length !== 1) return;
4700
+ const id = String(methods[0] ?? "").toLowerCase().replace(/[^a-z]/g, "");
4701
+ const usable = {
4702
+ kustom: "kustom",
4703
+ qliro: "qliro",
4704
+ walley: "walley",
4705
+ nexi: "nexi",
4706
+ // Selected, not mounted: Adyen waits for the form (see adyenRequested).
4707
+ adyen: "adyen",
4708
+ vipps: "vipps",
4709
+ stripe: "stripe",
4710
+ klarna: this.klarnaAvailable ? "klarna" : void 0,
4711
+ applepay: this.applePayAvailable ? "apple-pay" : void 0
4712
+ };
4713
+ const only = usable[id];
4714
+ if (!only) return;
4715
+ Vio.checkout.selectPaymentMethod(only);
4716
+ }
3868
4717
  /** Backend names arrive as e.g. "Apple Pay" — compare letters only. */
3869
- methodEnabled(name) {
3870
- if (this.availableMethods === null) return true;
3871
- const norm = (s) => s.toLowerCase().replace(/[^a-z]/g, "");
3872
- return this.availableMethods.some((m) => norm(m) === norm(name));
4718
+ methodEnabled(...names) {
4719
+ return isMethodEnabled(this.availableMethods, ...names);
4720
+ }
4721
+ /**
4722
+ * Stripe's Payment Element shows the wallets of the account that charges —
4723
+ * Apple Pay and Google Pay among them. Offering our own Apple Pay button
4724
+ * next to it is the same wallet twice, on the same screen, paying through
4725
+ * the same account: when Stripe pays on our page, Stripe owns the wallets
4726
+ * (Angelo, 2026-09-24). The cart's express button is another surface and
4727
+ * keeps its own Apple Pay — the Element does not exist there.
4728
+ */
4729
+ stripeOwnsWallets() {
4730
+ return this.stripeMode === "native" && this.methodEnabled("stripe");
4731
+ }
4732
+ /** The methods the shopper actually sees (see stripeOwnsWallets). */
4733
+ offeredMethods() {
4734
+ const methods = this.availableMethods;
4735
+ if (!Array.isArray(methods) || !this.stripeOwnsWallets()) return methods;
4736
+ return methods.filter((m) => !isMethodEnabled([m], "apple-pay"));
3873
4737
  }
3874
4738
  async refreshApplePay() {
3875
4739
  Vio.checkout.clearApplePayCache();
@@ -3886,10 +4750,12 @@ var _VioCheckout = class _VioCheckout extends LitElement {
3886
4750
  async mountKlarnaIfNeeded() {
3887
4751
  if (!this.klarnaAvailable || !this.open || !this.checkoutState) return;
3888
4752
  if (this.checkoutState.paymentMethod !== "klarna" && !this.express) return;
4753
+ if (!this.express && (!this.isAddressValid || !this.hasSelectedShipping)) return;
3889
4754
  if (this.klarnaMounting) return;
3890
4755
  if (this.items.length > 0 && this.availableShippingsList.length === 0 && !this.shippingsAttempted) {
3891
4756
  await this.loadAvailableShippings();
3892
4757
  }
4758
+ if (this.shippingBlocked) return;
3893
4759
  const amount = this.checkoutState.subtotal;
3894
4760
  if (this.klarnaHandle && this.klarnaMountedAmount === amount && this.klarnaMountedShipping === this.selectedShipping) {
3895
4761
  return;
@@ -3921,7 +4787,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
3921
4787
  if (typeof console !== "undefined") {
3922
4788
  console.warn("[VioCheckout] Klarna Payments mount failed:", err);
3923
4789
  }
3924
- this.paymentError = `Kunne ikke laste Klarna: ${err instanceof Error ? err.message : String(err)}`;
4790
+ this.paymentError = friendlyPaymentError(errorText(err));
3925
4791
  } finally {
3926
4792
  this.klarnaMounting = false;
3927
4793
  }
@@ -3933,7 +4799,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
3933
4799
  }
3934
4800
  this.klarnaMountedAmount = null;
3935
4801
  this.klarnaMountedShipping = null;
3936
- this.klarnaCategories = [];
4802
+ if (this.klarnaCategories.length > 0) this.klarnaCategories = [];
3937
4803
  this.klarnaSelectedCat = "";
3938
4804
  this.klarnaAuthorizing = false;
3939
4805
  }
@@ -3982,7 +4848,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
3982
4848
  }
3983
4849
  /** Authorize the selected Klarna method → backend creates the order. */
3984
4850
  async onKlarnaAuthorize() {
3985
- if (!this.klarnaHandle || this.klarnaAuthorizing) return;
4851
+ if (!this.klarnaHandle || this.klarnaAuthorizing || this.shippingBlocked) return;
3986
4852
  this.paymentError = null;
3987
4853
  this.paymentNotice = null;
3988
4854
  this.klarnaAuthorizing = true;
@@ -4000,6 +4866,33 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4000
4866
  this.form = { ...this.form, [field]: value };
4001
4867
  Vio.checkout.setAddress(this.form);
4002
4868
  }
4869
+ /** After choosing a method that needs our form, bring the form into view. */
4870
+ revealAddressForm() {
4871
+ void this.updateComplete.then(() => {
4872
+ const form = this.renderRoot?.querySelector(".address-section");
4873
+ if (!form) return;
4874
+ const bar = this.renderRoot?.querySelector(".topbar");
4875
+ form.style.scrollMarginTop = `${(bar?.offsetHeight ?? 0) + 8}px`;
4876
+ form.scrollIntoView?.({ behavior: "smooth", block: "start" });
4877
+ });
4878
+ }
4879
+ /**
4880
+ * Vipps from the checkout: Express when the channel allows it, so the
4881
+ * shopper picks the delivery inside the app — our rate on the cart is the
4882
+ * one preselected there — and never silently keeps the default our form
4883
+ * chose for them. When the cart cannot have Express (several suppliers)
4884
+ * the backend says so without creating a payment, and the plain payment
4885
+ * follows: profile shared, the rate already on the cart.
4886
+ */
4887
+ async startVippsFromCheckout() {
4888
+ const sponsorId = this.checkoutState?.sponsorId;
4889
+ const express = await Vio.checkout.getVippsExpressEnabled(sponsorId);
4890
+ if (express) {
4891
+ const res = await Vio.checkout.startVippsPayment(sponsorId, this.form, { express: true });
4892
+ if (res?.payment_url) return;
4893
+ }
4894
+ await Vio.checkout.startVippsPayment(sponsorId, this.form);
4895
+ }
4003
4896
  async onApplePay() {
4004
4897
  if (this.applePayInProgress) return;
4005
4898
  this.applePayInProgress = true;
@@ -4014,6 +4907,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4014
4907
  }
4015
4908
  }
4016
4909
  async onPay(method) {
4910
+ if (this.shippingBlocked) return;
4017
4911
  this.paymentError = null;
4018
4912
  this.paymentNotice = null;
4019
4913
  if (method === "apple-pay") {
@@ -4032,24 +4926,35 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4032
4926
  Vio.checkout.selectPaymentMethod("walley");
4033
4927
  return;
4034
4928
  }
4035
- if (method === "vipps") {
4036
- const email = String(this.form.email ?? "").trim();
4037
- if (!email || !email.includes("@")) {
4038
- this.paymentError = "Vennligst fyll inn e-postadressen din.";
4039
- return;
4040
- }
4041
- } else {
4042
- if (!this.isAddressValid) {
4043
- this.paymentError = "Vennligst fyll ut alle feltene i leveringsadresse.";
4044
- return;
4045
- }
4046
- if (!this.hasSelectedShipping) {
4047
- this.paymentError = "Vennligst velg en fraktmetode.";
4929
+ if (method === "nexi") {
4930
+ Vio.checkout.selectPaymentMethod("nexi");
4931
+ return;
4932
+ }
4933
+ if (method !== "vipps") {
4934
+ const missing = !this.isAddressValid ? "Vennligst fyll ut alle feltene i leveringsadresse." : !this.hasSelectedShipping ? "Vennligst velg en fraktmetode." : null;
4935
+ if (missing) {
4936
+ if (this.checkoutState?.paymentMethod !== method) {
4937
+ Vio.checkout.selectPaymentMethod(method);
4938
+ this.revealAddressForm();
4939
+ return;
4940
+ }
4941
+ this.paymentError = missing;
4048
4942
  return;
4049
4943
  }
4050
4944
  }
4051
4945
  Vio.checkout.selectPaymentMethod(method);
4946
+ if (method === "adyen") {
4947
+ this.adyenRequested = true;
4948
+ return;
4949
+ }
4052
4950
  if (method === "stripe") {
4951
+ if (this.stripeMode === null) {
4952
+ this.stripeMode = await Vio.checkout.getStripeMode(this.checkoutState?.sponsorId);
4953
+ }
4954
+ if (this.stripeMode === "native") {
4955
+ this.stripeRequested = true;
4956
+ return;
4957
+ }
4053
4958
  const selectedOpt = this.shippingList.find((s) => s.id === this.selectedShipping);
4054
4959
  if (selectedOpt?.supplierId) {
4055
4960
  try {
@@ -4065,15 +4970,21 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4065
4970
  }
4066
4971
  this.stripeLoading = true;
4067
4972
  void Vio.checkout.startStripePayment(this.checkoutState?.sponsorId, this.form).catch((err) => {
4068
- 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));
4069
4977
  }).finally(() => {
4070
4978
  this.stripeLoading = false;
4071
4979
  });
4072
4980
  }
4073
4981
  if (method === "vipps") {
4074
4982
  this.vippsLoading = true;
4075
- void Vio.checkout.startVippsPayment(this.checkoutState?.sponsorId, this.form).catch((err) => {
4076
- this.paymentError = `Kunne ikke starte Vipps-betaling: ${err instanceof Error ? err.message : String(err)}`;
4983
+ void this.startVippsFromCheckout().catch((err) => {
4984
+ if (typeof console !== "undefined") {
4985
+ console.warn("[VioCheckout] Vipps payment could not start:", err);
4986
+ }
4987
+ this.paymentError = friendlyPaymentError(errorText(err));
4077
4988
  }).finally(() => {
4078
4989
  this.vippsLoading = false;
4079
4990
  });
@@ -4087,6 +4998,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4087
4998
  onCompleteOrder() {
4088
4999
  const s = this.checkoutState;
4089
5000
  if (!s || !s.paymentMethod) return;
5001
+ if (this.shippingBlocked) return;
4090
5002
  if (s.paymentMethod !== "vipps" && !this.isAddressValid) {
4091
5003
  this.paymentError = "Vennligst fyll ut alle feltene i leveringsadresse.";
4092
5004
  return;
@@ -4107,13 +5019,19 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4107
5019
  * the confirmation screen, emits `vio:payment-success`, and clears the
4108
5020
  * paid sponsor's cart so the badge + drawer reflect the purchase.
4109
5021
  */
4110
- confirmOrder(method, sponsorId, result) {
5022
+ confirmOrder(method, sponsorId, result, returnCheckoutId) {
4111
5023
  const r = result ?? {};
5024
+ const shippingPrice = this.shippingMajor();
5025
+ const email = String(this.form.email ?? "").trim();
5026
+ const checkoutId = returnCheckoutId || this.checkoutState?.checkoutId;
5027
+ const providerOrderId = r.order?.orderId != null ? String(r.order.orderId) : "";
4112
5028
  this.confirmedOrder = {
4113
5029
  items: [...this.items],
4114
5030
  currency: this.checkoutState?.currency ?? "",
4115
5031
  total: typeof r.chargedTotal === "number" ? r.chargedTotal : this.checkoutState?.subtotal ?? 0,
4116
- orderId: r.order?.orderId
5032
+ ...providerOrderId ? { providerRef: { label: `Ordre hos ${this.methodLabel(method)}`, value: providerOrderId } } : {},
5033
+ ...shippingPrice > 0 ? { shipping: { name: this.summaryShippingName() || "Frakt", price: shippingPrice } } : {},
5034
+ ...email ? { email } : {}
4117
5035
  };
4118
5036
  this.confirmedMethod = method;
4119
5037
  this.orderConfirmed = true;
@@ -4122,10 +5040,40 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4122
5040
  new CustomEvent("vio:payment-success", {
4123
5041
  bubbles: true,
4124
5042
  composed: true,
4125
- 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 }
4126
5047
  })
4127
5048
  );
4128
5049
  Vio.cart.clearSponsorCart(sponsorId);
5050
+ void this.fillOrderNumber(checkoutId, sponsorId);
5051
+ }
5052
+ static {
5053
+ /** Waits between asks for the Vio order number: the order is often born a
5054
+ * moment after the payment (Vipps and Stripe through their webhook). */
5055
+ this.ORDER_NUMBER_DELAYS_MS = [0, 1500, 3e3, 5e3, 8e3];
5056
+ }
5057
+ /**
5058
+ * Puts the Vio order number (#4497) on the confirmation once the order
5059
+ * exists. Until then the line is simply not there — never the checkout's
5060
+ * uuid in its place, which is what it used to print. Stops if the shopper
5061
+ * closes the drawer or another confirmation takes its place.
5062
+ */
5063
+ async fillOrderNumber(checkoutId, sponsorId) {
5064
+ this.confirmedCheckoutId = checkoutId || null;
5065
+ if (!checkoutId) return;
5066
+ const stillShowing = () => this.orderConfirmed && this.confirmedCheckoutId === checkoutId;
5067
+ for (const delayMs of _VioCheckout.ORDER_NUMBER_DELAYS_MS) {
5068
+ if (delayMs > 0) await new Promise((r) => setTimeout(r, delayMs));
5069
+ if (!stillShowing()) return;
5070
+ const n = await Vio.checkout.getOrderNumber(checkoutId, sponsorId || void 0);
5071
+ if (!stillShowing()) return;
5072
+ if (n && this.confirmedOrder) {
5073
+ this.confirmedOrder = { ...this.confirmedOrder, orderId: String(n) };
5074
+ return;
5075
+ }
5076
+ }
4129
5077
  }
4130
5078
  methodLabel(method) {
4131
5079
  switch (method) {
@@ -4139,6 +5087,11 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4139
5087
  return "Qliro";
4140
5088
  case "walley":
4141
5089
  return "Walley";
5090
+ case "nexi":
5091
+ return "Nexi";
5092
+ // Adyen is not a name shoppers know; what they pick from is inside it.
5093
+ case "adyen":
5094
+ return "Kort, Vipps og flere";
4142
5095
  case "vipps":
4143
5096
  return "Vipps";
4144
5097
  case "stripe":
@@ -4151,7 +5104,8 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4151
5104
  }
4152
5105
  /** True for methods that complete via a separate sheet/widget, not the CTA. */
4153
5106
  isExpressMethod(method) {
4154
- return method === "apple-pay" || method === "klarna" || method === "kustom" || method === "qliro" || method === "walley";
5107
+ return method === "apple-pay" || method === "klarna" || isEmbeddedMethod(method ?? "") || // Adyen's Drop-in brings its own Pay button.
5108
+ isFormFirstWidgetMethod(method ?? "");
4155
5109
  }
4156
5110
  orderTotal() {
4157
5111
  const s = this.checkoutState;
@@ -4162,11 +5116,33 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4162
5116
  * express always has one; the normal flow has one once the backend
4163
5117
  * shippings loaded (the choice IS persisted on the cart and charged). */
4164
5118
  shippingMajor() {
5119
+ if (this.checkoutState?.paymentMethod === "qliro" && typeof this.qliroShipping?.price === "number") {
5120
+ return this.qliroShipping.price;
5121
+ }
5122
+ if (this.checkoutState?.paymentMethod === "nexi" && typeof this.nexiShipping?.shipping_price === "number") {
5123
+ return this.nexiShipping.shipping_price;
5124
+ }
5125
+ if (this.checkoutState?.paymentMethod === "kustom" && typeof this.kustomShipping?.price === "number") {
5126
+ return this.kustomShipping.price;
5127
+ }
4165
5128
  if (!this.express && this.availableShippingsList.length === 0) return 0;
4166
5129
  const o = this.shippingOption;
4167
5130
  if (!o) return 0;
4168
5131
  return o.priceMajor ?? o.price / 100;
4169
5132
  }
5133
+ /** The rate the summary names — Qliro's own pick while Qliro is the method. */
5134
+ summaryShippingName() {
5135
+ if (this.checkoutState?.paymentMethod === "qliro" && this.qliroShipping) {
5136
+ const ref = this.qliroShipping.ref;
5137
+ const picked = ref ? this.availableShippingsList.find((o2) => o2.id === ref) : void 0;
5138
+ return picked ? picked.method || picked.name || "" : "";
5139
+ }
5140
+ if (this.checkoutState?.paymentMethod === "nexi" && this.nexiShipping) {
5141
+ return this.nexiShipping.shipping_name ?? "";
5142
+ }
5143
+ const o = this.shippingOption;
5144
+ return o ? o.method || o.name || "" : "";
5145
+ }
4170
5146
  /** Total to charge — items + shipping (express). */
4171
5147
  payTotalLabel() {
4172
5148
  const s = this.checkoutState;
@@ -4179,7 +5155,9 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4179
5155
  <div
4180
5156
  class="modal ${this.express ? "as-side" : this.orderConfirmed ? "as-confirm" : ""}"
4181
5157
  role="dialog"
5158
+ aria-modal="true"
4182
5159
  aria-label=${this.heading}
5160
+ tabindex="-1"
4183
5161
  >
4184
5162
  <div class="handle" @click=${this.close} aria-hidden="true"></div>
4185
5163
  <div class="topbar">
@@ -4188,81 +5166,280 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4188
5166
  </div>
4189
5167
 
4190
5168
  <div class="body">
4191
- ${this.orderConfirmed ? this.renderConfirmation() : this.express ? this.renderKlarnaExpress() : this.renderCheckoutBody()}
5169
+ ${this.providerReceipt ? this.renderProviderReceipt() : this.orderConfirmed ? this.renderConfirmation() : this.verifyingReturn ? this.renderVerifying() : this.express ? this.renderKlarnaExpress() : this.renderCheckoutBody()}
4192
5170
  </div>
4193
5171
  </div>
4194
5172
  `;
4195
5173
  }
5174
+ /**
5175
+ * The provider's receipt on its own. It must not depend on the cart: the
5176
+ * payment step is rendered from the cart, and clearing the cart after a
5177
+ * purchase is what took Qliro's receipt away on 2026-09-08 and brought back
5178
+ * "how do you want to pay". The provider's container lives in the light DOM
5179
+ * and is projected here, untouched.
5180
+ */
5181
+ renderProviderReceipt() {
5182
+ const slot = this.providerReceipt === "walley" ? "vio-walley" : this.providerReceipt === "kustom" ? "vio-kustom" : "vio-qliro";
5183
+ return html`
5184
+ <section class="section provider-receipt">
5185
+ <slot name=${slot}></slot>
5186
+ <button class="confirm-close" @click=${this.close}>Lukk</button>
5187
+ </section>
5188
+ `;
5189
+ }
5190
+ /**
5191
+ * A completed purchase whose receipt the PROVIDER shows. Same side effects as
5192
+ * confirmOrder — `vio:payment-success` for the host and analytics, the
5193
+ * sponsor's cart cleared — but the screen is the provider's receipt, not our
5194
+ * "Takk!": two receipts for one purchase is one too many.
5195
+ */
5196
+ showProviderReceipt(method, sponsorId, result, returnCheckoutId) {
5197
+ this.providerReceipt = method;
5198
+ this.confirmedMethod = method;
5199
+ this.open = true;
5200
+ const checkoutId = returnCheckoutId || this.checkoutState?.checkoutId;
5201
+ this.dispatchEvent(
5202
+ new CustomEvent("vio:payment-success", {
5203
+ bubbles: true,
5204
+ composed: true,
5205
+ detail: { method, sponsorId, result, checkoutId }
5206
+ })
5207
+ );
5208
+ if (sponsorId) {
5209
+ try {
5210
+ Vio.cart.clearSponsorCart(sponsorId);
5211
+ } catch {
5212
+ }
5213
+ }
5214
+ }
5215
+ /** Back from the payment page, asking whether it was paid — and nothing
5216
+ * else on screen while we do. */
5217
+ renderVerifying() {
5218
+ return html`
5219
+ <section class="verifying" role="status" aria-live="polite">
5220
+ <span class="verifying-spinner" aria-hidden="true"></span>
5221
+ <p class="verifying-text">${this.verifyingReturn}</p>
5222
+ <p class="verifying-hint">Dette kan ta litt tid. Ikke lukk siden.</p>
5223
+ </section>
5224
+ `;
5225
+ }
4196
5226
  renderConfirmation() {
4197
5227
  const o = this.confirmedOrder;
5228
+ const email = o?.email || String(this.form.email ?? "").trim();
5229
+ const details = [];
5230
+ if (o?.orderId) details.push(["Ordrenummer", `#${o.orderId}`]);
5231
+ details.push(["Betalingsm\xE5te", o?.paidWith || this.methodLabel(this.confirmedMethod)]);
5232
+ if (o?.providerRef) details.push([o.providerRef.label, o.providerRef.value]);
5233
+ if (email) details.push(["Kvittering til", email]);
4198
5234
  return html`
4199
5235
  <section class="confirmation">
4200
- <div class="confirm-check" aria-hidden="true">✓</div>
4201
- <h3 class="confirm-title">Takk for bestillingen!</h3>
4202
- <p class="confirm-text">
4203
- Betaling med ${this.methodLabel(this.confirmedMethod)} er bekreftet.${this.form.email ? html` Kvittering sendes til ${this.form.email}.` : ""}
4204
- </p>
4205
- ${o?.orderId ? html`<div class="confirm-order-id">Ordrenummer: <b>${o.orderId}</b></div>` : ""}
5236
+ <header class="confirm-head">
5237
+ <div class="confirm-check" aria-hidden="true">✓</div>
5238
+ <div>
5239
+ <h3 class="confirm-title">Takk for bestillingen!</h3>
5240
+ <p class="confirm-text">
5241
+ Betalingen er bekreftet.${email ? html` Du får en ordrebekreftelse på e-post.` : ""}
5242
+ </p>
5243
+ </div>
5244
+ </header>
5245
+
5246
+ <dl class="confirm-details">
5247
+ ${details.map(
5248
+ ([k, v]) => html`<div><dt>${k}</dt><dd class=${k === o?.providerRef?.label ? "ref" : ""}>${v}</dd></div>`
5249
+ )}
5250
+ </dl>
5251
+
4206
5252
  ${o && o.items.length > 0 ? html`
4207
5253
  <div class="confirm-summary">
5254
+ <div class="confirm-label">Din bestilling</div>
4208
5255
  ${o.items.map(
4209
5256
  (it) => html`
4210
- <div class="order-row">
4211
- <span>${it.brand ? `${it.brand} ` : ""}${it.name} ×${it.quantity}</span>
4212
- <span>${formatPrice(it.unitPrice * it.quantity, it.currency)}</span>
5257
+ <div class="confirm-line">
5258
+ ${it.imageUrl ? html`<img class="confirm-thumb" src=${it.imageUrl} alt="" loading="lazy" />` : html`<span class="confirm-thumb" aria-hidden="true"></span>`}
5259
+ <span class="confirm-line-name">
5260
+ ${it.brand ? html`<small>${it.brand}</small>` : ""}
5261
+ <span>${it.name}</span>
5262
+ <small>Antall: ${it.quantity}</small>
5263
+ </span>
5264
+ <span class="confirm-line-price">
5265
+ ${formatPrice(it.unitPrice * it.quantity, it.currency)}
5266
+ </span>
4213
5267
  </div>
4214
5268
  `
4215
5269
  )}
5270
+ ${o.shipping ? html`
5271
+ <div class="order-row">
5272
+ <span>Frakt – ${o.shipping.name}</span>
5273
+ <span>${formatPrice(o.shipping.price, o.currency)}</span>
5274
+ </div>
5275
+ ` : ""}
4216
5276
  <div class="order-row total">
4217
5277
  <span>Totalt</span><span>${formatPrice(o.total, o.currency)}</span>
4218
5278
  </div>
4219
5279
  </div>
4220
5280
  ` : ""}
4221
- <button class="confirm-close" @click=${this.close}>Lukk</button>
5281
+ <button class="confirm-close" @click=${this.close}>Fortsett å handle</button>
4222
5282
  </section>
4223
5283
  `;
4224
5284
  }
5285
+ /**
5286
+ * Whether mounting an embedded checkout now would be starting a payment the
5287
+ * shopper asked for. Mounting CREATES an order at the provider (and a backend
5288
+ * checkout if there is none), so it must not happen on a confirmation return,
5289
+ * once this checkout's order is confirmed, or for a cart that cannot ship.
5290
+ */
5291
+ mayStartEmbeddedPayment() {
5292
+ return !this.returningFrom && !this.orderConfirmed && !this.providerReceipt && !this.shippingBlocked && this.shippingsSettled();
5293
+ }
5294
+ /**
5295
+ * Whether we know enough about shipping to create an order. The provider
5296
+ * prices the order when it is created, so a first opening waits for the
5297
+ * shippings to answer: the cart may turn out unable to ship at all. A list
5298
+ * already on screen counts, and a later answer that blocks the cart takes
5299
+ * the widget down again (see `updated`).
5300
+ */
5301
+ shippingsSettled() {
5302
+ return this.items.length === 0 || this.availableShippingsList.length > 0 || this.shippingsReadyFor === this.embedSession();
5303
+ }
5304
+ /** Why nothing can be paid: the cart's products cannot ship together. */
5305
+ renderShippingBlocked() {
5306
+ return html`<div class="payment-error" role="alert">${_VioCheckout.SHIPPING_BLOCKED_MESSAGE}</div>`;
5307
+ }
5308
+ /**
5309
+ * The checkout an embedded widget's order belongs to. Kustom, Qliro and
5310
+ * Walley price the order when it is created, so a widget kept into a NEW
5311
+ * checkout shows — and charges — the cart it was created for.
5312
+ *
5313
+ * Closing drops the widgets, but a checkout can start again without a
5314
+ * close: in Vev the cart view opens on top of the checkout and "Til kassen"
5315
+ * calls `Vio.checkout.open()` again (Angelo, 2026-09-14: quantities raised
5316
+ * in the cart view, Qliro kept the old total). Every `open()` is a new
5317
+ * session, so a widget from an earlier one starts over.
5318
+ *
5319
+ * 0.11.3 compared the cart LINES instead, and that was wrong: within one
5320
+ * session the backend rewrites them (saving the shipping re-reads the cart),
5321
+ * so the first Qliro event after that — the customer picking a shipping —
5322
+ * tore the widget down mid-purchase (Alan, 2026-09-14). The session does
5323
+ * not change under the customer's hands.
5324
+ */
5325
+ embedSession() {
5326
+ const s = this.checkoutState;
5327
+ return `${s?.sponsorId ?? ""}|${s?.session ?? ""}`;
5328
+ }
4225
5329
  /** Klarna Payments widget panel: shipping + category chips + widget + pay button. */
5330
+ /**
5331
+ * The cart a Kustom order describes. When it changes under a mounted
5332
+ * widget, the order is UPDATED in place (Kustom's rule) — see
5333
+ * syncKustomIfStale. The order is a photo of the lines, not of the form:
5334
+ * address and shipping live inside the widget.
5335
+ */
5336
+ kustomCartKey() {
5337
+ return JSON.stringify([
5338
+ this.items.map((i) => [i.productId, i.variantId ?? "", i.quantity]),
5339
+ this.checkoutState?.subtotal ?? 0
5340
+ ]);
5341
+ }
4226
5342
  /** Mount the Kustom embedded checkout once it's the chosen method. */
4227
5343
  async mountKustomIfNeeded() {
4228
5344
  if (!this.open || !this.checkoutState) return;
4229
5345
  if (this.checkoutState.paymentMethod !== "kustom") return;
4230
- if (this.kustomMounting) return;
4231
- const container = this.renderRoot?.querySelector(
4232
- "#vio-kustom-checkout-container"
4233
- );
4234
- if (!container) return;
4235
- if (this.kustomMountedOrderId && container.childElementCount > 0) return;
5346
+ if (!this.mayStartEmbeddedPayment()) return;
5347
+ if (this.kustomMounting || this.kustomSyncing) return;
5348
+ if (this.kustomMountedOrderId) {
5349
+ if (this.kustomMountedFor === this.embedSession()) return;
5350
+ this.unmountKustom();
5351
+ }
5352
+ const container = this.lightContainer("vio-kustom-checkout-container", "vio-kustom");
5353
+ const mountedFor = this.embedSession();
5354
+ const mountedKey = this.kustomCartKey();
4236
5355
  this.kustomMounting = true;
5356
+ this.kustomNotice = null;
5357
+ this.paymentError = null;
4237
5358
  container.innerHTML = "";
4238
5359
  try {
4239
5360
  const order = await Vio.checkout.mountKustomCheckout(
4240
5361
  container,
4241
- this.checkoutState.sponsorId
5362
+ this.checkoutState.sponsorId,
5363
+ this.kustomHandlers()
4242
5364
  );
4243
5365
  this.kustomMountedOrderId = order.order_id;
5366
+ this.kustomMountedFor = mountedFor;
5367
+ this.kustomMountedKey = mountedKey;
5368
+ this.kustomShipping = order.shipping_name !== void 0 || order.shipping_price !== void 0 ? { name: order.shipping_name, price: order.shipping_price } : null;
5369
+ this.kustomTotal = typeof order.total_price === "number" ? order.total_price : null;
4244
5370
  } catch (err) {
4245
5371
  if (typeof console !== "undefined") {
4246
5372
  console.warn("[VioCheckout] Kustom mount failed:", err);
4247
5373
  }
4248
- this.paymentError = `Kunne ikke laste Kustom: ${err instanceof Error ? err.message : String(err)}`;
5374
+ this.paymentError = friendlyPaymentError(errorText(err));
4249
5375
  Vio.checkout.selectPaymentMethod("");
4250
5376
  } finally {
4251
5377
  this.kustomMounting = false;
4252
5378
  }
4253
5379
  }
5380
+ /** What the widget tells us, kept only to show — never acted on server-side. */
5381
+ kustomHandlers() {
5382
+ return {
5383
+ onOrderTotalChange: (minor) => {
5384
+ this.kustomTotal = minor / 100;
5385
+ },
5386
+ onShippingOptionChange: (o) => {
5387
+ this.kustomShipping = {
5388
+ name: o?.name,
5389
+ price: typeof o?.price === "number" ? o.price / 100 : void 0
5390
+ };
5391
+ },
5392
+ onCannotComplete: () => {
5393
+ this.kustomNotice = "Betalingen kunne ikke gjennomf\xF8res med valgt metode. Velg en annen betalingsm\xE5te.";
5394
+ },
5395
+ onNetworkError: () => {
5396
+ this.kustomNotice = "Mistet kontakt med betalingsvinduet. Last inn siden p\xE5 nytt.";
5397
+ }
5398
+ };
5399
+ }
5400
+ /**
5401
+ * The cart changed under a mounted Kustom widget: suspend it, push the
5402
+ * cart into the same order, resume it. Never a second order, never a
5403
+ * widget showing a total the backend will not charge (the validation
5404
+ * callback would refuse it). A failed sync takes the widget down.
5405
+ */
5406
+ syncKustomIfStale() {
5407
+ if (!this.kustomMountedOrderId || this.kustomMounting || this.kustomSyncing) return;
5408
+ if (this.providerReceipt || this.returningFrom) return;
5409
+ const key = this.kustomCartKey();
5410
+ if (key === this.kustomMountedKey) return;
5411
+ this.kustomSyncing = true;
5412
+ this.kustomMountedKey = key;
5413
+ void Vio.checkout.syncKustomOrder(this.checkoutState?.sponsorId).then((order) => {
5414
+ if (!order) throw new Error("no order");
5415
+ this.kustomMountedOrderId = order.order_id;
5416
+ this.kustomTotal = typeof order.total_price === "number" ? order.total_price : this.kustomTotal;
5417
+ }).catch((err) => {
5418
+ if (typeof console !== "undefined") {
5419
+ console.warn("[VioCheckout] Kustom sync failed:", err);
5420
+ }
5421
+ this.unmountKustom();
5422
+ this.kustomNotice = "Bestillingen ble endret. Last inn betalingen p\xE5 nytt.";
5423
+ }).finally(() => {
5424
+ this.kustomSyncing = false;
5425
+ });
5426
+ }
4254
5427
  unmountKustom() {
4255
5428
  this.kustomMountedOrderId = null;
4256
- const container = this.renderRoot?.querySelector(
4257
- "#vio-kustom-checkout-container"
4258
- );
4259
- if (container) container.innerHTML = "";
5429
+ this.kustomMountedFor = null;
5430
+ this.kustomMountedKey = null;
5431
+ this.kustomShipping = null;
5432
+ this.kustomTotal = null;
5433
+ Vio.checkout.destroyKustomListeners();
5434
+ this.querySelector("#vio-kustom-checkout-container")?.remove();
4260
5435
  }
4261
5436
  renderKustomPanel() {
4262
5437
  return html`
4263
5438
  <div class="kustom-panel">
4264
5439
  ${this.kustomMounting ? html`<div style="font-size:13px;opacity:0.7;padding:8px 0;">Laster Kustom…</div>` : ""}
4265
- <div id="vio-kustom-checkout-container"></div>
5440
+ ${this.kustomSyncing ? html`<div style="font-size:13px;opacity:0.7;padding:8px 0;">Oppdaterer bestillingen…</div>` : ""}
5441
+ ${this.kustomNotice ? html`<div class="payment-notice">${this.kustomNotice}</div>` : ""}
5442
+ <slot name="vio-kustom"></slot>
4266
5443
  </div>
4267
5444
  `;
4268
5445
  }
@@ -4292,9 +5469,15 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4292
5469
  async mountQliroIfNeeded() {
4293
5470
  if (!this.open || !this.checkoutState) return;
4294
5471
  if (this.checkoutState.paymentMethod !== "qliro") return;
5472
+ if (!this.mayStartEmbeddedPayment()) return;
4295
5473
  if (this.qliroMounting) return;
4296
- const container = this.lightContainer("vio-qliro-checkout-container", "vio-qliro");
4297
- if (this.qliroMountedOrderId && container.childElementCount > 0) return;
5474
+ let container = this.lightContainer("vio-qliro-checkout-container", "vio-qliro");
5475
+ if (this.qliroMountedOrderId && container.childElementCount > 0) {
5476
+ if (this.qliroMountedFor === this.embedSession()) return;
5477
+ this.unmountQliro();
5478
+ container = this.lightContainer("vio-qliro-checkout-container", "vio-qliro");
5479
+ }
5480
+ const mountedFor = this.embedSession();
4298
5481
  this.qliroMounting = true;
4299
5482
  container.innerHTML = "";
4300
5483
  try {
@@ -4303,11 +5486,12 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4303
5486
  this.checkoutState.sponsorId
4304
5487
  );
4305
5488
  this.qliroMountedOrderId = order.order_id;
5489
+ this.qliroMountedFor = mountedFor;
4306
5490
  } catch (err) {
4307
5491
  if (typeof console !== "undefined") {
4308
5492
  console.warn("[VioCheckout] Qliro mount failed:", err);
4309
5493
  }
4310
- this.paymentError = `Kunne ikke laste Qliro: ${err instanceof Error ? err.message : String(err)}`;
5494
+ this.paymentError = friendlyPaymentError(errorText(err));
4311
5495
  Vio.checkout.selectPaymentMethod("");
4312
5496
  } finally {
4313
5497
  this.qliroMounting = false;
@@ -4315,6 +5499,8 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4315
5499
  }
4316
5500
  unmountQliro() {
4317
5501
  this.qliroMountedOrderId = null;
5502
+ this.qliroMountedFor = null;
5503
+ this.qliroShipping = null;
4318
5504
  Vio.checkout.destroyQliroListeners();
4319
5505
  this.querySelector("#vio-qliro-checkout-container")?.remove();
4320
5506
  }
@@ -4335,9 +5521,15 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4335
5521
  async mountWalleyIfNeeded() {
4336
5522
  if (!this.open || !this.checkoutState) return;
4337
5523
  if (this.checkoutState.paymentMethod !== "walley") return;
5524
+ if (!this.mayStartEmbeddedPayment()) return;
4338
5525
  if (this.walleyMounting) return;
4339
- const container = this.lightContainer("vio-walley-checkout-container", "vio-walley");
4340
- if (this.walleyMountedOrderId && container.childElementCount > 0) return;
5526
+ let container = this.lightContainer("vio-walley-checkout-container", "vio-walley");
5527
+ if (this.walleyMountedOrderId && container.childElementCount > 0) {
5528
+ if (this.walleyMountedFor === this.embedSession()) return;
5529
+ this.unmountWalley();
5530
+ container = this.lightContainer("vio-walley-checkout-container", "vio-walley");
5531
+ }
5532
+ const mountedFor = this.embedSession();
4341
5533
  this.walleyMounting = true;
4342
5534
  container.innerHTML = "";
4343
5535
  try {
@@ -4345,28 +5537,18 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4345
5537
  container,
4346
5538
  this.checkoutState.sponsorId,
4347
5539
  () => {
4348
- this.dispatchEvent(
4349
- new CustomEvent("vio:payment-success", {
4350
- detail: { method: "walley", orderId: order.order_id },
4351
- bubbles: true,
4352
- composed: true
4353
- })
4354
- );
4355
- const spId = this.checkoutState?.sponsorId;
4356
- if (spId) {
4357
- try {
4358
- Vio.cart.clearSponsorCart(spId);
4359
- } catch {
4360
- }
4361
- }
5540
+ this.showProviderReceipt("walley", this.checkoutState?.sponsorId ?? 0, {
5541
+ orderId: order.order_id
5542
+ });
4362
5543
  }
4363
5544
  );
4364
5545
  this.walleyMountedOrderId = order.order_id;
5546
+ this.walleyMountedFor = mountedFor;
4365
5547
  } catch (err) {
4366
5548
  if (typeof console !== "undefined") {
4367
5549
  console.warn("[VioCheckout] Walley mount failed:", err);
4368
5550
  }
4369
- this.paymentError = `Kunne ikke laste Walley: ${err instanceof Error ? err.message : String(err)}`;
5551
+ this.paymentError = friendlyPaymentError(errorText(err));
4370
5552
  Vio.checkout.selectPaymentMethod("");
4371
5553
  } finally {
4372
5554
  this.walleyMounting = false;
@@ -4374,6 +5556,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4374
5556
  }
4375
5557
  unmountWalley() {
4376
5558
  this.walleyMountedOrderId = null;
5559
+ this.walleyMountedFor = null;
4377
5560
  this.querySelector("#vio-walley-checkout-container")?.remove();
4378
5561
  }
4379
5562
  renderWalleyPanel() {
@@ -4384,6 +5567,503 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4384
5567
  </div>
4385
5568
  `;
4386
5569
  }
5570
+ /**
5571
+ * Mount the Nexi embedded checkout once it's the chosen method. Nexi's own
5572
+ * JS SDK renders into a LIGHT-DOM container (it refuses a shadow root);
5573
+ * completion is its `payment-completed` event — Nexi neither redirects nor
5574
+ * shows a receipt, so success lands in Vio's confirmation drawer. Shipping
5575
+ * is re-priced on `address-changed` (see the manager).
5576
+ */
5577
+ async mountNexiIfNeeded() {
5578
+ if (!this.open || !this.checkoutState) return;
5579
+ if (this.checkoutState.paymentMethod !== "nexi") return;
5580
+ if (!this.mayStartEmbeddedPayment()) return;
5581
+ if (this.nexiMounting) return;
5582
+ let container = this.lightContainer("vio-nexi-checkout-container", "vio-nexi");
5583
+ if (this.nexiMountedOrderId && container.childElementCount > 0) {
5584
+ if (this.nexiMountedFor === this.embedSession()) return;
5585
+ this.unmountNexi();
5586
+ container = this.lightContainer("vio-nexi-checkout-container", "vio-nexi");
5587
+ }
5588
+ const mountedFor = this.embedSession();
5589
+ const resumePaymentId = this.nexiResumePaymentId;
5590
+ this.nexiResumePaymentId = null;
5591
+ this.nexiMounting = true;
5592
+ this.nexiShippingNotice = null;
5593
+ container.innerHTML = "";
5594
+ try {
5595
+ const order = await Vio.checkout.mountNexiCheckout(container, this.checkoutState.sponsorId, {
5596
+ onCompleted: (paid) => this.onNexiCompleted(paid.order_id),
5597
+ onShipping: (result, err) => {
5598
+ const wanted = this.nexiShipping?.shipping_id;
5599
+ const pickedNow = this.nexiPicking;
5600
+ this.nexiPicking = false;
5601
+ if (err || !result) {
5602
+ this.nexiShipping = null;
5603
+ this.nexiShippingNotice = "Kunne ikke beregne frakt. Pr\xF8v \xE5 endre adressen.";
5604
+ } else if (result.reason === "AWAITING_ADDRESS") {
5605
+ this.nexiShipping = result;
5606
+ this.nexiShippingNotice = null;
5607
+ } else if (!result.ok) {
5608
+ this.nexiShipping = null;
5609
+ this.nexiShippingNotice = "Vi kan dessverre ikke sende til dette landet.";
5610
+ } else {
5611
+ this.nexiShipping = result;
5612
+ if (result.repriced_at_pay) {
5613
+ this.nexiShippingNotice = "Frakten er oppdatert for leveringsadressen din. Sjekk totalen og trykk \xABBetal\xBB igjen.";
5614
+ } else {
5615
+ this.nexiShippingNotice = !pickedNow && wanted && result.shipping_id !== wanted ? `Valgt frakt leveres ikke til denne adressen. Vi har valgt ${result.shipping_name ?? "en annen"}.` : null;
5616
+ }
5617
+ }
5618
+ }
5619
+ }, resumePaymentId ? { paymentId: resumePaymentId } : void 0);
5620
+ this.nexiMountedOrderId = order.order_id;
5621
+ this.nexiMountedFor = mountedFor;
5622
+ } catch (err) {
5623
+ if (typeof console !== "undefined") {
5624
+ console.warn("[VioCheckout] Nexi mount failed:", err);
5625
+ }
5626
+ this.paymentError = friendlyPaymentError(errorText(err));
5627
+ Vio.checkout.selectPaymentMethod("");
5628
+ } finally {
5629
+ this.nexiMounting = false;
5630
+ }
5631
+ }
5632
+ /** payment-completed: the reservation exists — confirm like the redirect methods do. */
5633
+ onNexiCompleted(paymentId) {
5634
+ const spId = this.checkoutState?.sponsorId ?? 0;
5635
+ const checkoutId = this.checkoutState?.checkoutId ?? "";
5636
+ const charged = this.nexiShipping?.ok ? this.nexiShipping.total_price : void 0;
5637
+ this.confirmOrder("nexi", spId, { chargedTotal: charged });
5638
+ if (this.confirmedOrder) {
5639
+ this.confirmedOrder = {
5640
+ ...this.confirmedOrder,
5641
+ providerRef: { label: "Nexi betalings-ID", value: paymentId }
5642
+ };
5643
+ }
5644
+ this.unmountNexi();
5645
+ if (checkoutId) void this.addNexiPaymentDetails(checkoutId, spId, paymentId);
5646
+ }
5647
+ /**
5648
+ * How it was paid and where the receipt goes — Nexi knows, we ask. Its
5649
+ * payment details can lag the completion event by a moment, hence the
5650
+ * few retries. Best effort: the confirmation stands without them.
5651
+ */
5652
+ async addNexiPaymentDetails(checkoutId, sponsorId, paymentId) {
5653
+ for (const delay of _VioCheckout.NEXI_DETAILS_DELAYS_MS) {
5654
+ if (delay) await new Promise((r) => setTimeout(r, delay));
5655
+ const o = await Vio.checkout.getNexiOrder(checkoutId, sponsorId).catch(() => null);
5656
+ const current = this.confirmedOrder;
5657
+ if (!current || current.providerRef?.value !== paymentId) return;
5658
+ if (!o) continue;
5659
+ const paidWith = o.payment_method ? o.card_last4 ? `${o.payment_method} \u2022\u2022\u2022\u2022 ${o.card_last4}` : o.payment_method : void 0;
5660
+ this.confirmedOrder = {
5661
+ ...current,
5662
+ ...paidWith ? { paidWith } : {},
5663
+ ...o.email ? { email: o.email } : {}
5664
+ };
5665
+ if (paidWith) return;
5666
+ }
5667
+ }
5668
+ static {
5669
+ this.NEXI_DETAILS_DELAYS_MS = [0, 1500, 3e3];
5670
+ }
5671
+ unmountNexi() {
5672
+ this.nexiMountedOrderId = null;
5673
+ this.nexiMountedFor = null;
5674
+ this.nexiShippingNotice = null;
5675
+ this.nexiShipping = null;
5676
+ this.nexiPicking = false;
5677
+ Vio.checkout.destroyNexi();
5678
+ this.querySelector("#vio-nexi-checkout-container")?.remove();
5679
+ }
5680
+ /**
5681
+ * Nexi has no shipping picker: once an address is typed in the widget, the
5682
+ * rates that reach it are listed here and the pick is pushed to the payment.
5683
+ * With a single rate there is nothing to choose, so nothing is shown.
5684
+ */
5685
+ renderNexiShippingChoice() {
5686
+ const options = this.nexiShipping?.options ?? [];
5687
+ if (options.length < 2) return "";
5688
+ const currency = this.checkoutState?.currency || getGlobalCurrency();
5689
+ const current = this.nexiShipping?.shipping_id;
5690
+ return html`
5691
+ <div class="ship-select nexi-ship" role="radiogroup" aria-label="Frakt">
5692
+ ${options.map(
5693
+ (o) => html`
5694
+ <button
5695
+ class="ship-opt ${current === o.id ? "active" : ""}"
5696
+ role="radio"
5697
+ aria-checked=${current === o.id}
5698
+ ?disabled=${this.nexiPicking}
5699
+ @click=${() => void this.onPickNexiShipping(o.id)}
5700
+ >
5701
+ <span class="ship-meta"><b>${o.name}</b></span>
5702
+ <span class="ship-price">${formatPrice(o.price, currency)}</span>
5703
+ </button>
5704
+ `
5705
+ )}
5706
+ </div>
5707
+ `;
5708
+ }
5709
+ async onPickNexiShipping(id) {
5710
+ if (this.nexiPicking || id === this.nexiShipping?.shipping_id) return;
5711
+ this.nexiPicking = true;
5712
+ try {
5713
+ await Vio.checkout.pickNexiShipping(id);
5714
+ } finally {
5715
+ this.nexiPicking = false;
5716
+ }
5717
+ }
5718
+ /* ── Adyen: our form first, then Adyen's Drop-in on a session ─────────── */
5719
+ /**
5720
+ * What an embedded payment is a photo OF — an Adyen session, a Stripe
5721
+ * PaymentIntent. When any of it changes underneath, that photo no longer
5722
+ * describes the purchase: it is dropped, and the shopper asks for the
5723
+ * payment step again.
5724
+ */
5725
+ embedPurchaseKey() {
5726
+ return JSON.stringify([
5727
+ this.embedFormKey(),
5728
+ this.items.map((i) => [i.productId, i.variantId ?? "", i.quantity]),
5729
+ this.checkoutState?.subtotal ?? 0
5730
+ ]);
5731
+ }
5732
+ /** The part of the photo the shopper types and picks. */
5733
+ embedFormKey() {
5734
+ const f = this.form;
5735
+ return JSON.stringify([
5736
+ f.firstName,
5737
+ f.lastName,
5738
+ f.email,
5739
+ f.address,
5740
+ f.postalCode,
5741
+ f.city,
5742
+ f.country ?? "",
5743
+ this.selectedShipping
5744
+ ]);
5745
+ }
5746
+ /** A mounted session whose purchase changed is taken down — never patched. */
5747
+ dropStaleAdyen() {
5748
+ if (!this.adyenMounted || this.adyenMounting) return;
5749
+ if (this.adyenMountedKey === this.embedPurchaseKey()) return;
5750
+ this.unmountAdyen();
5751
+ this.adyenStale = true;
5752
+ }
5753
+ async mountAdyenIfNeeded() {
5754
+ if (!this.open || !this.checkoutState) return;
5755
+ if (this.checkoutState.paymentMethod !== "adyen") return;
5756
+ if (!this.mayStartEmbeddedPayment() || this.adyenSettled) return;
5757
+ if (this.adyenMounting) return;
5758
+ if (this.adyenMounted) {
5759
+ if (this.adyenMountedFor === this.embedSession()) return;
5760
+ this.unmountAdyen();
5761
+ }
5762
+ if (!this.adyenRequested || !this.isAddressValid || !this.hasSelectedShipping) return;
5763
+ this.adyenRequested = false;
5764
+ this.adyenMounting = true;
5765
+ this.adyenStale = false;
5766
+ this.paymentError = null;
5767
+ const spId = this.checkoutState.sponsorId;
5768
+ const mountedFor = this.embedSession();
5769
+ const formKey = this.embedFormKey();
5770
+ try {
5771
+ const selectedOpt = this.shippingList.find((o) => o.id === this.selectedShipping);
5772
+ if (selectedOpt?.supplierId) {
5773
+ await Vio.checkout.updateShippingsBySupplier(
5774
+ [{ shipping_id: selectedOpt.id, supplier_id: Number(selectedOpt.supplierId) }],
5775
+ spId
5776
+ );
5777
+ }
5778
+ await this.updateComplete;
5779
+ const container = this.lightContainer("vio-adyen-checkout-container", "vio-adyen");
5780
+ container.innerHTML = "";
5781
+ await Vio.checkout.mountAdyenCheckout(container, spId, this.form, {
5782
+ onCompleted: (payment, session) => this.onAdyenCompleted(spId, payment, session),
5783
+ onFailed: () => {
5784
+ this.unmountAdyen();
5785
+ this.paymentError = "Betalingen ble ikke gjennomf\xF8rt. Pr\xF8v igjen, eller velg en annen betalingsm\xE5te.";
5786
+ },
5787
+ onReplaced: (reason) => {
5788
+ this.unmountAdyen();
5789
+ this.adyenStale = true;
5790
+ if (reason === "ALREADY_PAID") {
5791
+ this.adyenSettled = true;
5792
+ this.paymentNotice = "Denne bestillingen er allerede betalt.";
5793
+ }
5794
+ },
5795
+ onError: (error) => {
5796
+ if (typeof console !== "undefined") console.warn("[VioCheckout] Adyen error:", error);
5797
+ }
5798
+ });
5799
+ if (this.embedSession() !== mountedFor || this.checkoutState?.paymentMethod !== "adyen" || !this.open) {
5800
+ this.unmountAdyen();
5801
+ return;
5802
+ }
5803
+ if (this.embedFormKey() !== formKey) {
5804
+ this.unmountAdyen();
5805
+ this.adyenStale = true;
5806
+ return;
5807
+ }
5808
+ this.adyenMounted = true;
5809
+ this.adyenMountedFor = mountedFor;
5810
+ this.adyenMountedKey = this.embedPurchaseKey();
5811
+ } catch (err) {
5812
+ this.unmountAdyen();
5813
+ this.paymentError = err instanceof AdyenOriginError ? "Betaling er ikke aktivert for denne siden enn\xE5. Ta kontakt med butikken." : friendlyPaymentError(errorText(err));
5814
+ if (typeof console !== "undefined") console.warn("[VioCheckout] Adyen mount failed:", err);
5815
+ } finally {
5816
+ this.adyenMounting = false;
5817
+ }
5818
+ }
5819
+ onAdyenCompleted(sponsorId, payment, session) {
5820
+ const outcome = adyenReturnOutcome(payment);
5821
+ this.unmountAdyen();
5822
+ if (outcome === "paid") {
5823
+ this.adyenSettled = true;
5824
+ this.confirmOrder("adyen", sponsorId, {
5825
+ chargedTotal: payment?.total_price ?? session.total_price
5826
+ });
5827
+ this.addAdyenPaymentDetails(payment);
5828
+ } else if (outcome === "pending") {
5829
+ this.adyenSettled = true;
5830
+ this.paymentNotice = "Betalingen behandles fortsatt. Du f\xE5r ordrebekreftelse p\xE5 e-post n\xE5r den er gjennomf\xF8rt. Handlekurven er uendret.";
5831
+ } else {
5832
+ this.paymentError = "Betalingen ble ikke gjennomf\xF8rt. Pr\xF8v igjen, eller velg en annen betalingsm\xE5te.";
5833
+ }
5834
+ }
5835
+ /** What Adyen told us about the payment, on Vio's confirmation. */
5836
+ addAdyenPaymentDetails(payment) {
5837
+ if (!this.confirmedOrder || !payment) return;
5838
+ this.confirmedOrder = {
5839
+ ...this.confirmedOrder,
5840
+ ...payment.psp_reference ? { providerRef: { label: "Betalingsreferanse", value: payment.psp_reference } } : {},
5841
+ ...payment.payment_method ? { paidWith: payment.payment_method } : {},
5842
+ ...payment.email && !this.confirmedOrder.email ? { email: payment.email } : {},
5843
+ ...payment.shipping_name && !this.confirmedOrder.shipping ? { shipping: { name: payment.shipping_name, price: payment.shipping_price ?? 0 } } : {}
5844
+ };
5845
+ }
5846
+ unmountAdyen() {
5847
+ Vio.checkout.destroyAdyen();
5848
+ this.adyenMounted = false;
5849
+ this.adyenMountedFor = null;
5850
+ this.adyenMountedKey = null;
5851
+ this.adyenRequested = false;
5852
+ this.adyenStale = false;
5853
+ this.querySelector("#vio-adyen-checkout-container")?.remove();
5854
+ }
5855
+ renderAdyenPanel() {
5856
+ const ready = this.isAddressValid && this.hasSelectedShipping;
5857
+ const showButton = !this.adyenMounted && !this.adyenMounting && !this.adyenSettled;
5858
+ return html`
5859
+ <div class="adyen-panel">
5860
+ ${this.adyenMounting ? html`<div style="font-size:13px;opacity:0.7;padding:8px 0;">Laster betaling…</div>` : ""}
5861
+ ${this.adyenStale && showButton ? html`<div class="payment-notice">Bestillingen ble endret. Oppdater betalingen for å fortsette.</div>` : ""}
5862
+ ${showButton ? html`
5863
+ <button
5864
+ class="payment-btn primary complete-cta"
5865
+ ?disabled=${!ready}
5866
+ title=${!this.isAddressValid ? "Vennligst fyll ut leveringsadresse" : !this.hasSelectedShipping ? "Vennligst velg en fraktmetode" : ""}
5867
+ @click=${() => {
5868
+ this.adyenRequested = true;
5869
+ }}
5870
+ >
5871
+ ${this.adyenStale ? "Oppdater betaling" : `G\xE5 til betaling \xB7 ${this.payTotalLabel()}`}
5872
+ </button>
5873
+ ` : ""}
5874
+ <slot name="vio-adyen"></slot>
5875
+ </div>
5876
+ `;
5877
+ }
5878
+ /* ── Stripe, native: the Payment Element on our own page ──────────────── */
5879
+ /** A mounted Element whose purchase changed is taken down — never patched. */
5880
+ dropStaleStripe() {
5881
+ if (!this.stripeMounted || this.stripeMounting) return;
5882
+ if (this.stripeMountedKey === this.embedPurchaseKey()) return;
5883
+ this.unmountStripe();
5884
+ this.stripeStale = true;
5885
+ }
5886
+ async mountStripeIfNeeded() {
5887
+ if (!this.open || !this.checkoutState) return;
5888
+ if (this.checkoutState.paymentMethod !== "stripe" || this.stripeMode !== "native") return;
5889
+ if (!this.mayStartEmbeddedPayment() || this.stripeSettled) return;
5890
+ if (this.stripeMounting) return;
5891
+ if (this.stripeMounted) {
5892
+ if (this.stripeMountedFor === this.embedSession()) return;
5893
+ this.unmountStripe();
5894
+ }
5895
+ if (!this.stripeRequested || !this.isAddressValid || !this.hasSelectedShipping) return;
5896
+ this.stripeRequested = false;
5897
+ this.stripeMounting = true;
5898
+ this.stripeStale = false;
5899
+ this.paymentError = null;
5900
+ const spId = this.checkoutState.sponsorId;
5901
+ const mountedFor = this.embedSession();
5902
+ const formKey = this.embedFormKey();
5903
+ try {
5904
+ const selectedOpt = this.shippingList.find((o) => o.id === this.selectedShipping);
5905
+ if (selectedOpt?.supplierId) {
5906
+ await Vio.checkout.updateShippingsBySupplier(
5907
+ [{ shipping_id: selectedOpt.id, supplier_id: Number(selectedOpt.supplierId) }],
5908
+ spId
5909
+ );
5910
+ }
5911
+ await this.updateComplete;
5912
+ const container = this.lightContainer("vio-stripe-checkout-container", "vio-stripe");
5913
+ container.innerHTML = "";
5914
+ this.stripeReady = false;
5915
+ const mounted = await Vio.checkout.mountStripeCheckout(container, spId, this.form, {
5916
+ // `mount()` resolves before Stripe paints anything — seconds before,
5917
+ // on a cold cache. Its own "ready" is what turns the spinner off.
5918
+ onReady: () => {
5919
+ this.stripeReady = true;
5920
+ },
5921
+ onLoadError: (message) => {
5922
+ this.unmountStripe();
5923
+ if (typeof console !== "undefined") {
5924
+ console.warn("[VioCheckout] Stripe Payment Element failed to load:", message);
5925
+ }
5926
+ this.paymentError = friendlyPaymentError(message, "stripe");
5927
+ }
5928
+ });
5929
+ if (this.embedSession() !== mountedFor || this.checkoutState?.paymentMethod !== "stripe" || !this.open) {
5930
+ this.unmountStripe();
5931
+ return;
5932
+ }
5933
+ if (this.embedFormKey() !== formKey) {
5934
+ this.unmountStripe();
5935
+ this.stripeStale = true;
5936
+ return;
5937
+ }
5938
+ this.stripeElement = mounted.handle;
5939
+ this.stripeCheckoutId = mounted.checkoutId;
5940
+ this.stripeMounted = true;
5941
+ this.stripeMountedFor = mountedFor;
5942
+ this.stripeMountedKey = this.embedPurchaseKey();
5943
+ } catch (err) {
5944
+ this.unmountStripe();
5945
+ this.paymentError = friendlyPaymentError(errorText(err));
5946
+ if (typeof console !== "undefined") console.warn("[VioCheckout] Stripe mount failed:", err);
5947
+ } finally {
5948
+ this.stripeMounting = false;
5949
+ }
5950
+ }
5951
+ /**
5952
+ * Pay with the mounted Element. Stripe answers the BROWSER; the order is
5953
+ * created server-side by the `payment_intent.succeeded` webhook, so a
5954
+ * success is then waited for on the checkout itself.
5955
+ */
5956
+ async onStripePay() {
5957
+ if (!this.stripeElement || this.stripePaying) return;
5958
+ const spId = this.checkoutState?.sponsorId ?? 0;
5959
+ const checkoutId = this.stripeCheckoutId;
5960
+ const charged = (this.checkoutState?.subtotal ?? 0) + this.shippingMajor();
5961
+ this.stripePaying = true;
5962
+ this.paymentError = null;
5963
+ try {
5964
+ const outcome = await this.stripeElement.confirm();
5965
+ if (outcome.status === "failed") {
5966
+ this.paymentError = outcome.message || "Betalingen ble ikke gjennomf\xF8rt. Pr\xF8v igjen, eller velg en annen betalingsm\xE5te.";
5967
+ return;
5968
+ }
5969
+ this.stripeSettled = true;
5970
+ this.unmountStripe();
5971
+ const settled = checkoutId ? await this.waitForStripeOrder(checkoutId, spId) : "pending";
5972
+ if (settled === "paid") {
5973
+ this.confirmOrder("stripe", spId, { chargedTotal: charged });
5974
+ } else if (settled === "duplicate") {
5975
+ this.stripeSettled = false;
5976
+ this.paymentError = CART_PAID_MESSAGE;
5977
+ } else if (settled === "failed") {
5978
+ this.stripeSettled = false;
5979
+ this.paymentError = "Betalingen ble avbrutt eller feilet. Vennligst pr\xF8v igjen.";
5980
+ } else {
5981
+ this.paymentNotice = "Betalingen behandles fortsatt. Du f\xE5r ordrebekreftelse p\xE5 e-post n\xE5r den er gjennomf\xF8rt. Handlekurven er uendret.";
5982
+ }
5983
+ } catch (err) {
5984
+ this.paymentError = `Betalingen kunne ikke fullf\xF8res: ${err instanceof Error ? err.message : String(err)}`;
5985
+ if (typeof console !== "undefined") console.warn("[VioCheckout] Stripe confirm failed:", err);
5986
+ } finally {
5987
+ this.stripePaying = false;
5988
+ }
5989
+ }
5990
+ /**
5991
+ * Wait for Stripe's webhook to make the checkout paid — the same window the
5992
+ * redirect return uses, and ONLY the status: a Stripe checkout carries
5993
+ * `origin_payment_id` from the moment the intent is created, so that
5994
+ * pointer would read as paid before anyone paid.
5995
+ */
5996
+ async waitForStripeOrder(checkoutId, sponsorId) {
5997
+ for (const delayMs of _VioCheckout.RETURN_VERIFY_DELAYS_MS) {
5998
+ if (delayMs > 0) await new Promise((r) => setTimeout(r, delayMs));
5999
+ try {
6000
+ const checkout = await Vio.checkout.getCheckout(checkoutId, sponsorId || void 0);
6001
+ const status = String(checkout?.status ?? "").toLowerCase();
6002
+ if (checkout?.duplicate_of) return "duplicate";
6003
+ if (_VioCheckout.FAILED_STATUSES.has(status)) return "failed";
6004
+ if (_VioCheckout.PAID_STATUSES.has(status)) return "paid";
6005
+ } catch (err) {
6006
+ if (typeof console !== "undefined") {
6007
+ console.warn("[VioCheckout] Stripe order check failed:", err);
6008
+ }
6009
+ }
6010
+ }
6011
+ return "pending";
6012
+ }
6013
+ unmountStripe() {
6014
+ Vio.checkout.destroyStripe();
6015
+ this.stripeReady = false;
6016
+ this.stripeElement = null;
6017
+ this.stripeCheckoutId = null;
6018
+ this.stripeMounted = false;
6019
+ this.stripeMountedFor = null;
6020
+ this.stripeMountedKey = null;
6021
+ this.stripeRequested = false;
6022
+ this.stripeStale = false;
6023
+ this.querySelector("#vio-stripe-checkout-container")?.remove();
6024
+ }
6025
+ renderStripePanel() {
6026
+ const ready = this.isAddressValid && this.hasSelectedShipping;
6027
+ const showButton = !this.stripeMounted && !this.stripeMounting && !this.stripeSettled;
6028
+ return html`
6029
+ <div class="stripe-panel">
6030
+ ${this.stripeMounting || this.stripeMounted && !this.stripeReady ? html`<div style="font-size:13px;opacity:0.7;padding:8px 0;">Laster betaling…</div>` : ""}
6031
+ ${this.stripeStale && showButton ? html`<div class="payment-notice">Bestillingen ble endret. Oppdater betalingen for å fortsette.</div>` : ""}
6032
+ ${showButton ? html`
6033
+ <button
6034
+ class="payment-btn primary complete-cta"
6035
+ ?disabled=${!ready}
6036
+ title=${!this.isAddressValid ? "Vennligst fyll ut leveringsadresse" : !this.hasSelectedShipping ? "Vennligst velg en fraktmetode" : ""}
6037
+ @click=${() => {
6038
+ this.stripeRequested = true;
6039
+ }}
6040
+ >
6041
+ ${this.stripeStale ? "Oppdater betaling" : `G\xE5 til betaling \xB7 ${this.payTotalLabel()}`}
6042
+ </button>
6043
+ ` : ""}
6044
+ <slot name="vio-stripe"></slot>
6045
+ ${this.stripeMounted ? html`
6046
+ <button
6047
+ class="payment-btn primary complete-cta"
6048
+ ?disabled=${this.stripePaying || !this.stripeReady}
6049
+ @click=${() => void this.onStripePay()}
6050
+ >
6051
+ ${this.stripePaying ? "Betaler\u2026" : `Betal ${this.payTotalLabel()}`}
6052
+ </button>
6053
+ ` : ""}
6054
+ </div>
6055
+ `;
6056
+ }
6057
+ renderNexiPanel() {
6058
+ return html`
6059
+ <div class="nexi-panel">
6060
+ ${this.nexiMounting ? html`<div style="font-size:13px;opacity:0.7;padding:8px 0;">Laster Nexi…</div>` : ""}
6061
+ ${this.renderNexiShippingChoice()}
6062
+ <slot name="vio-nexi"></slot>
6063
+ ${this.nexiShippingNotice ? html`<div class="payment-notice">${this.nexiShippingNotice}</div>` : ""}
6064
+ </div>
6065
+ `;
6066
+ }
4387
6067
  renderKlarnaPanel() {
4388
6068
  const currency = this.checkoutState?.currency || getGlobalCurrency();
4389
6069
  return html`
@@ -4433,7 +6113,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4433
6113
  <button
4434
6114
  class="payment-btn primary complete-cta"
4435
6115
  @click=${() => void this.onKlarnaAuthorize()}
4436
- ?disabled=${this.klarnaAuthorizing || !this.klarnaHandle || this.klarnaMounting}
6116
+ ?disabled=${this.klarnaAuthorizing || !this.klarnaHandle || this.klarnaMounting || !this.express && (!this.isAddressValid || !this.hasSelectedShipping)}
4437
6117
  >
4438
6118
  ${this.klarnaAuthorizing ? "Behandler\u2026" : this.klarnaMounting ? "Klargj\xF8r Klarna\u2026" : `Betal ${this.payTotalLabel()} med Klarna`}
4439
6119
  </button>
@@ -4483,7 +6163,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4483
6163
  </div>
4484
6164
  </div>
4485
6165
  ` : ""}
4486
- ${this.renderKlarnaPanel()}
6166
+ ${this.shippingBlocked ? this.renderShippingBlocked() : this.renderKlarnaPanel()}
4487
6167
  ${this.paymentNotice ? html`<div class="payment-notice">${this.paymentNotice}</div>` : ""}
4488
6168
  ${this.paymentError ? html`<div class="payment-error">${this.paymentError}</div>` : ""}
4489
6169
  </section>
@@ -4493,10 +6173,10 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4493
6173
  const method = this.checkoutState?.paymentMethod;
4494
6174
  const showCompleteCta = !!method && !this.isExpressMethod(method);
4495
6175
  const isVipps = method === "vipps";
4496
- const isKustom = method === "kustom" || method === "qliro" || method === "walley";
6176
+ const needsOurForm = !!method && !collectsOwnAddress(method);
4497
6177
  return html`
4498
- ${!isVipps && !isKustom ? html`
4499
- <section class="section">
6178
+ ${needsOurForm ? html`
6179
+ <section class="section address-section">
4500
6180
  <div class="section-label">Steg 1</div>
4501
6181
  <h3 class="section-heading">Leveringsadresse</h3>
4502
6182
  <div class="form-row">
@@ -4612,32 +6292,41 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4612
6292
  ` : ""}
4613
6293
 
4614
6294
  <section class="section">
4615
- <div class="section-label">${isVipps || isKustom ? "Betaling" : "Steg 2"}</div>
4616
- ${method ? html`
6295
+ <div class="section-label">
6296
+ ${needsOurForm ? "Steg 2" : "Betaling"}
6297
+ </div>
6298
+ ${this.shippingBlocked ? this.renderShippingBlocked() : method ? html`
4617
6299
  <div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px;">
4618
6300
  <h3 class="section-heading" style="margin:0;">
4619
6301
  Betalingsmåte: <b>${this.methodLabel(method)}</b>
4620
6302
  </h3>
4621
- <button
4622
- type="button"
4623
- style="background:none; border:none; color:var(--vio-color-accent, #c14a3b); text-decoration:underline; font-size:13px; cursor:pointer;"
4624
- @click=${() => {
6303
+ ${(this.availableMethods?.length ?? 2) > 1 ? html`
6304
+ <button
6305
+ type="button"
6306
+ style="background:none; border:none; color:var(--vio-color-accent, #c14a3b); text-decoration:underline; font-size:13px; cursor:pointer;"
6307
+ @click=${() => {
4625
6308
  this.unmountKlarna();
4626
6309
  this.unmountKustom();
4627
6310
  this.unmountQliro();
4628
6311
  this.unmountWalley();
6312
+ this.unmountNexi();
6313
+ this.unmountAdyen();
4629
6314
  Vio.checkout.selectPaymentMethod("");
4630
6315
  }}
4631
- >
4632
- Endre
4633
- </button>
6316
+ >
6317
+ Endre
6318
+ </button>
6319
+ ` : ""}
4634
6320
  </div>
4635
6321
 
4636
6322
  ${method === "klarna" ? this.renderKlarnaPanel() : ""}
4637
6323
  ${method === "kustom" ? this.renderKustomPanel() : ""}
4638
6324
  ${method === "qliro" ? this.renderQliroPanel() : ""}
4639
6325
  ${method === "walley" ? this.renderWalleyPanel() : ""}
4640
- ${method === "stripe" ? html`
6326
+ ${method === "nexi" ? this.renderNexiPanel() : ""}
6327
+ ${method === "adyen" ? this.renderAdyenPanel() : ""}
6328
+ ${method === "stripe" && this.stripeMode === "native" ? this.renderStripePanel() : ""}
6329
+ ${method === "stripe" && this.stripeMode !== "native" ? html`
4641
6330
  <button
4642
6331
  class="payment-btn primary complete-cta"
4643
6332
  @click=${() => void this.onPay("stripe")}
@@ -4676,19 +6365,25 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4676
6365
  Betal ${this.payTotalLabel()} med ${this.methodLabel(method ?? null)}
4677
6366
  </button>
4678
6367
  ` : ""}
4679
- ${!isVipps && !this.isAddressValid ? html`
6368
+ ${needsOurForm && !this.isAddressValid ? html`
4680
6369
  <div style="font-size: 12px; color: var(--vio-color-accent, #c14a3b); text-align: center; margin-top: 8px;">
4681
6370
  Vennligst fyll ut leveringsadresse for å fullføre betalingen
4682
6371
  </div>
4683
- ` : !isVipps && !this.hasSelectedShipping ? html`
6372
+ ` : needsOurForm && !this.hasSelectedShipping ? html`
4684
6373
  <div style="font-size: 12px; color: var(--vio-color-accent, #c14a3b); text-align: center; margin-top: 8px;">
4685
6374
  Vennligst velg en fraktmetode for å fullføre betalingen
4686
6375
  </div>
4687
6376
  ` : ""}
4688
- ` : html`
6377
+ ` : !this.paymentMethodsResolved ? html`
6378
+ <h3 class="section-heading">Velg betalingsmåte</h3>
6379
+ <div class="payment-loading">
6380
+ <span class="payment-spinner" aria-hidden="true"></span>
6381
+ <span>Henter betalingsmåter…</span>
6382
+ </div>
6383
+ ` : html`
4689
6384
  <h3 class="section-heading">Velg betalingsmåte</h3>
4690
6385
  <div class="payment-grid">
4691
- ${this.methodEnabled("apple-pay") && this.applePayAvailable ? html`
6386
+ ${this.methodEnabled("apple-pay") && this.applePayAvailable && !this.stripeOwnsWallets() ? html`
4692
6387
  <button
4693
6388
  class="payment-btn primary"
4694
6389
  @click=${() => this.onPay("apple-pay")}
@@ -4726,15 +6421,26 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4726
6421
  <span style="font-weight:800;font-size:16px;letter-spacing:-0.01em">Walley</span>
4727
6422
  </button>
4728
6423
  ` : ""}
4729
- ${this.methodEnabled("vipps") ? html`
4730
- <button class="payment-btn" @click=${() => this.onPay("vipps")}>
4731
- <span style="color:#ff5b24;font-weight:800;font-size:17px;letter-spacing:-0.02em">vipps</span>
6424
+ ${this.methodEnabled("nexi") ? html`
6425
+ <button class="payment-btn" @click=${() => this.onPay("nexi")}>
6426
+ <span style="font-weight:800;font-size:16px;letter-spacing:-0.01em">Nexi</span>
4732
6427
  </button>
4733
6428
  ` : ""}
4734
- ${this.methodEnabled("apple-pay") && !this.applePayAvailable ? html`
4735
- <div class="apple-pay-note">
4736
- Pay krever Safari på iOS eller macOS med Apple Pay konfigurert.
4737
- </div>
6429
+ ${this.methodEnabled("adyen") ? html`
6430
+ <button class="payment-btn" @click=${() => this.onPay("adyen")}>
6431
+ <span style="font-weight:700;font-size:15px;letter-spacing:-0.01em">${this.methodLabel("adyen")}</span>
6432
+ </button>
6433
+ ` : ""}
6434
+ ${this.methodEnabled("vipps") ? html`
6435
+ <button class="payment-btn" @click=${() => this.onPay("vipps")} aria-label="Vipps">
6436
+ ${this.vippsOfficial ? html`<vipps-mobilepay-button
6437
+ brand="vipps"
6438
+ variant="primary"
6439
+ rounded="true"
6440
+ compact="true"
6441
+ style="pointer-events:none"
6442
+ ></vipps-mobilepay-button>` : html`<span style="color:#ff5b24;font-weight:800;font-size:17px;letter-spacing:-0.02em">vipps</span>`}
6443
+ </button>
4738
6444
  ` : ""}
4739
6445
  </div>
4740
6446
  `}
@@ -4765,7 +6471,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4765
6471
  <span>Sum</span><span>${this.orderTotal()}</span>
4766
6472
  </div>
4767
6473
  <div class="order-row">
4768
- <span>Frakt${this.shippingMajor() > 0 && this.shippingOption ? ` \u2013 ${this.shippingOption.method || this.shippingOption.name}` : ""}</span>
6474
+ <span>Frakt${this.shippingMajor() > 0 && this.summaryShippingName() ? ` \u2013 ${this.summaryShippingName()}` : ""}</span>
4769
6475
  <span>${this.shippingMajor() > 0 ? formatPrice(this.shippingMajor(), this.checkoutState?.currency) : this.shippingLabel}</span>
4770
6476
  </div>
4771
6477
  <div class="order-row total">
@@ -4808,6 +6514,9 @@ __decorateClass([
4808
6514
  __decorateClass([
4809
6515
  state()
4810
6516
  ], _VioCheckout.prototype, "orderConfirmed", 2);
6517
+ __decorateClass([
6518
+ state()
6519
+ ], _VioCheckout.prototype, "providerReceipt", 2);
4811
6520
  __decorateClass([
4812
6521
  state()
4813
6522
  ], _VioCheckout.prototype, "confirmedMethod", 2);
@@ -4817,6 +6526,9 @@ __decorateClass([
4817
6526
  __decorateClass([
4818
6527
  state()
4819
6528
  ], _VioCheckout.prototype, "paymentNotice", 2);
6529
+ __decorateClass([
6530
+ state()
6531
+ ], _VioCheckout.prototype, "verifyingReturn", 2);
4820
6532
  __decorateClass([
4821
6533
  state()
4822
6534
  ], _VioCheckout.prototype, "confirmedOrder", 2);
@@ -4838,15 +6550,84 @@ __decorateClass([
4838
6550
  __decorateClass([
4839
6551
  state()
4840
6552
  ], _VioCheckout.prototype, "availableMethods", 2);
6553
+ __decorateClass([
6554
+ state()
6555
+ ], _VioCheckout.prototype, "vippsOfficial", 2);
4841
6556
  __decorateClass([
4842
6557
  state()
4843
6558
  ], _VioCheckout.prototype, "kustomMounting", 2);
6559
+ __decorateClass([
6560
+ state()
6561
+ ], _VioCheckout.prototype, "kustomSyncing", 2);
6562
+ __decorateClass([
6563
+ state()
6564
+ ], _VioCheckout.prototype, "kustomShipping", 2);
6565
+ __decorateClass([
6566
+ state()
6567
+ ], _VioCheckout.prototype, "kustomTotal", 2);
6568
+ __decorateClass([
6569
+ state()
6570
+ ], _VioCheckout.prototype, "kustomNotice", 2);
4844
6571
  __decorateClass([
4845
6572
  state()
4846
6573
  ], _VioCheckout.prototype, "qliroMounting", 2);
4847
6574
  __decorateClass([
4848
6575
  state()
4849
6576
  ], _VioCheckout.prototype, "walleyMounting", 2);
6577
+ __decorateClass([
6578
+ state()
6579
+ ], _VioCheckout.prototype, "nexiMounting", 2);
6580
+ __decorateClass([
6581
+ state()
6582
+ ], _VioCheckout.prototype, "nexiShippingNotice", 2);
6583
+ __decorateClass([
6584
+ state()
6585
+ ], _VioCheckout.prototype, "nexiShipping", 2);
6586
+ __decorateClass([
6587
+ state()
6588
+ ], _VioCheckout.prototype, "nexiPicking", 2);
6589
+ __decorateClass([
6590
+ state()
6591
+ ], _VioCheckout.prototype, "adyenMounting", 2);
6592
+ __decorateClass([
6593
+ state()
6594
+ ], _VioCheckout.prototype, "adyenRequested", 2);
6595
+ __decorateClass([
6596
+ state()
6597
+ ], _VioCheckout.prototype, "adyenMounted", 2);
6598
+ __decorateClass([
6599
+ state()
6600
+ ], _VioCheckout.prototype, "adyenStale", 2);
6601
+ __decorateClass([
6602
+ state()
6603
+ ], _VioCheckout.prototype, "adyenSettled", 2);
6604
+ __decorateClass([
6605
+ state()
6606
+ ], _VioCheckout.prototype, "stripeMode", 2);
6607
+ __decorateClass([
6608
+ state()
6609
+ ], _VioCheckout.prototype, "stripeRequested", 2);
6610
+ __decorateClass([
6611
+ state()
6612
+ ], _VioCheckout.prototype, "stripeMounting", 2);
6613
+ __decorateClass([
6614
+ state()
6615
+ ], _VioCheckout.prototype, "stripeMounted", 2);
6616
+ __decorateClass([
6617
+ state()
6618
+ ], _VioCheckout.prototype, "stripeStale", 2);
6619
+ __decorateClass([
6620
+ state()
6621
+ ], _VioCheckout.prototype, "stripeSettled", 2);
6622
+ __decorateClass([
6623
+ state()
6624
+ ], _VioCheckout.prototype, "stripePaying", 2);
6625
+ __decorateClass([
6626
+ state()
6627
+ ], _VioCheckout.prototype, "stripeReady", 2);
6628
+ __decorateClass([
6629
+ state()
6630
+ ], _VioCheckout.prototype, "qliroShipping", 2);
4850
6631
  __decorateClass([
4851
6632
  state()
4852
6633
  ], _VioCheckout.prototype, "stripeLoading", 2);
@@ -4856,6 +6637,15 @@ __decorateClass([
4856
6637
  __decorateClass([
4857
6638
  state()
4858
6639
  ], _VioCheckout.prototype, "form", 2);
6640
+ __decorateClass([
6641
+ state()
6642
+ ], _VioCheckout.prototype, "paymentMethodsResolved", 2);
6643
+ __decorateClass([
6644
+ state()
6645
+ ], _VioCheckout.prototype, "shippingBlocked", 2);
6646
+ __decorateClass([
6647
+ state()
6648
+ ], _VioCheckout.prototype, "shippingsReadyFor", 2);
4859
6649
  var VioCheckout = _VioCheckout;
4860
6650
 
4861
6651
  // src/ui/elements.ts
@@ -4875,5 +6665,5 @@ function registerVioElements() {
4875
6665
  }
4876
6666
 
4877
6667
  export { VioCart, VioCheckout, VioProduct, VioProductCarousel, VioProductDetail, VioTokens, applyVioTheme, registerVioElements };
4878
- //# sourceMappingURL=chunk-7TGCXO5J.js.map
4879
- //# sourceMappingURL=chunk-7TGCXO5J.js.map
6668
+ //# sourceMappingURL=chunk-MRJR6DG5.js.map
6669
+ //# sourceMappingURL=chunk-MRJR6DG5.js.map