@vio-live/web-sdk 0.11.1 → 0.19.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-CMNTHF66.js';
2
2
  import { LitElement, css, html } from 'lit';
3
3
  import { property, state, query } from 'lit/decorators.js';
4
4
 
@@ -863,6 +863,29 @@ 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/components/vio-product-detail.ts
866
889
  var STRIPE_LOGO_SRC = "data:image/svg+xml;utf8," + encodeURIComponent(
867
890
  '<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
891
  );
@@ -875,6 +898,8 @@ var VioProductDetail = class extends LitElement {
875
898
  this.autoOpen = true;
876
899
  this.currency = "NOK";
877
900
  this.product = null;
901
+ this.sellerName = null;
902
+ this.sellerSettled = false;
878
903
  this.isLoading = false;
879
904
  this.fetchError = "";
880
905
  this.selectedOptions = {};
@@ -883,6 +908,7 @@ var VioProductDetail = class extends LitElement {
883
908
  this.adding = false;
884
909
  this.applePayOk = false;
885
910
  this.availableMethods = null;
911
+ this.vippsOfficial = false;
886
912
  /** Race guard: only the latest fetch may write state (two quick taps on
887
913
  * different cards would otherwise let the slow response win). */
888
914
  this.fetchSeq = 0;
@@ -1193,6 +1219,25 @@ var VioProductDetail = class extends LitElement {
1193
1219
  cursor: not-allowed;
1194
1220
  }
1195
1221
  /* Klarna express button — sits full-width below the add-to-cart row. */
1222
+ /* Generic buy-now for the embedded checkouts, which have no branded
1223
+ express button of their own. */
1224
+ .buy-now {
1225
+ width: 100%;
1226
+ margin-top: 10px;
1227
+ padding: 14px 24px;
1228
+ background: var(--vio-color-text, #0a0a0a);
1229
+ color: var(--vio-color-text-on-primary, #fff);
1230
+ border: none;
1231
+ border-radius: var(--vio-radius-md, 4px);
1232
+ font-family: var(--vio-font-sans, inherit);
1233
+ font-size: 12px;
1234
+ text-transform: uppercase;
1235
+ letter-spacing: var(--vio-tracking-label, 0.1em);
1236
+ font-weight: 700;
1237
+ cursor: pointer;
1238
+ }
1239
+ .buy-now:hover:not(:disabled) { opacity: 0.9; }
1240
+ .buy-now:disabled { opacity: 0.5; cursor: not-allowed; }
1196
1241
  .buy-klarna {
1197
1242
  width: 100%;
1198
1243
  margin-top: 10px;
@@ -1244,6 +1289,8 @@ var VioProductDetail = class extends LitElement {
1244
1289
  }
1245
1290
  .buy-vipps:hover:not(:disabled) { background: #ec4f1c; }
1246
1291
  .buy-vipps:disabled { opacity: 0.5; cursor: not-allowed; }
1292
+ /* Vipps' own button (web component) — same slot, their drawing. */
1293
+ .buy-vipps-official { display: block; width: 100%; margin-top: 10px; cursor: pointer; }
1247
1294
  /* Apple Pay express button — black, per Apple's button guidelines. Only
1248
1295
  shown in Safari (canApplePay). The glyph is the Apple logo (Safari). */
1249
1296
  .buy-applepay {
@@ -1328,6 +1375,7 @@ var VioProductDetail = class extends LitElement {
1328
1375
  }
1329
1376
  connectedCallback() {
1330
1377
  super.connectedCallback();
1378
+ void this.loadVippsButton();
1331
1379
  document.addEventListener("vio:product-click", this.boundProductClick);
1332
1380
  }
1333
1381
  disconnectedCallback() {
@@ -1342,6 +1390,8 @@ var VioProductDetail = class extends LitElement {
1342
1390
  setTimeout(() => {
1343
1391
  if (!this.open) {
1344
1392
  this.product = null;
1393
+ this.sellerName = null;
1394
+ this.sellerSettled = false;
1345
1395
  this.selectedOptions = {};
1346
1396
  this.quantity = 1;
1347
1397
  this.activeImageIndex = 0;
@@ -1358,6 +1408,8 @@ var VioProductDetail = class extends LitElement {
1358
1408
  const seq = ++this.fetchSeq;
1359
1409
  this.isLoading = true;
1360
1410
  this.fetchError = "";
1411
+ this.sellerName = null;
1412
+ this.sellerSettled = false;
1361
1413
  let products = null;
1362
1414
  let lastError = null;
1363
1415
  for (let attempt = 0; attempt < 4; attempt++) {
@@ -1386,6 +1438,7 @@ var VioProductDetail = class extends LitElement {
1386
1438
  }
1387
1439
  this.optMapCache = /* @__PURE__ */ new WeakMap();
1388
1440
  this.product = products?.[0] ?? null;
1441
+ if (this.product) void this.loadSeller(productIdNum, seq);
1389
1442
  if (this.product) {
1390
1443
  try {
1391
1444
  const price = Number(
@@ -1452,9 +1505,7 @@ var VioProductDetail = class extends LitElement {
1452
1505
  }
1453
1506
  /** Backend names arrive as e.g. "Apple Pay" — compare letters only. */
1454
1507
  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)));
1508
+ return isMethodEnabled(this.availableMethods, ...names);
1458
1509
  }
1459
1510
  /* eslint-disable @typescript-eslint/no-explicit-any */
1460
1511
  /**
@@ -1773,10 +1824,60 @@ var VioProductDetail = class extends LitElement {
1773
1824
  }
1774
1825
  }
1775
1826
  /** Buy with Vipps — opens the checkout with Vipps preselected. */
1827
+ /**
1828
+ * Buy now with an embedded checkout (Kustom, Qliro, Walley).
1829
+ *
1830
+ * Those three have no express path of their own, so the product page showed
1831
+ * a "Kjøp nå" for Apple Pay, Stripe, Klarna and Vipps and nothing at all for
1832
+ * them — the same structural gap the cart footer had. Opens the checkout
1833
+ * without preselecting: with a single method the checkout selects it itself,
1834
+ * with several the shopper picks.
1835
+ *
1836
+ * Like every other "Kjøp nå" here, it adds to the cart first — so a cart
1837
+ * that already had items buys those too. Isolating a single-product purchase
1838
+ * needs a checkout that is not bound to the sponsor's cart, which is a
1839
+ * separate piece of work and deliberately not done here.
1840
+ */
1841
+ buyNow() {
1842
+ if (!this.product || this.availableQuantity <= 0 || this.adding) return;
1843
+ this.addCurrentToCart();
1844
+ this.close();
1845
+ this.dispatchEvent(
1846
+ new CustomEvent("vio:checkout-open", {
1847
+ bubbles: true,
1848
+ composed: true,
1849
+ detail: { sponsorId: this.sponsorId, express: false }
1850
+ })
1851
+ );
1852
+ }
1853
+ /**
1854
+ * Buy with Vipps — Express: the app opens with address and delivery
1855
+ * already there, nothing to fill in here (Vipps' own recommendation,
1856
+ * 2026-09-29). When this cart cannot have Express, or Vipps cannot be
1857
+ * started, the checkout opens with Vipps preselected, as before.
1858
+ */
1859
+ /** Vipps' own button when its script answers; our orange one otherwise. */
1860
+ async loadVippsButton() {
1861
+ this.vippsOfficial = await ensureVippsButton();
1862
+ }
1776
1863
  buyWithVipps() {
1777
1864
  if (!this.product || this.availableQuantity <= 0 || this.adding) return;
1778
1865
  this.addCurrentToCart();
1779
1866
  this.close();
1867
+ void this.startVippsExpressOrCheckout();
1868
+ }
1869
+ async startVippsExpressOrCheckout() {
1870
+ let fallback = true;
1871
+ try {
1872
+ if (!await Vio.checkout.getVippsExpressEnabled(this.sponsorId)) throw new Error("express off");
1873
+ const res = await Vio.checkout.startVippsExpress(this.sponsorId);
1874
+ fallback = !res?.payment_url;
1875
+ } catch (err) {
1876
+ if (typeof console !== "undefined") {
1877
+ console.warn("[VioProductDetail] Vipps Express not started:", err);
1878
+ }
1879
+ }
1880
+ if (!fallback) return;
1780
1881
  this.dispatchEvent(
1781
1882
  new CustomEvent("vio:checkout-open", {
1782
1883
  bubbles: true,
@@ -1818,13 +1919,29 @@ var VioProductDetail = class extends LitElement {
1818
1919
  void this.applePayHandle.show();
1819
1920
  this.close();
1820
1921
  }
1922
+ /** The seller's Company name; the brand when there is none; nothing while asking. */
1923
+ headerName() {
1924
+ if (this.sellerName) return this.sellerName;
1925
+ return this.sellerSettled ? this.product?.brand ?? "" : "";
1926
+ }
1927
+ async loadSeller(productId, seq) {
1928
+ let name = null;
1929
+ try {
1930
+ name = await Vio.commerceFor(this.sponsorId).channel.product.getSellerCompany(productId);
1931
+ } catch {
1932
+ name = null;
1933
+ }
1934
+ if (seq !== this.fetchSeq) return;
1935
+ this.sellerName = name;
1936
+ this.sellerSettled = true;
1937
+ }
1821
1938
  render() {
1822
1939
  return html`
1823
1940
  <div class="backdrop" @click=${this.close}></div>
1824
1941
  <div class="modal" role="dialog" aria-label="Produkt">
1825
1942
  <div class="handle" @click=${this.close} aria-hidden="true"></div>
1826
1943
  <div class="topbar">
1827
- <span class="topbar-brand">${this.product?.brand ?? ""}</span>
1944
+ <span class="topbar-brand">${this.headerName()}</span>
1828
1945
  <button class="close" @click=${this.close} aria-label="Lukk">×</button>
1829
1946
  </div>
1830
1947
 
@@ -1922,6 +2039,17 @@ var VioProductDetail = class extends LitElement {
1922
2039
  </button>
1923
2040
  </div>
1924
2041
 
2042
+ ${this.availableQuantity > 0 && this.methodEnabled(...NO_EXPRESS_BUTTON_METHODS) ? html`
2043
+ <button
2044
+ class="buy-now"
2045
+ @click=${this.buyNow}
2046
+ ?disabled=${this.adding}
2047
+ aria-label="Kjøp nå"
2048
+ >
2049
+ Kjøp nå
2050
+ </button>
2051
+ ` : ""}
2052
+
1925
2053
  ${this.availableQuantity > 0 && this.applePayOk && this.methodEnabled("apple-pay", "applepay") ? html`
1926
2054
  <button
1927
2055
  class="buy-applepay"
@@ -1965,17 +2093,30 @@ var VioProductDetail = class extends LitElement {
1965
2093
  </button>
1966
2094
  ` : ""}
1967
2095
 
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
- ` : ""}
2096
+ ${this.availableQuantity > 0 && this.methodEnabled("vipps") ? this.vippsOfficial ? html`
2097
+ <vipps-mobilepay-button
2098
+ class="buy-vipps-official"
2099
+ brand="vipps"
2100
+ language="no"
2101
+ verb="buy"
2102
+ variant="primary"
2103
+ rounded="true"
2104
+ stretched="true"
2105
+ role="button"
2106
+ aria-label="Kjøp nå med Vipps"
2107
+ @click=${this.buyWithVipps}
2108
+ ></vipps-mobilepay-button>
2109
+ ` : html`
2110
+ <button
2111
+ class="buy-vipps"
2112
+ @click=${this.buyWithVipps}
2113
+ ?disabled=${this.adding}
2114
+ aria-label="Kjøp nå med Vipps"
2115
+ >
2116
+ <span>Kjøp nå med</span>
2117
+ <span class="vipps-badge">vipps</span>
2118
+ </button>
2119
+ ` : ""}
1979
2120
 
1980
2121
  ${p.description ? html`
1981
2122
  <div>
@@ -2040,6 +2181,12 @@ __decorateClass([
2040
2181
  __decorateClass([
2041
2182
  state()
2042
2183
  ], VioProductDetail.prototype, "product", 2);
2184
+ __decorateClass([
2185
+ state()
2186
+ ], VioProductDetail.prototype, "sellerName", 2);
2187
+ __decorateClass([
2188
+ state()
2189
+ ], VioProductDetail.prototype, "sellerSettled", 2);
2043
2190
  __decorateClass([
2044
2191
  state()
2045
2192
  ], VioProductDetail.prototype, "isLoading", 2);
@@ -2064,6 +2211,9 @@ __decorateClass([
2064
2211
  __decorateClass([
2065
2212
  state()
2066
2213
  ], VioProductDetail.prototype, "availableMethods", 2);
2214
+ __decorateClass([
2215
+ state()
2216
+ ], VioProductDetail.prototype, "vippsOfficial", 2);
2067
2217
  function normalizeValues(raw) {
2068
2218
  if (Array.isArray(raw)) {
2069
2219
  return raw.map((v) => String(v).trim()).filter((v) => v.length > 0);
@@ -2087,6 +2237,9 @@ var VioCart = class extends LitElement {
2087
2237
  this.itemCount = 0;
2088
2238
  this.applePayAvailable = false;
2089
2239
  this.availableMethods = null;
2240
+ this.vippsOfficial = false;
2241
+ /** Whether the method lookup has been attempted — see boundOnCartChange. */
2242
+ this.paymentMethodsAttempted = false;
2090
2243
  this.confirmedOrder = null;
2091
2244
  /** Pre-resolved Apple Pay handle; `show()` fires the sheet synchronously. */
2092
2245
  this.applePayHandle = null;
@@ -2098,7 +2251,8 @@ var VioCart = class extends LitElement {
2098
2251
  const detail = e.detail;
2099
2252
  this.carts = detail.cartsBySponsor;
2100
2253
  this.itemCount = detail.itemCount;
2101
- if (this.availableMethods === null) {
2254
+ if (!this.paymentMethodsAttempted) {
2255
+ this.paymentMethodsAttempted = true;
2102
2256
  void this.loadAvailablePaymentMethods();
2103
2257
  }
2104
2258
  };
@@ -2273,6 +2427,23 @@ var VioCart = class extends LitElement {
2273
2427
  font-size: 14px;
2274
2428
  }
2275
2429
 
2430
+ /* Always present: the express buttons below are shortcuts, not the only
2431
+ way out of the cart. Without this a channel offering only an embedded
2432
+ checkout (Kustom, Qliro, Walley) had no button at all. */
2433
+ .checkout-btn {
2434
+ width: 100%;
2435
+ padding: 14px;
2436
+ margin-bottom: var(--vio-space-sm, 8px);
2437
+ background: var(--vio-color-text, #0a0a0a);
2438
+ color: var(--vio-color-text-on-primary, #fff);
2439
+ border: none;
2440
+ border-radius: var(--vio-radius-md, 4px);
2441
+ font-family: var(--vio-font-sans, inherit);
2442
+ font-size: var(--vio-size-sm, 14px);
2443
+ font-weight: 600;
2444
+ cursor: pointer;
2445
+ }
2446
+ .checkout-btn:hover { opacity: 0.9; }
2276
2447
  .footer {
2277
2448
  padding: var(--vio-space-lg, 24px);
2278
2449
  border-top: 1px solid var(--vio-color-border, #e5e5e5);
@@ -2347,6 +2518,7 @@ var VioCart = class extends LitElement {
2347
2518
  transition: background 0.15s;
2348
2519
  }
2349
2520
  .vipps-btn:hover { background: #ec4f1c; }
2521
+ .vipps-btn-official { display: block; width: 100%; margin-bottom: 10px; cursor: pointer; }
2350
2522
  .vipps-btn .vipps-logo {
2351
2523
  font-size: 20px;
2352
2524
  font-weight: 800;
@@ -2445,6 +2617,7 @@ var VioCart = class extends LitElement {
2445
2617
  }
2446
2618
  connectedCallback() {
2447
2619
  super.connectedCallback();
2620
+ void this.loadVippsButton();
2448
2621
  this.carts = Vio.cart.getAllCarts();
2449
2622
  this.itemCount = Vio.cart.itemCount;
2450
2623
  Vio.cart.addEventListener("change", this.boundOnCartChange);
@@ -2494,10 +2667,8 @@ var VioCart = class extends LitElement {
2494
2667
  }
2495
2668
  }
2496
2669
  /** 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));
2670
+ methodEnabled(...names) {
2671
+ return isMethodEnabled(this.availableMethods, ...names);
2501
2672
  }
2502
2673
  updated(changed) {
2503
2674
  if (changed.has("open") && this.open) {
@@ -2585,15 +2756,59 @@ var VioCart = class extends LitElement {
2585
2756
  );
2586
2757
  }
2587
2758
  /** Vipps — opens the checkout with Vipps preselected. */
2759
+ /**
2760
+ * Open the checkout without preselecting a method.
2761
+ *
2762
+ * The footer used to be express-only — Apple Pay, Stripe, Klarna, Vipps —
2763
+ * so a channel offering just an embedded checkout (Kustom, Qliro, Walley)
2764
+ * had NO button at all and the cart was a dead end. Even with an express
2765
+ * method present the others were unreachable from here.
2766
+ */
2767
+ onCheckout() {
2768
+ if (this.itemCount === 0) return;
2769
+ const firstSponsorId = [...this.carts.keys()][0];
2770
+ if (firstSponsorId === void 0) return;
2771
+ this.dispatchEvent(
2772
+ new CustomEvent("vio:checkout-open", {
2773
+ bubbles: true,
2774
+ composed: true,
2775
+ detail: { sponsorId: firstSponsorId, express: false }
2776
+ })
2777
+ );
2778
+ }
2779
+ /**
2780
+ * Vipps — Express: the app collects address and delivery, so the cart
2781
+ * goes straight to Vipps. When this cart cannot have Express (several
2782
+ * suppliers, digital goods, no rate) or Vipps cannot be started, the
2783
+ * checkout opens with Vipps preselected, as before.
2784
+ */
2588
2785
  onVipps() {
2589
2786
  if (this.itemCount === 0) return;
2590
2787
  const firstSponsorId = [...this.carts.keys()][0];
2591
2788
  if (firstSponsorId === void 0) return;
2789
+ void this.startVippsExpressOrCheckout(firstSponsorId);
2790
+ }
2791
+ /** Vipps' own button when its script answers; our orange one otherwise. */
2792
+ async loadVippsButton() {
2793
+ this.vippsOfficial = await ensureVippsButton();
2794
+ }
2795
+ async startVippsExpressOrCheckout(sponsorId) {
2796
+ let fallback = true;
2797
+ try {
2798
+ if (!await Vio.checkout.getVippsExpressEnabled(sponsorId)) throw new Error("express off");
2799
+ const res = await Vio.checkout.startVippsExpress(sponsorId);
2800
+ fallback = !res?.payment_url;
2801
+ } catch (err) {
2802
+ if (typeof console !== "undefined") {
2803
+ console.warn("[VioCart] Vipps Express not started:", err);
2804
+ }
2805
+ }
2806
+ if (!fallback) return;
2592
2807
  this.dispatchEvent(
2593
2808
  new CustomEvent("vio:checkout-open", {
2594
2809
  bubbles: true,
2595
2810
  composed: true,
2596
- detail: { sponsorId: firstSponsorId, paymentMethod: "vipps", express: false }
2811
+ detail: { sponsorId, paymentMethod: "vipps", express: false }
2597
2812
  })
2598
2813
  );
2599
2814
  }
@@ -2705,6 +2920,13 @@ var VioCart = class extends LitElement {
2705
2920
  ${formatPrice(this.totalAcrossSponsors, this.primaryCurrency)}
2706
2921
  </span>
2707
2922
  </div>
2923
+ <button
2924
+ class="checkout-btn"
2925
+ @click=${this.onCheckout}
2926
+ aria-label="Til kassen"
2927
+ >
2928
+ Til kassen
2929
+ </button>
2708
2930
  ${this.methodEnabled("apple-pay") && this.applePayAvailable ? html`
2709
2931
  <button
2710
2932
  class="applepay-btn"
@@ -2738,15 +2960,28 @@ var VioCart = class extends LitElement {
2738
2960
  />
2739
2961
  </button>
2740
2962
  ` : ""}
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
- ` : ""}
2963
+ ${this.methodEnabled("vipps") ? this.vippsOfficial ? html`
2964
+ <vipps-mobilepay-button
2965
+ class="vipps-btn-official"
2966
+ brand="vipps"
2967
+ language="no"
2968
+ verb="pay"
2969
+ variant="primary"
2970
+ rounded="true"
2971
+ stretched="true"
2972
+ role="button"
2973
+ aria-label="Betal med Vipps"
2974
+ @click=${this.onVipps}
2975
+ ></vipps-mobilepay-button>
2976
+ ` : html`
2977
+ <button
2978
+ class="vipps-btn"
2979
+ @click=${this.onVipps}
2980
+ aria-label="Betal med Vipps"
2981
+ >
2982
+ <span class="vipps-logo">vipps</span>
2983
+ </button>
2984
+ ` : ""}
2750
2985
  </div>
2751
2986
  `}
2752
2987
  </aside>
@@ -2803,6 +3038,9 @@ __decorateClass([
2803
3038
  __decorateClass([
2804
3039
  state()
2805
3040
  ], VioCart.prototype, "availableMethods", 2);
3041
+ __decorateClass([
3042
+ state()
3043
+ ], VioCart.prototype, "vippsOfficial", 2);
2806
3044
  __decorateClass([
2807
3045
  state()
2808
3046
  ], VioCart.prototype, "confirmedOrder", 2);
@@ -2822,9 +3060,13 @@ var _VioCheckout = class _VioCheckout extends LitElement {
2822
3060
  this.applePayInProgress = false;
2823
3061
  this.klarnaAvailable = false;
2824
3062
  this.orderConfirmed = false;
3063
+ this.providerReceipt = null;
2825
3064
  this.confirmedMethod = null;
2826
3065
  this.paymentError = null;
2827
3066
  this.paymentNotice = null;
3067
+ this.verifyingReturn = null;
3068
+ /** The checkout whose confirmation is on screen — see fillOrderNumber. */
3069
+ this.confirmedCheckoutId = null;
2828
3070
  this.confirmedOrder = null;
2829
3071
  /** Active Klarna Payments widget handle + the subtotal it was mounted for. */
2830
3072
  this.klarnaHandle = null;
@@ -2837,12 +3079,70 @@ var _VioCheckout = class _VioCheckout extends LitElement {
2837
3079
  this.selectedShipping = "";
2838
3080
  this.availableShippingsList = [];
2839
3081
  this.availableMethods = null;
3082
+ this.vippsOfficial = false;
2840
3083
  this.kustomMounting = false;
3084
+ this.kustomSyncing = false;
3085
+ this.kustomShipping = null;
3086
+ this.kustomTotal = null;
3087
+ this.kustomNotice = null;
3088
+ /** The cart the mounted Kustom order describes — see kustomCartKey. */
3089
+ this.kustomMountedKey = null;
2841
3090
  this.kustomMountedOrderId = null;
3091
+ /** The checkout session (see embedSession) each embedded widget's order belongs to. */
3092
+ this.kustomMountedFor = null;
3093
+ this.qliroMountedFor = null;
3094
+ this.walleyMountedFor = null;
3095
+ this.nexiMountedFor = null;
2842
3096
  this.qliroMounting = false;
2843
3097
  this.qliroMountedOrderId = null;
2844
3098
  this.walleyMounting = false;
2845
3099
  this.walleyMountedOrderId = null;
3100
+ this.nexiMounting = false;
3101
+ this.nexiMountedOrderId = null;
3102
+ this.nexiShippingNotice = null;
3103
+ this.nexiShipping = null;
3104
+ this.nexiPicking = false;
3105
+ /** The Nexi payment to resume on the next mount — set only by the ?paymentId= return. */
3106
+ this.nexiResumePaymentId = null;
3107
+ this.adyenMounting = false;
3108
+ this.adyenRequested = false;
3109
+ this.adyenMounted = false;
3110
+ this.adyenStale = false;
3111
+ this.adyenSettled = false;
3112
+ /** The checkout session (see embedSession) and the purchase the mounted photo is of. */
3113
+ this.adyenMountedFor = null;
3114
+ this.adyenMountedKey = null;
3115
+ this.stripeMode = null;
3116
+ this.stripeRequested = false;
3117
+ this.stripeMounting = false;
3118
+ this.stripeMounted = false;
3119
+ this.stripeStale = false;
3120
+ this.stripeSettled = false;
3121
+ this.stripePaying = false;
3122
+ this.stripeReady = false;
3123
+ this.stripeMountedFor = null;
3124
+ this.stripeMountedKey = null;
3125
+ /** The checkout the mounted PaymentIntent pays for. */
3126
+ this.stripeCheckoutId = null;
3127
+ this.stripeElement = null;
3128
+ this.qliroShipping = null;
3129
+ /**
3130
+ * Set while this page load is showing the outcome of an embedded checkout's
3131
+ * confirmation redirect (Kustom, Qliro, Walley). While set, nothing may start
3132
+ * a new payment.
3133
+ *
3134
+ * The return handler selects the method so the panel has something to show,
3135
+ * and only THEN asks the provider whether the order was paid. Selecting is a
3136
+ * state change; the render it schedules runs `updated()` → `mount*IfNeeded()`
3137
+ * long before the provider answers, and in a fresh page load there is no
3138
+ * mounted order to stop it — so every return created a new backend checkout
3139
+ * and a new provider order for a cart that had just been paid. Not a race
3140
+ * that needs a slow network: a microtask always beats a round trip.
3141
+ * Reproduced in `vio-checkout.return.dom.test.ts` before this existed.
3142
+ *
3143
+ * Cleared when the checkout closes: reopening it is a fresh checkout.
3144
+ */
3145
+ this.returningFrom = null;
2846
3146
  this.stripeLoading = false;
2847
3147
  this.vippsLoading = false;
2848
3148
  this.form = {
@@ -2855,10 +3155,31 @@ var _VioCheckout = class _VioCheckout extends LitElement {
2855
3155
  };
2856
3156
  /** In-flight guard for loadAvailablePaymentMethods. */
2857
3157
  this.loadingPaymentMethods = false;
3158
+ this.paymentMethodsResolved = false;
3159
+ /** Guards `autoSelectSoleMethod` to ONE attempt per opening — see there. */
3160
+ this.autoSelectAttempted = false;
2858
3161
  /** In-flight + attempted guards for loadAvailableShippings (change events
2859
3162
  * fire per keystroke — without these, typing an address hammers the API). */
2860
3163
  this.loadingShippings = false;
2861
3164
  this.shippingsAttempted = false;
3165
+ this.shippingBlocked = false;
3166
+ this.shippingsReadyFor = null;
3167
+ this.boundOnQliroEvent = (e) => {
3168
+ const event = e.detail;
3169
+ if (!event) return;
3170
+ if (event.type === "shipping-price-changed" && typeof event.price === "number") {
3171
+ this.qliroShipping = { ...this.qliroShipping ?? {}, price: event.price };
3172
+ } else if (event.type === "shipping-changed") {
3173
+ const shipping = event.shipping ?? {};
3174
+ const ref = shipping.method ?? shipping.merchantReference ?? shipping.MerchantReference;
3175
+ const price = typeof shipping.price === "number" ? shipping.price : this.availableShippingsList.find((o) => o.id === String(ref))?.priceMajor;
3176
+ this.qliroShipping = {
3177
+ ...this.qliroShipping ?? {},
3178
+ ...ref != null ? { ref: String(ref) } : {},
3179
+ ...typeof price === "number" ? { price } : {}
3180
+ };
3181
+ }
3182
+ };
2862
3183
  this.boundOnCheckoutChange = (e) => {
2863
3184
  const detail = e.detail;
2864
3185
  const prevSponsorId = this.checkoutState?.sponsorId;
@@ -2866,7 +3187,8 @@ var _VioCheckout = class _VioCheckout extends LitElement {
2866
3187
  this.checkoutState = detail.state;
2867
3188
  this.items = Vio.checkout.items;
2868
3189
  if (this.checkoutState) {
2869
- if (sponsorChanged || this.availableMethods === null) {
3190
+ if (sponsorChanged || !this.paymentMethodsResolved) {
3191
+ if (sponsorChanged) this.paymentMethodsResolved = false;
2870
3192
  void this.refreshApplePay();
2871
3193
  void this.refreshKlarna();
2872
3194
  void this.loadAvailablePaymentMethods();
@@ -2878,13 +3200,28 @@ var _VioCheckout = class _VioCheckout extends LitElement {
2878
3200
  this.applePayAvailable = false;
2879
3201
  this.klarnaAvailable = false;
2880
3202
  this.orderConfirmed = false;
3203
+ this.providerReceipt = null;
2881
3204
  this.confirmedMethod = null;
2882
3205
  this.confirmedOrder = null;
2883
3206
  this.paymentError = null;
2884
3207
  this.paymentNotice = null;
3208
+ this.verifyingReturn = null;
3209
+ this.confirmedCheckoutId = null;
2885
3210
  this.express = false;
2886
3211
  this.availableMethods = null;
3212
+ this.paymentMethodsResolved = false;
3213
+ this.autoSelectAttempted = false;
3214
+ this.returningFrom = null;
3215
+ this.shippingBlocked = false;
2887
3216
  this.unmountKlarna();
3217
+ this.unmountKustom();
3218
+ this.unmountQliro();
3219
+ this.unmountWalley();
3220
+ this.unmountNexi();
3221
+ this.unmountAdyen();
3222
+ this.adyenSettled = false;
3223
+ this.unmountStripe();
3224
+ this.stripeSettled = false;
2888
3225
  }
2889
3226
  };
2890
3227
  this.boundOnPaymentComplete = (e) => {
@@ -2966,10 +3303,12 @@ var _VioCheckout = class _VioCheckout extends LitElement {
2966
3303
  :host([open]) .modal.as-side { transform: translateX(0); }
2967
3304
 
2968
3305
  /* 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. */
3306
+ product detail and express drawer) — never a full-width overlay. The
3307
+ confirmation stays in that panel too: a centered card jumped out of the
3308
+ panel the shopper was in and looked like the phone layout (Angelo,
3309
+ 2026-09-16). */
2971
3310
  @media (min-width: 601px) {
2972
- .modal:not(.as-confirm) {
3311
+ .modal {
2973
3312
  inset: 0 0 0 auto;
2974
3313
  width: min(440px, 100%);
2975
3314
  max-width: 440px;
@@ -2977,7 +3316,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
2977
3316
  border-radius: 0;
2978
3317
  box-shadow: -8px 0 24px rgba(0, 0, 0, 0.1);
2979
3318
  }
2980
- :host([open]) .modal:not(.as-confirm) { transform: translateX(0); }
3319
+ :host([open]) .modal { transform: translateX(0); }
2981
3320
  }
2982
3321
  @media (max-width: 600px) {
2983
3322
  /* On phones, keep it a bottom sheet for reachability. */
@@ -2992,37 +3331,12 @@ var _VioCheckout = class _VioCheckout extends LitElement {
2992
3331
  :host([open]) .modal.as-side { transform: translateY(0); }
2993
3332
  }
2994
3333
 
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
- }
3334
+ /* Phones: the confirmation is a compact bottom sheet, as tall as it needs. */
3014
3335
  @media (max-width: 600px) {
3015
3336
  .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;
3337
+ height: auto;
3338
+ max-height: 88vh;
3024
3339
  }
3025
- :host([open]) .modal.as-confirm { transform: translateY(0); }
3026
3340
  }
3027
3341
 
3028
3342
  .handle { display: none; }
@@ -3139,6 +3453,61 @@ var _VioCheckout = class _VioCheckout extends LitElement {
3139
3453
  grid-template-columns: 1fr 1fr;
3140
3454
  gap: var(--vio-space-md, 16px);
3141
3455
  }
3456
+ /* Shown until we know which methods the channel offers. Rendering every
3457
+ button meanwhile would let a shopper click one that is not enabled. */
3458
+ .payment-loading {
3459
+ display: flex;
3460
+ align-items: center;
3461
+ gap: 10px;
3462
+ padding: 20px 0;
3463
+ font-size: 13px;
3464
+ color: var(--vio-color-text-secondary, #666);
3465
+ }
3466
+ .payment-spinner {
3467
+ width: 14px;
3468
+ height: 14px;
3469
+ border: 2px solid var(--vio-color-border, #e5e5e5);
3470
+ border-top-color: var(--vio-color-text-secondary, #666);
3471
+ border-radius: 50%;
3472
+ animation: vio-payment-spin 0.7s linear infinite;
3473
+ }
3474
+ @keyframes vio-payment-spin {
3475
+ to { transform: rotate(360deg); }
3476
+ }
3477
+ @media (prefers-reduced-motion: reduce) {
3478
+ .payment-spinner { animation: none; }
3479
+ }
3480
+ /* Back from a payment page: only this until we know. */
3481
+ .verifying {
3482
+ display: flex;
3483
+ flex-direction: column;
3484
+ align-items: center;
3485
+ text-align: center;
3486
+ gap: 10px;
3487
+ padding: 56px 16px;
3488
+ }
3489
+ .verifying-spinner {
3490
+ width: 28px;
3491
+ height: 28px;
3492
+ border: 3px solid var(--vio-color-border, #e5e5e5);
3493
+ border-top-color: var(--vio-color-accent, #c14a3b);
3494
+ border-radius: 50%;
3495
+ animation: vio-payment-spin 0.8s linear infinite;
3496
+ }
3497
+ .verifying-text {
3498
+ margin: 6px 0 0;
3499
+ font-size: 15px;
3500
+ font-weight: 600;
3501
+ color: var(--vio-color-text, #1a1a1a);
3502
+ }
3503
+ .verifying-hint {
3504
+ margin: 0;
3505
+ font-size: 13px;
3506
+ color: var(--vio-color-text-secondary, #666);
3507
+ }
3508
+ @media (prefers-reduced-motion: reduce) {
3509
+ .verifying-spinner { animation: none; }
3510
+ }
3142
3511
  .payment-btn {
3143
3512
  padding: 20px;
3144
3513
  background: #fff;
@@ -3175,13 +3544,6 @@ var _VioCheckout = class _VioCheckout extends LitElement {
3175
3544
  .payment-btn[aria-pressed='true'] {
3176
3545
  border-color: var(--vio-color-accent, #c14a3b);
3177
3546
  }
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
3547
  /* Klarna Payments — inline widget panel, shown once Klarna is selected. */
3186
3548
  .klarna-panel {
3187
3549
  grid-column: 1 / -1;
@@ -3223,6 +3585,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
3223
3585
  }
3224
3586
  /* Shipping selector (express) — Standard / Express options. */
3225
3587
  .ship-select { display: flex; flex-direction: column; gap: 8px; }
3588
+ .nexi-ship { margin-bottom: 12px; }
3226
3589
  .ship-opt {
3227
3590
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
3228
3591
  padding: 12px 14px; border: 1px solid var(--vio-color-border, #ddd);
@@ -3272,37 +3635,86 @@ var _VioCheckout = class _VioCheckout extends LitElement {
3272
3635
 
3273
3636
  /* Order confirmation — compact, shown inside the drawer. */
3274
3637
  .confirmation {
3275
- text-align: center;
3276
- padding: 24px 24px 28px;
3638
+ padding: 8px 0 28px;
3277
3639
  display: flex;
3278
3640
  flex-direction: column;
3641
+ gap: 20px;
3642
+ }
3643
+ .confirm-head {
3644
+ display: flex;
3279
3645
  align-items: center;
3280
- gap: 14px;
3646
+ gap: 16px;
3647
+ }
3648
+ .confirm-details {
3649
+ margin: 0;
3650
+ display: flex;
3651
+ flex-direction: column;
3652
+ border: 1px solid var(--vio-color-border, #eee);
3653
+ border-radius: var(--vio-radius-lg, 8px);
3654
+ }
3655
+ .confirm-details > div {
3656
+ display: flex;
3657
+ justify-content: space-between;
3658
+ gap: 16px;
3659
+ padding: 10px 14px;
3660
+ font-size: 13px;
3661
+ }
3662
+ .confirm-details > div + div { border-top: 1px solid var(--vio-color-border, #eee); }
3663
+ .confirm-details dt { color: var(--vio-color-text-secondary, #666); white-space: nowrap; }
3664
+ .confirm-details dd.ref { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-weight: 500; font-size: 12px; }
3665
+ .confirm-details dd {
3666
+ margin: 0;
3667
+ font-weight: 600;
3668
+ text-align: right;
3669
+ font-variant-numeric: tabular-nums;
3670
+ word-break: break-all;
3671
+ }
3672
+ .confirm-label {
3673
+ font-size: 11px;
3674
+ text-transform: uppercase;
3675
+ letter-spacing: var(--vio-tracking-label, 0.1em);
3676
+ color: var(--vio-color-text-secondary, #666);
3677
+ }
3678
+ .confirm-line {
3679
+ display: grid;
3680
+ grid-template-columns: 56px 1fr auto;
3681
+ gap: 12px;
3682
+ align-items: center;
3683
+ }
3684
+ .confirm-thumb {
3685
+ width: 56px;
3686
+ height: 56px;
3687
+ object-fit: cover;
3688
+ background: var(--vio-color-surface-muted, #f2f2f2);
3689
+ display: block;
3281
3690
  }
3691
+ .confirm-line-name { display: flex; flex-direction: column; gap: 2px; font-size: 14px; }
3692
+ .confirm-line-name small { font-size: 11px; color: var(--vio-color-text-secondary, #666); }
3693
+ .confirm-line-price { font-size: 14px; font-weight: 600; }
3282
3694
  .confirm-check {
3283
- width: 64px;
3284
- height: 64px;
3695
+ flex-shrink: 0;
3696
+ width: 48px;
3697
+ height: 48px;
3285
3698
  border-radius: 50%;
3286
3699
  background: #2e7d32;
3287
3700
  color: #fff;
3288
3701
  display: flex;
3289
3702
  align-items: center;
3290
3703
  justify-content: center;
3291
- font-size: 32px;
3704
+ font-size: 24px;
3292
3705
  line-height: 1;
3293
3706
  }
3294
3707
  .confirm-title {
3295
3708
  font-family: var(--vio-font-serif, Georgia, serif);
3296
- font-size: 28px;
3709
+ font-size: 24px;
3297
3710
  font-weight: 400;
3298
3711
  margin: 0;
3299
3712
  }
3300
3713
  .confirm-text {
3301
3714
  font-size: 14px;
3302
3715
  color: var(--vio-color-text-secondary, #666);
3303
- max-width: 360px;
3304
- line-height: 1.6;
3305
- margin: 0;
3716
+ line-height: 1.5;
3717
+ margin: 4px 0 0;
3306
3718
  }
3307
3719
  .confirm-order-id {
3308
3720
  font-size: 13px;
@@ -3313,17 +3725,13 @@ var _VioCheckout = class _VioCheckout extends LitElement {
3313
3725
  font-variant-numeric: tabular-nums;
3314
3726
  }
3315
3727
  .confirm-summary {
3316
- width: 100%;
3317
- max-width: 360px;
3318
- border-top: 1px solid var(--vio-color-border, #eee);
3319
- padding-top: 14px;
3320
3728
  display: flex;
3321
3729
  flex-direction: column;
3322
- gap: 8px;
3323
- text-align: left;
3730
+ gap: 12px;
3324
3731
  font-size: 13px;
3325
3732
  }
3326
3733
  .confirm-close {
3734
+ width: 100%;
3327
3735
  border: none;
3328
3736
  background: var(--vio-color-text, #0a0a0a);
3329
3737
  color: var(--vio-color-text-on-primary, #fff);
@@ -3388,11 +3796,13 @@ var _VioCheckout = class _VioCheckout extends LitElement {
3388
3796
  }
3389
3797
  connectedCallback() {
3390
3798
  super.connectedCallback();
3799
+ void this.loadVippsButton();
3391
3800
  this.checkoutState = Vio.checkout.currentState;
3392
3801
  this.items = Vio.checkout.items;
3393
3802
  Vio.checkout.addEventListener("change", this.boundOnCheckoutChange);
3394
3803
  Vio.checkout.addEventListener("payment-complete", this.boundOnPaymentComplete);
3395
3804
  Vio.checkout.addEventListener("payment-error", this.boundOnPaymentError);
3805
+ Vio.checkout.addEventListener("qliro-event", this.boundOnQliroEvent);
3396
3806
  if (this.checkoutState) {
3397
3807
  void this.refreshApplePay();
3398
3808
  void this.refreshKlarna();
@@ -3400,6 +3810,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
3400
3810
  }
3401
3811
  void Vio.checkout.resumeKlarnaReturn();
3402
3812
  void this.checkReturnPaymentStatus();
3813
+ this.requestUpdate();
3403
3814
  }
3404
3815
  static {
3405
3816
  /**
@@ -3435,6 +3846,18 @@ var _VioCheckout = class _VioCheckout extends LitElement {
3435
3846
  "unpaid"
3436
3847
  ]);
3437
3848
  }
3849
+ static {
3850
+ /**
3851
+ * Shown instead of the payment step when the cart's products cannot ship
3852
+ * together. Angelo, 2026-09-16: block the purchase rather than sell it
3853
+ * without shipping, or with a rate that does not fit every product.
3854
+ */
3855
+ this.SHIPPING_BLOCKED_MESSAGE = "Produktene i handlekurven kan ikke sendes sammen. Fjern ett av dem for \xE5 fullf\xF8re kj\xF8pet.";
3856
+ }
3857
+ static {
3858
+ /** The payment may or may not have gone through, and we could not find out. */
3859
+ this.RETURN_UNVERIFIED_MESSAGE = "Vi kunne ikke bekrefte betalingen. Handlekurven er uendret \u2014 pr\xF8v igjen, eller kontakt support hvis du ble belastet.";
3860
+ }
3438
3861
  static {
3439
3862
  /** Waits between GetCheckout attempts — PSP webhooks (Vipps especially) can
3440
3863
  * land seconds after the shopper is redirected back, so a single immediate
@@ -3457,6 +3880,10 @@ var _VioCheckout = class _VioCheckout extends LitElement {
3457
3880
  * anything else (including an unrecognized state) is treated as failed —
3458
3881
  * fail-closed by default rather than guessing.
3459
3882
  */
3883
+ /** Vipps' own button when its script answers; our orange one otherwise. */
3884
+ async loadVippsButton() {
3885
+ this.vippsOfficial = await ensureVippsButton();
3886
+ }
3460
3887
  async pollVippsStatus(checkoutId, sponsorId) {
3461
3888
  let sawStatus = false;
3462
3889
  for (let attempt = 0; attempt < _VioCheckout.VIPPS_STATUS_MAX_ATTEMPTS; attempt++) {
@@ -3486,12 +3913,12 @@ var _VioCheckout = class _VioCheckout extends LitElement {
3486
3913
  this.confirmedOrder = {
3487
3914
  items: [],
3488
3915
  currency: getGlobalCurrency(),
3489
- total: 0,
3490
- orderId: checkoutId || void 0
3916
+ total: 0
3491
3917
  };
3492
3918
  this.confirmedMethod = vioMethod;
3493
3919
  this.orderConfirmed = true;
3494
3920
  this.open = true;
3921
+ void this.fillOrderNumber(checkoutId, sponsorId);
3495
3922
  this.dispatchEvent(
3496
3923
  new CustomEvent("vio:payment-success", {
3497
3924
  bubbles: true,
@@ -3506,7 +3933,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
3506
3933
  this.paymentNotice = "Betalingen behandles fortsatt. Du f\xE5r ordrebekreftelse p\xE5 e-post n\xE5r den er gjennomf\xF8rt. Handlekurven er uendret.";
3507
3934
  this.open = true;
3508
3935
  } else {
3509
- this.paymentError = "Vi kunne ikke bekrefte betalingen. Handlekurven er uendret \u2014 pr\xF8v igjen, eller kontakt support hvis du ble belastet.";
3936
+ this.paymentError = _VioCheckout.RETURN_UNVERIFIED_MESSAGE;
3510
3937
  this.open = true;
3511
3938
  }
3512
3939
  }
@@ -3518,9 +3945,33 @@ var _VioCheckout = class _VioCheckout extends LitElement {
3518
3945
  const vioMethod = urlParams.get("vio_method") || "stripe";
3519
3946
  const sponsorId = Number(urlParams.get("vio_sponsor") || 0);
3520
3947
  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) {
3948
+ const nexiPaymentId = nexiReturnPaymentId();
3949
+ const nexiPending = readNexiPending();
3950
+ if (nexiPaymentId && nexiPending && nexiPending.paymentId === nexiPaymentId) {
3951
+ this.nexiResumePaymentId = nexiPaymentId;
3952
+ this.cleanReturnQueryParams(["paymentId"]);
3953
+ if (!this.checkoutState && nexiPending.sponsorId) {
3954
+ try {
3955
+ Vio.checkout.open(nexiPending.sponsorId);
3956
+ } catch {
3957
+ }
3958
+ }
3959
+ try {
3960
+ Vio.checkout.selectPaymentMethod("nexi");
3961
+ } catch {
3962
+ }
3963
+ this.open = true;
3964
+ return;
3965
+ }
3966
+ const kustomReturn = kustomReturnOutcome(urlParams);
3967
+ if (kustomReturn?.kind === "rejected") {
3968
+ this.cleanReturnQueryParams([...KUSTOM_RETURN_QUERY_KEYS]);
3969
+ this.kustomNotice = "Bestillingen ble endret. Betalingen ble ikke gjennomf\xF8rt \u2014 pr\xF8v igjen.";
3970
+ return;
3971
+ }
3972
+ if (kustomReturn?.kind === "confirmation") {
3973
+ const kustomOrderId = kustomReturn.orderId;
3974
+ this.returningFrom = "kustom";
3524
3975
  if (!this.checkoutState) {
3525
3976
  const spIdToUse = sponsorId || [...Vio.cart.getAllCarts().keys()][0] || 0;
3526
3977
  if (spIdToUse) {
@@ -3535,49 +3986,35 @@ var _VioCheckout = class _VioCheckout extends LitElement {
3535
3986
  } catch {
3536
3987
  }
3537
3988
  this.open = true;
3538
- this.paymentNotice = "Bekrefter betalingen\u2026";
3989
+ this.verifyingReturn = "Bekrefter betalingen\u2026";
3990
+ this.cleanReturnQueryParams([...KUSTOM_RETURN_QUERY_KEYS]);
3539
3991
  try {
3540
3992
  const order = await Vio.checkout.getKustomOrder(
3541
3993
  kustomOrderId,
3542
3994
  this.checkoutState?.sponsorId
3543
3995
  );
3544
- this.paymentNotice = null;
3545
- if (order?.html_snippet) {
3546
- this.kustomMountedOrderId = order.order_id;
3996
+ this.verifyingReturn = null;
3997
+ if (order?.status === "checkout_complete" && order.html_snippet) {
3998
+ const spId = this.checkoutState?.sponsorId ?? 0;
3999
+ this.showProviderReceipt("kustom", spId, order);
3547
4000
  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
- }
4001
+ const container = this.lightContainer("vio-kustom-checkout-container", "vio-kustom");
4002
+ Vio.checkout.renderKustomSnippet(container, order.html_snippet);
4003
+ } else {
4004
+ this.paymentError = _VioCheckout.RETURN_UNVERIFIED_MESSAGE;
3570
4005
  }
3571
4006
  } catch (err) {
3572
- this.paymentNotice = null;
4007
+ this.verifyingReturn = null;
4008
+ this.paymentError = _VioCheckout.RETURN_UNVERIFIED_MESSAGE;
3573
4009
  if (typeof console !== "undefined") {
3574
4010
  console.warn("[VioCheckout] Kustom confirmation read failed:", err);
3575
4011
  }
3576
4012
  }
3577
- this.cleanReturnQueryParams(["order_id", "payment_processor"]);
3578
4013
  return;
3579
4014
  }
4015
+ if (!vioPayment && !checkoutId) return;
3580
4016
  if (urlParams.get("payment_processor") === "QLIRO" && checkoutId) {
4017
+ this.returningFrom = "qliro";
3581
4018
  if (!this.checkoutState) {
3582
4019
  const spIdToUse = sponsorId || [...Vio.cart.getAllCarts().keys()][0] || 0;
3583
4020
  if (spIdToUse) {
@@ -3591,40 +4028,38 @@ var _VioCheckout = class _VioCheckout extends LitElement {
3591
4028
  Vio.checkout.selectPaymentMethod("qliro");
3592
4029
  } catch {
3593
4030
  }
4031
+ this.autoSelectAttempted = true;
3594
4032
  this.open = true;
3595
- this.paymentNotice = "Bekrefter betalingen\u2026";
4033
+ this.verifyingReturn = "Bekrefter betalingen\u2026";
3596
4034
  try {
3597
4035
  const order = await Vio.checkout.getQliroOrder(
3598
4036
  checkoutId,
3599
4037
  this.checkoutState?.sponsorId
3600
4038
  );
3601
- this.paymentNotice = null;
3602
- if (order?.html_snippet) {
4039
+ this.verifyingReturn = null;
4040
+ if (order?.status === "Completed") {
4041
+ const spId = this.checkoutState?.sponsorId ?? sponsorId ?? 0;
4042
+ const result = { order: { orderId: order.order_id }, chargedTotal: order.total_price };
4043
+ this.qliroMountedOrderId = order.order_id;
4044
+ if (order.html_snippet) {
4045
+ this.showProviderReceipt("qliro", spId, result);
4046
+ await this.updateComplete;
4047
+ const container = this.lightContainer("vio-qliro-checkout-container", "vio-qliro");
4048
+ Vio.checkout.renderKustomSnippet(container, order.html_snippet);
4049
+ } else {
4050
+ this.confirmOrder("qliro", spId, result);
4051
+ }
4052
+ } else if (order?.html_snippet) {
3603
4053
  this.qliroMountedOrderId = order.order_id;
3604
4054
  await this.updateComplete;
3605
4055
  const container = this.lightContainer("vio-qliro-checkout-container", "vio-qliro");
3606
4056
  if (container) {
3607
4057
  Vio.checkout.renderKustomSnippet(container, order.html_snippet);
3608
4058
  }
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
4059
  }
3626
4060
  } catch (err) {
3627
- this.paymentNotice = null;
4061
+ this.verifyingReturn = null;
4062
+ this.paymentError = _VioCheckout.RETURN_UNVERIFIED_MESSAGE;
3628
4063
  if (typeof console !== "undefined") {
3629
4064
  console.warn("[VioCheckout] Qliro confirmation read failed:", err);
3630
4065
  }
@@ -3633,6 +4068,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
3633
4068
  return;
3634
4069
  }
3635
4070
  if (urlParams.get("payment_processor") === "WALLEY" && checkoutId) {
4071
+ this.returningFrom = "walley";
3636
4072
  if (!this.checkoutState) {
3637
4073
  const spIdToUse = sponsorId || [...Vio.cart.getAllCarts().keys()][0] || 0;
3638
4074
  if (spIdToUse) {
@@ -3647,13 +4083,13 @@ var _VioCheckout = class _VioCheckout extends LitElement {
3647
4083
  } catch {
3648
4084
  }
3649
4085
  this.open = true;
3650
- this.paymentNotice = "Bekrefter betalingen\u2026";
4086
+ this.verifyingReturn = "Bekrefter betalingen\u2026";
3651
4087
  try {
3652
4088
  const order = await Vio.checkout.getWalleyOrder(
3653
4089
  checkoutId,
3654
4090
  this.checkoutState?.sponsorId
3655
4091
  );
3656
- this.paymentNotice = null;
4092
+ this.verifyingReturn = null;
3657
4093
  if (order?.html_snippet) {
3658
4094
  this.walleyMountedOrderId = order.order_id;
3659
4095
  await this.updateComplete;
@@ -3662,24 +4098,14 @@ var _VioCheckout = class _VioCheckout extends LitElement {
3662
4098
  Vio.checkout.renderKustomSnippet(container, order.html_snippet);
3663
4099
  }
3664
4100
  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
- })
3671
- );
3672
- const spId = this.checkoutState?.sponsorId;
3673
- if (spId) {
3674
- try {
3675
- Vio.cart.clearSponsorCart(spId);
3676
- } catch {
3677
- }
3678
- }
4101
+ this.showProviderReceipt("walley", this.checkoutState?.sponsorId ?? 0, {
4102
+ orderId: order.order_id
4103
+ });
3679
4104
  }
3680
4105
  }
3681
4106
  } catch (err) {
3682
- this.paymentNotice = null;
4107
+ this.verifyingReturn = null;
4108
+ this.paymentError = _VioCheckout.RETURN_UNVERIFIED_MESSAGE;
3683
4109
  if (typeof console !== "undefined") {
3684
4110
  console.warn("[VioCheckout] Walley confirmation read failed:", err);
3685
4111
  }
@@ -3691,40 +4117,94 @@ var _VioCheckout = class _VioCheckout extends LitElement {
3691
4117
  this.cleanReturnQueryParams(["vio_payment", "vio_method", "vio_sponsor", "checkout_id"]);
3692
4118
  return;
3693
4119
  }
3694
- if (vioMethod === "vipps" && checkoutId) {
4120
+ if (vioMethod === ADYEN_RETURN_METHOD && checkoutId) {
4121
+ const adyenReturn = adyenReturnParams(window.location.search);
4122
+ this.cleanReturnQueryParams(ADYEN_RETURN_QUERY_KEYS);
4123
+ if (!adyenReturn) return;
4124
+ this.returningFrom = "adyen";
4125
+ this.adyenSettled = true;
4126
+ this.autoSelectAttempted = true;
3695
4127
  if (!this.checkoutState) {
3696
4128
  const spIdToUse = sponsorId || [...Vio.cart.getAllCarts().keys()][0] || 0;
3697
4129
  if (spIdToUse) {
3698
4130
  try {
3699
4131
  Vio.checkout.open(spIdToUse);
3700
- Vio.checkout.selectPaymentMethod("vipps");
4132
+ Vio.checkout.selectPaymentMethod("adyen");
3701
4133
  } catch {
3702
4134
  }
3703
4135
  }
3704
4136
  }
3705
4137
  this.open = true;
3706
- this.paymentNotice = "Bekrefter betalingen med Vipps\u2026";
3707
- const { outcome, sawStatus } = await this.pollVippsStatus(checkoutId, sponsorId);
3708
- this.paymentNotice = null;
3709
- this.applyReturnOutcome(outcome, sawStatus, vioMethod, sponsorId, checkoutId);
3710
- this.cleanReturnQueryParams(["vio_payment", "vio_method", "vio_sponsor", "checkout_id"]);
3711
- } else if (vioPayment === "success") {
3712
- let outcome = "pending";
4138
+ this.verifyingReturn = "Bekrefter betalingen\u2026";
4139
+ let payment = null;
3713
4140
  let sawStatus = false;
3714
- if (checkoutId) {
3715
- this.open = true;
3716
- this.paymentNotice = "Bekrefter betalingen\u2026";
3717
- for (const delayMs of _VioCheckout.RETURN_VERIFY_DELAYS_MS) {
3718
- if (delayMs > 0) await new Promise((r) => setTimeout(r, delayMs));
3719
- try {
3720
- const checkout = await Vio.checkout.getCheckout(checkoutId, sponsorId || void 0);
4141
+ try {
4142
+ payment = await Vio.checkout.getAdyenPayment(
4143
+ checkoutId,
4144
+ { redirectResult: adyenReturn.redirectResult },
4145
+ sponsorId || void 0
4146
+ );
4147
+ sawStatus = !!payment;
4148
+ } catch (err) {
4149
+ if (typeof console !== "undefined") {
4150
+ console.warn("[VioCheckout] Adyen return could not be verified:", err);
4151
+ }
4152
+ }
4153
+ this.verifyingReturn = null;
4154
+ const outcome = sawStatus ? adyenReturnOutcome(payment) : "pending";
4155
+ this.applyReturnOutcome(outcome, sawStatus, "adyen", sponsorId, checkoutId);
4156
+ if (outcome === "paid" && this.confirmedOrder) {
4157
+ this.confirmedOrder = {
4158
+ ...this.confirmedOrder,
4159
+ currency: payment?.purchase_currency ?? this.confirmedOrder.currency,
4160
+ total: payment?.total_price ?? this.confirmedOrder.total
4161
+ };
4162
+ this.addAdyenPaymentDetails(payment);
4163
+ }
4164
+ if (outcome === "failed") {
4165
+ this.returningFrom = null;
4166
+ this.adyenSettled = false;
4167
+ }
4168
+ return;
4169
+ }
4170
+ if (vioMethod === "vipps" && checkoutId) {
4171
+ if (!this.checkoutState) {
4172
+ const spIdToUse = sponsorId || [...Vio.cart.getAllCarts().keys()][0] || 0;
4173
+ if (spIdToUse) {
4174
+ try {
4175
+ Vio.checkout.open(spIdToUse);
4176
+ Vio.checkout.selectPaymentMethod("vipps");
4177
+ } catch {
4178
+ }
4179
+ }
4180
+ }
4181
+ this.open = true;
4182
+ this.verifyingReturn = "Bekrefter betalingen med Vipps\u2026";
4183
+ const { outcome, sawStatus } = await this.pollVippsStatus(checkoutId, sponsorId);
4184
+ this.verifyingReturn = null;
4185
+ this.applyReturnOutcome(outcome, sawStatus, vioMethod, sponsorId, checkoutId);
4186
+ this.cleanReturnQueryParams(["vio_payment", "vio_method", "vio_sponsor", "checkout_id"]);
4187
+ } else if (vioPayment === "success") {
4188
+ let outcome = "pending";
4189
+ let sawStatus = false;
4190
+ if (checkoutId) {
4191
+ this.open = true;
4192
+ this.verifyingReturn = "Bekrefter betalingen\u2026";
4193
+ for (const delayMs of _VioCheckout.RETURN_VERIFY_DELAYS_MS) {
4194
+ if (delayMs > 0) await new Promise((r) => setTimeout(r, delayMs));
4195
+ try {
4196
+ const checkout = await Vio.checkout.getCheckout(checkoutId, sponsorId || void 0);
3721
4197
  const status = String(checkout?.status ?? "").toLowerCase();
3722
4198
  sawStatus = true;
3723
4199
  if (_VioCheckout.FAILED_STATUSES.has(status)) {
3724
4200
  outcome = "failed";
3725
4201
  break;
3726
4202
  }
3727
- if (_VioCheckout.PAID_STATUSES.has(status) || Boolean(checkout?.origin_payment_id)) {
4203
+ if (_VioCheckout.PAID_STATUSES.has(status) || // A Stripe checkout carries `origin_payment_id` from the
4204
+ // moment the link or the intent is CREATED (shopcart), so for
4205
+ // Stripe that pointer says nothing about who paid. Only the
4206
+ // status does, and it is the webhook that sets it.
4207
+ vioMethod !== "stripe" && Boolean(checkout?.origin_payment_id)) {
3728
4208
  outcome = "paid";
3729
4209
  break;
3730
4210
  }
@@ -3734,7 +4214,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
3734
4214
  }
3735
4215
  }
3736
4216
  }
3737
- this.paymentNotice = null;
4217
+ this.verifyingReturn = null;
3738
4218
  }
3739
4219
  this.applyReturnOutcome(outcome, sawStatus, vioMethod, sponsorId, checkoutId);
3740
4220
  this.cleanReturnQueryParams(["vio_payment", "vio_method", "vio_sponsor", "checkout_id"]);
@@ -3773,17 +4253,44 @@ var _VioCheckout = class _VioCheckout extends LitElement {
3773
4253
  Vio.checkout.removeEventListener("change", this.boundOnCheckoutChange);
3774
4254
  Vio.checkout.removeEventListener("payment-complete", this.boundOnPaymentComplete);
3775
4255
  Vio.checkout.removeEventListener("payment-error", this.boundOnPaymentError);
4256
+ Vio.checkout.removeEventListener("qliro-event", this.boundOnQliroEvent);
3776
4257
  this.unmountKlarna();
4258
+ this.unmountNexi();
4259
+ this.unmountAdyen();
3777
4260
  super.disconnectedCallback();
3778
4261
  }
3779
4262
  updated(changed) {
3780
4263
  if (changed.has("open") && this.open && this.items.length > 0 && this.availableShippingsList.length === 0) {
3781
4264
  void this.loadAvailableShippings();
3782
4265
  }
4266
+ if (changed.has("open") && this.open) {
4267
+ this.autoSelectSoleMethod();
4268
+ }
4269
+ if ((changed.has("klarnaAvailable") && this.klarnaAvailable || changed.has("applePayAvailable") && this.applePayAvailable) && this.paymentMethodsResolved && !this.checkoutState?.paymentMethod) {
4270
+ this.autoSelectAttempted = false;
4271
+ this.autoSelectSoleMethod();
4272
+ }
4273
+ if (changed.has("shippingBlocked") && this.shippingBlocked) {
4274
+ this.unmountKlarna();
4275
+ this.unmountKustom();
4276
+ this.unmountQliro();
4277
+ this.unmountWalley();
4278
+ this.unmountNexi();
4279
+ this.unmountAdyen();
4280
+ this.unmountStripe();
4281
+ this.paymentError = null;
4282
+ }
4283
+ if (!this.isConnected) return;
3783
4284
  void this.mountKlarnaIfNeeded();
3784
4285
  void this.mountKustomIfNeeded();
4286
+ this.syncKustomIfStale();
3785
4287
  void this.mountQliroIfNeeded();
3786
4288
  void this.mountWalleyIfNeeded();
4289
+ void this.mountNexiIfNeeded();
4290
+ this.dropStaleAdyen();
4291
+ void this.mountAdyenIfNeeded();
4292
+ this.dropStaleStripe();
4293
+ void this.mountStripeIfNeeded();
3787
4294
  }
3788
4295
  close() {
3789
4296
  this.open = false;
@@ -3796,17 +4303,30 @@ var _VioCheckout = class _VioCheckout extends LitElement {
3796
4303
  void this.loadAvailableShippings();
3797
4304
  }
3798
4305
  }
3799
- /** Fetch the real per-supplier shippings and preselect + persist the first. */
4306
+ /**
4307
+ * Fetch the real per-supplier shippings and preselect + persist the first.
4308
+ *
4309
+ * Also decides whether the cart can ship at all (`shippingBlocked`), and
4310
+ * records which opening the answer belongs to (`shippingsReadyFor`).
4311
+ */
3800
4312
  async loadAvailableShippings() {
3801
4313
  const spId = this.checkoutState?.sponsorId;
3802
4314
  if (!spId) return;
3803
4315
  if (this.items.length === 0) return;
3804
4316
  if (this.loadingShippings) return;
4317
+ const session = this.embedSession();
3805
4318
  this.loadingShippings = true;
3806
4319
  this.shippingsAttempted = true;
3807
4320
  try {
3808
4321
  const shippings = await Vio.checkout.fetchAvailableShippings(spId);
3809
- if (Array.isArray(shippings) && shippings.length > 0) {
4322
+ if (session !== this.embedSession()) return;
4323
+ if (!Array.isArray(shippings) || shippings.length === 0) {
4324
+ if (Vio.checkout.lastShippingsResolved) {
4325
+ this.availableShippingsList = [];
4326
+ this.shippingBlocked = this.someLineShipsOnItsOwn();
4327
+ }
4328
+ } else {
4329
+ this.shippingBlocked = false;
3810
4330
  this.availableShippingsList = shippings;
3811
4331
  if (!this.selectedShipping || !shippings.some((s) => s.id === this.selectedShipping)) {
3812
4332
  const firstOpt = shippings[0];
@@ -3825,8 +4345,24 @@ var _VioCheckout = class _VioCheckout extends LitElement {
3825
4345
  }
3826
4346
  } finally {
3827
4347
  this.loadingShippings = false;
4348
+ if (session === this.embedSession()) {
4349
+ this.shippingsReadyFor = session;
4350
+ } else if (this.checkoutState && this.items.length > 0) {
4351
+ void this.loadAvailableShippings();
4352
+ }
3828
4353
  }
3829
4354
  }
4355
+ /**
4356
+ * Whether some product in the cart has shipping rates of its own. With no
4357
+ * rate shared by all of them, that means they cannot ship TOGETHER. A cart
4358
+ * where no line has rates is left alone: digital products have none, and
4359
+ * shopcart refuses the physical case on its side.
4360
+ */
4361
+ someLineShipsOnItsOwn() {
4362
+ return this.items.some(
4363
+ (i) => Array.isArray(i.availableShippings) && i.availableShippings.length > 0
4364
+ );
4365
+ }
3830
4366
  /** Whether all mandatory Leveringsadresse fields are filled. */
3831
4367
  get isAddressValid() {
3832
4368
  const f = this.form;
@@ -3848,11 +4384,13 @@ var _VioCheckout = class _VioCheckout extends LitElement {
3848
4384
  if (this.loadingPaymentMethods) return;
3849
4385
  this.loadingPaymentMethods = true;
3850
4386
  try {
4387
+ const spId = this.checkoutState.sponsorId;
3851
4388
  const methods = await Vio.checkout.getAvailablePaymentMethods(
3852
- this.checkoutState.sponsorId
4389
+ spId
3853
4390
  );
3854
4391
  if (Array.isArray(methods)) {
3855
4392
  this.availableMethods = methods.map((m) => m.name);
4393
+ this.stripeMode = await Vio.checkout.getStripeMode(spId);
3856
4394
  } else {
3857
4395
  this.availableMethods = null;
3858
4396
  }
@@ -3863,13 +4401,70 @@ var _VioCheckout = class _VioCheckout extends LitElement {
3863
4401
  this.availableMethods = null;
3864
4402
  } finally {
3865
4403
  this.loadingPaymentMethods = false;
4404
+ this.paymentMethodsResolved = true;
4405
+ this.autoSelectSoleMethod();
3866
4406
  }
3867
4407
  }
4408
+ /**
4409
+ * With exactly one payment method there is no choice to make, so making the
4410
+ * shopper click it is a step that can only have one outcome. Select it —
4411
+ * and since the checkout asks for the method first (2026-09-22), this is
4412
+ * what skips that step: the form, or the provider's widget, comes straight
4413
+ * up.
4414
+ *
4415
+ * Deliberately narrow:
4416
+ * - never overrides a method the shopper already picked;
4417
+ * - only for methods usable RIGHT NOW — Apple Pay and Klarna depend on the
4418
+ * browser and on their script loading, and auto-selecting an unusable one
4419
+ * would strand the shopper on a panel that cannot pay (see `updated`: a
4420
+ * late "available" gets one more try);
4421
+ * - selecting never pays: Stripe and Vipps still wait for their own button.
4422
+ */
4423
+ autoSelectSoleMethod() {
4424
+ if (!this.open) return;
4425
+ if (!this.paymentMethodsResolved) return;
4426
+ if (this.autoSelectAttempted) return;
4427
+ this.autoSelectAttempted = true;
4428
+ if (this.checkoutState?.paymentMethod) return;
4429
+ const methods = this.offeredMethods();
4430
+ if (!Array.isArray(methods) || methods.length !== 1) return;
4431
+ const id = String(methods[0] ?? "").toLowerCase().replace(/[^a-z]/g, "");
4432
+ const usable = {
4433
+ kustom: "kustom",
4434
+ qliro: "qliro",
4435
+ walley: "walley",
4436
+ nexi: "nexi",
4437
+ // Selected, not mounted: Adyen waits for the form (see adyenRequested).
4438
+ adyen: "adyen",
4439
+ vipps: "vipps",
4440
+ stripe: "stripe",
4441
+ klarna: this.klarnaAvailable ? "klarna" : void 0,
4442
+ applepay: this.applePayAvailable ? "apple-pay" : void 0
4443
+ };
4444
+ const only = usable[id];
4445
+ if (!only) return;
4446
+ Vio.checkout.selectPaymentMethod(only);
4447
+ }
3868
4448
  /** 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));
4449
+ methodEnabled(...names) {
4450
+ return isMethodEnabled(this.availableMethods, ...names);
4451
+ }
4452
+ /**
4453
+ * Stripe's Payment Element shows the wallets of the account that charges —
4454
+ * Apple Pay and Google Pay among them. Offering our own Apple Pay button
4455
+ * next to it is the same wallet twice, on the same screen, paying through
4456
+ * the same account: when Stripe pays on our page, Stripe owns the wallets
4457
+ * (Angelo, 2026-09-24). The cart's express button is another surface and
4458
+ * keeps its own Apple Pay — the Element does not exist there.
4459
+ */
4460
+ stripeOwnsWallets() {
4461
+ return this.stripeMode === "native" && this.methodEnabled("stripe");
4462
+ }
4463
+ /** The methods the shopper actually sees (see stripeOwnsWallets). */
4464
+ offeredMethods() {
4465
+ const methods = this.availableMethods;
4466
+ if (!Array.isArray(methods) || !this.stripeOwnsWallets()) return methods;
4467
+ return methods.filter((m) => !isMethodEnabled([m], "apple-pay"));
3873
4468
  }
3874
4469
  async refreshApplePay() {
3875
4470
  Vio.checkout.clearApplePayCache();
@@ -3886,10 +4481,12 @@ var _VioCheckout = class _VioCheckout extends LitElement {
3886
4481
  async mountKlarnaIfNeeded() {
3887
4482
  if (!this.klarnaAvailable || !this.open || !this.checkoutState) return;
3888
4483
  if (this.checkoutState.paymentMethod !== "klarna" && !this.express) return;
4484
+ if (!this.express && (!this.isAddressValid || !this.hasSelectedShipping)) return;
3889
4485
  if (this.klarnaMounting) return;
3890
4486
  if (this.items.length > 0 && this.availableShippingsList.length === 0 && !this.shippingsAttempted) {
3891
4487
  await this.loadAvailableShippings();
3892
4488
  }
4489
+ if (this.shippingBlocked) return;
3893
4490
  const amount = this.checkoutState.subtotal;
3894
4491
  if (this.klarnaHandle && this.klarnaMountedAmount === amount && this.klarnaMountedShipping === this.selectedShipping) {
3895
4492
  return;
@@ -3933,7 +4530,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
3933
4530
  }
3934
4531
  this.klarnaMountedAmount = null;
3935
4532
  this.klarnaMountedShipping = null;
3936
- this.klarnaCategories = [];
4533
+ if (this.klarnaCategories.length > 0) this.klarnaCategories = [];
3937
4534
  this.klarnaSelectedCat = "";
3938
4535
  this.klarnaAuthorizing = false;
3939
4536
  }
@@ -3982,7 +4579,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
3982
4579
  }
3983
4580
  /** Authorize the selected Klarna method → backend creates the order. */
3984
4581
  async onKlarnaAuthorize() {
3985
- if (!this.klarnaHandle || this.klarnaAuthorizing) return;
4582
+ if (!this.klarnaHandle || this.klarnaAuthorizing || this.shippingBlocked) return;
3986
4583
  this.paymentError = null;
3987
4584
  this.paymentNotice = null;
3988
4585
  this.klarnaAuthorizing = true;
@@ -4000,6 +4597,33 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4000
4597
  this.form = { ...this.form, [field]: value };
4001
4598
  Vio.checkout.setAddress(this.form);
4002
4599
  }
4600
+ /** After choosing a method that needs our form, bring the form into view. */
4601
+ revealAddressForm() {
4602
+ void this.updateComplete.then(() => {
4603
+ const form = this.renderRoot?.querySelector(".address-section");
4604
+ if (!form) return;
4605
+ const bar = this.renderRoot?.querySelector(".topbar");
4606
+ form.style.scrollMarginTop = `${(bar?.offsetHeight ?? 0) + 8}px`;
4607
+ form.scrollIntoView?.({ behavior: "smooth", block: "start" });
4608
+ });
4609
+ }
4610
+ /**
4611
+ * Vipps from the checkout: Express when the channel allows it, so the
4612
+ * shopper picks the delivery inside the app — our rate on the cart is the
4613
+ * one preselected there — and never silently keeps the default our form
4614
+ * chose for them. When the cart cannot have Express (several suppliers)
4615
+ * the backend says so without creating a payment, and the plain payment
4616
+ * follows: profile shared, the rate already on the cart.
4617
+ */
4618
+ async startVippsFromCheckout() {
4619
+ const sponsorId = this.checkoutState?.sponsorId;
4620
+ const express = await Vio.checkout.getVippsExpressEnabled(sponsorId);
4621
+ if (express) {
4622
+ const res = await Vio.checkout.startVippsPayment(sponsorId, this.form, { express: true });
4623
+ if (res?.payment_url) return;
4624
+ }
4625
+ await Vio.checkout.startVippsPayment(sponsorId, this.form);
4626
+ }
4003
4627
  async onApplePay() {
4004
4628
  if (this.applePayInProgress) return;
4005
4629
  this.applePayInProgress = true;
@@ -4014,6 +4638,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4014
4638
  }
4015
4639
  }
4016
4640
  async onPay(method) {
4641
+ if (this.shippingBlocked) return;
4017
4642
  this.paymentError = null;
4018
4643
  this.paymentNotice = null;
4019
4644
  if (method === "apple-pay") {
@@ -4032,24 +4657,35 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4032
4657
  Vio.checkout.selectPaymentMethod("walley");
4033
4658
  return;
4034
4659
  }
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.";
4660
+ if (method === "nexi") {
4661
+ Vio.checkout.selectPaymentMethod("nexi");
4662
+ return;
4663
+ }
4664
+ if (method !== "vipps") {
4665
+ const missing = !this.isAddressValid ? "Vennligst fyll ut alle feltene i leveringsadresse." : !this.hasSelectedShipping ? "Vennligst velg en fraktmetode." : null;
4666
+ if (missing) {
4667
+ if (this.checkoutState?.paymentMethod !== method) {
4668
+ Vio.checkout.selectPaymentMethod(method);
4669
+ this.revealAddressForm();
4670
+ return;
4671
+ }
4672
+ this.paymentError = missing;
4048
4673
  return;
4049
4674
  }
4050
4675
  }
4051
4676
  Vio.checkout.selectPaymentMethod(method);
4677
+ if (method === "adyen") {
4678
+ this.adyenRequested = true;
4679
+ return;
4680
+ }
4052
4681
  if (method === "stripe") {
4682
+ if (this.stripeMode === null) {
4683
+ this.stripeMode = await Vio.checkout.getStripeMode(this.checkoutState?.sponsorId);
4684
+ }
4685
+ if (this.stripeMode === "native") {
4686
+ this.stripeRequested = true;
4687
+ return;
4688
+ }
4053
4689
  const selectedOpt = this.shippingList.find((s) => s.id === this.selectedShipping);
4054
4690
  if (selectedOpt?.supplierId) {
4055
4691
  try {
@@ -4072,7 +4708,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4072
4708
  }
4073
4709
  if (method === "vipps") {
4074
4710
  this.vippsLoading = true;
4075
- void Vio.checkout.startVippsPayment(this.checkoutState?.sponsorId, this.form).catch((err) => {
4711
+ void this.startVippsFromCheckout().catch((err) => {
4076
4712
  this.paymentError = `Kunne ikke starte Vipps-betaling: ${err instanceof Error ? err.message : String(err)}`;
4077
4713
  }).finally(() => {
4078
4714
  this.vippsLoading = false;
@@ -4087,6 +4723,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4087
4723
  onCompleteOrder() {
4088
4724
  const s = this.checkoutState;
4089
4725
  if (!s || !s.paymentMethod) return;
4726
+ if (this.shippingBlocked) return;
4090
4727
  if (s.paymentMethod !== "vipps" && !this.isAddressValid) {
4091
4728
  this.paymentError = "Vennligst fyll ut alle feltene i leveringsadresse.";
4092
4729
  return;
@@ -4109,11 +4746,17 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4109
4746
  */
4110
4747
  confirmOrder(method, sponsorId, result) {
4111
4748
  const r = result ?? {};
4749
+ const shippingPrice = this.shippingMajor();
4750
+ const email = String(this.form.email ?? "").trim();
4751
+ const checkoutId = this.checkoutState?.checkoutId;
4752
+ const providerOrderId = r.order?.orderId != null ? String(r.order.orderId) : "";
4112
4753
  this.confirmedOrder = {
4113
4754
  items: [...this.items],
4114
4755
  currency: this.checkoutState?.currency ?? "",
4115
4756
  total: typeof r.chargedTotal === "number" ? r.chargedTotal : this.checkoutState?.subtotal ?? 0,
4116
- orderId: r.order?.orderId
4757
+ ...providerOrderId ? { providerRef: { label: `Ordre hos ${this.methodLabel(method)}`, value: providerOrderId } } : {},
4758
+ ...shippingPrice > 0 ? { shipping: { name: this.summaryShippingName() || "Frakt", price: shippingPrice } } : {},
4759
+ ...email ? { email } : {}
4117
4760
  };
4118
4761
  this.confirmedMethod = method;
4119
4762
  this.orderConfirmed = true;
@@ -4126,6 +4769,33 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4126
4769
  })
4127
4770
  );
4128
4771
  Vio.cart.clearSponsorCart(sponsorId);
4772
+ void this.fillOrderNumber(checkoutId, sponsorId);
4773
+ }
4774
+ static {
4775
+ /** Waits between asks for the Vio order number: the order is often born a
4776
+ * moment after the payment (Vipps and Stripe through their webhook). */
4777
+ this.ORDER_NUMBER_DELAYS_MS = [0, 1500, 3e3, 5e3, 8e3];
4778
+ }
4779
+ /**
4780
+ * Puts the Vio order number (#4497) on the confirmation once the order
4781
+ * exists. Until then the line is simply not there — never the checkout's
4782
+ * uuid in its place, which is what it used to print. Stops if the shopper
4783
+ * closes the drawer or another confirmation takes its place.
4784
+ */
4785
+ async fillOrderNumber(checkoutId, sponsorId) {
4786
+ this.confirmedCheckoutId = checkoutId || null;
4787
+ if (!checkoutId) return;
4788
+ const stillShowing = () => this.orderConfirmed && this.confirmedCheckoutId === checkoutId;
4789
+ for (const delayMs of _VioCheckout.ORDER_NUMBER_DELAYS_MS) {
4790
+ if (delayMs > 0) await new Promise((r) => setTimeout(r, delayMs));
4791
+ if (!stillShowing()) return;
4792
+ const n = await Vio.checkout.getOrderNumber(checkoutId, sponsorId || void 0);
4793
+ if (!stillShowing()) return;
4794
+ if (n && this.confirmedOrder) {
4795
+ this.confirmedOrder = { ...this.confirmedOrder, orderId: String(n) };
4796
+ return;
4797
+ }
4798
+ }
4129
4799
  }
4130
4800
  methodLabel(method) {
4131
4801
  switch (method) {
@@ -4139,6 +4809,11 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4139
4809
  return "Qliro";
4140
4810
  case "walley":
4141
4811
  return "Walley";
4812
+ case "nexi":
4813
+ return "Nexi";
4814
+ // Adyen is not a name shoppers know; what they pick from is inside it.
4815
+ case "adyen":
4816
+ return "Kort, Vipps og flere";
4142
4817
  case "vipps":
4143
4818
  return "Vipps";
4144
4819
  case "stripe":
@@ -4151,7 +4826,8 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4151
4826
  }
4152
4827
  /** True for methods that complete via a separate sheet/widget, not the CTA. */
4153
4828
  isExpressMethod(method) {
4154
- return method === "apple-pay" || method === "klarna" || method === "kustom" || method === "qliro" || method === "walley";
4829
+ return method === "apple-pay" || method === "klarna" || isEmbeddedMethod(method ?? "") || // Adyen's Drop-in brings its own Pay button.
4830
+ isFormFirstWidgetMethod(method ?? "");
4155
4831
  }
4156
4832
  orderTotal() {
4157
4833
  const s = this.checkoutState;
@@ -4162,11 +4838,33 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4162
4838
  * express always has one; the normal flow has one once the backend
4163
4839
  * shippings loaded (the choice IS persisted on the cart and charged). */
4164
4840
  shippingMajor() {
4841
+ if (this.checkoutState?.paymentMethod === "qliro" && typeof this.qliroShipping?.price === "number") {
4842
+ return this.qliroShipping.price;
4843
+ }
4844
+ if (this.checkoutState?.paymentMethod === "nexi" && typeof this.nexiShipping?.shipping_price === "number") {
4845
+ return this.nexiShipping.shipping_price;
4846
+ }
4847
+ if (this.checkoutState?.paymentMethod === "kustom" && typeof this.kustomShipping?.price === "number") {
4848
+ return this.kustomShipping.price;
4849
+ }
4165
4850
  if (!this.express && this.availableShippingsList.length === 0) return 0;
4166
4851
  const o = this.shippingOption;
4167
4852
  if (!o) return 0;
4168
4853
  return o.priceMajor ?? o.price / 100;
4169
4854
  }
4855
+ /** The rate the summary names — Qliro's own pick while Qliro is the method. */
4856
+ summaryShippingName() {
4857
+ if (this.checkoutState?.paymentMethod === "qliro" && this.qliroShipping) {
4858
+ const ref = this.qliroShipping.ref;
4859
+ const picked = ref ? this.availableShippingsList.find((o2) => o2.id === ref) : void 0;
4860
+ return picked ? picked.method || picked.name || "" : "";
4861
+ }
4862
+ if (this.checkoutState?.paymentMethod === "nexi" && this.nexiShipping) {
4863
+ return this.nexiShipping.shipping_name ?? "";
4864
+ }
4865
+ const o = this.shippingOption;
4866
+ return o ? o.method || o.name || "" : "";
4867
+ }
4170
4868
  /** Total to charge — items + shipping (express). */
4171
4869
  payTotalLabel() {
4172
4870
  const s = this.checkoutState;
@@ -4188,59 +4886,206 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4188
4886
  </div>
4189
4887
 
4190
4888
  <div class="body">
4191
- ${this.orderConfirmed ? this.renderConfirmation() : this.express ? this.renderKlarnaExpress() : this.renderCheckoutBody()}
4889
+ ${this.providerReceipt ? this.renderProviderReceipt() : this.orderConfirmed ? this.renderConfirmation() : this.verifyingReturn ? this.renderVerifying() : this.express ? this.renderKlarnaExpress() : this.renderCheckoutBody()}
4192
4890
  </div>
4193
4891
  </div>
4194
4892
  `;
4195
4893
  }
4894
+ /**
4895
+ * The provider's receipt on its own. It must not depend on the cart: the
4896
+ * payment step is rendered from the cart, and clearing the cart after a
4897
+ * purchase is what took Qliro's receipt away on 2026-09-08 and brought back
4898
+ * "how do you want to pay". The provider's container lives in the light DOM
4899
+ * and is projected here, untouched.
4900
+ */
4901
+ renderProviderReceipt() {
4902
+ const slot = this.providerReceipt === "walley" ? "vio-walley" : this.providerReceipt === "kustom" ? "vio-kustom" : "vio-qliro";
4903
+ return html`
4904
+ <section class="section provider-receipt">
4905
+ <slot name=${slot}></slot>
4906
+ <button class="confirm-close" @click=${this.close}>Lukk</button>
4907
+ </section>
4908
+ `;
4909
+ }
4910
+ /**
4911
+ * A completed purchase whose receipt the PROVIDER shows. Same side effects as
4912
+ * confirmOrder — `vio:payment-success` for the host and analytics, the
4913
+ * sponsor's cart cleared — but the screen is the provider's receipt, not our
4914
+ * "Takk!": two receipts for one purchase is one too many.
4915
+ */
4916
+ showProviderReceipt(method, sponsorId, result) {
4917
+ this.providerReceipt = method;
4918
+ this.confirmedMethod = method;
4919
+ this.open = true;
4920
+ this.dispatchEvent(
4921
+ new CustomEvent("vio:payment-success", {
4922
+ bubbles: true,
4923
+ composed: true,
4924
+ detail: { method, sponsorId, result }
4925
+ })
4926
+ );
4927
+ if (sponsorId) {
4928
+ try {
4929
+ Vio.cart.clearSponsorCart(sponsorId);
4930
+ } catch {
4931
+ }
4932
+ }
4933
+ }
4934
+ /** Back from the payment page, asking whether it was paid — and nothing
4935
+ * else on screen while we do. */
4936
+ renderVerifying() {
4937
+ return html`
4938
+ <section class="verifying" role="status" aria-live="polite">
4939
+ <span class="verifying-spinner" aria-hidden="true"></span>
4940
+ <p class="verifying-text">${this.verifyingReturn}</p>
4941
+ <p class="verifying-hint">Dette kan ta litt tid. Ikke lukk siden.</p>
4942
+ </section>
4943
+ `;
4944
+ }
4196
4945
  renderConfirmation() {
4197
4946
  const o = this.confirmedOrder;
4947
+ const email = o?.email || String(this.form.email ?? "").trim();
4948
+ const details = [];
4949
+ if (o?.orderId) details.push(["Ordrenummer", `#${o.orderId}`]);
4950
+ details.push(["Betalingsm\xE5te", o?.paidWith || this.methodLabel(this.confirmedMethod)]);
4951
+ if (o?.providerRef) details.push([o.providerRef.label, o.providerRef.value]);
4952
+ if (email) details.push(["Kvittering til", email]);
4198
4953
  return html`
4199
4954
  <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>` : ""}
4955
+ <header class="confirm-head">
4956
+ <div class="confirm-check" aria-hidden="true">✓</div>
4957
+ <div>
4958
+ <h3 class="confirm-title">Takk for bestillingen!</h3>
4959
+ <p class="confirm-text">
4960
+ Betalingen er bekreftet.${email ? html` Du får en ordrebekreftelse på e-post.` : ""}
4961
+ </p>
4962
+ </div>
4963
+ </header>
4964
+
4965
+ <dl class="confirm-details">
4966
+ ${details.map(
4967
+ ([k, v]) => html`<div><dt>${k}</dt><dd class=${k === o?.providerRef?.label ? "ref" : ""}>${v}</dd></div>`
4968
+ )}
4969
+ </dl>
4970
+
4206
4971
  ${o && o.items.length > 0 ? html`
4207
4972
  <div class="confirm-summary">
4973
+ <div class="confirm-label">Din bestilling</div>
4208
4974
  ${o.items.map(
4209
4975
  (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>
4976
+ <div class="confirm-line">
4977
+ ${it.imageUrl ? html`<img class="confirm-thumb" src=${it.imageUrl} alt="" loading="lazy" />` : html`<span class="confirm-thumb" aria-hidden="true"></span>`}
4978
+ <span class="confirm-line-name">
4979
+ ${it.brand ? html`<small>${it.brand}</small>` : ""}
4980
+ <span>${it.name}</span>
4981
+ <small>Antall: ${it.quantity}</small>
4982
+ </span>
4983
+ <span class="confirm-line-price">
4984
+ ${formatPrice(it.unitPrice * it.quantity, it.currency)}
4985
+ </span>
4213
4986
  </div>
4214
4987
  `
4215
4988
  )}
4989
+ ${o.shipping ? html`
4990
+ <div class="order-row">
4991
+ <span>Frakt – ${o.shipping.name}</span>
4992
+ <span>${formatPrice(o.shipping.price, o.currency)}</span>
4993
+ </div>
4994
+ ` : ""}
4216
4995
  <div class="order-row total">
4217
4996
  <span>Totalt</span><span>${formatPrice(o.total, o.currency)}</span>
4218
4997
  </div>
4219
4998
  </div>
4220
4999
  ` : ""}
4221
- <button class="confirm-close" @click=${this.close}>Lukk</button>
5000
+ <button class="confirm-close" @click=${this.close}>Fortsett å handle</button>
4222
5001
  </section>
4223
5002
  `;
4224
5003
  }
5004
+ /**
5005
+ * Whether mounting an embedded checkout now would be starting a payment the
5006
+ * shopper asked for. Mounting CREATES an order at the provider (and a backend
5007
+ * checkout if there is none), so it must not happen on a confirmation return,
5008
+ * once this checkout's order is confirmed, or for a cart that cannot ship.
5009
+ */
5010
+ mayStartEmbeddedPayment() {
5011
+ return !this.returningFrom && !this.orderConfirmed && !this.providerReceipt && !this.shippingBlocked && this.shippingsSettled();
5012
+ }
5013
+ /**
5014
+ * Whether we know enough about shipping to create an order. The provider
5015
+ * prices the order when it is created, so a first opening waits for the
5016
+ * shippings to answer: the cart may turn out unable to ship at all. A list
5017
+ * already on screen counts, and a later answer that blocks the cart takes
5018
+ * the widget down again (see `updated`).
5019
+ */
5020
+ shippingsSettled() {
5021
+ return this.items.length === 0 || this.availableShippingsList.length > 0 || this.shippingsReadyFor === this.embedSession();
5022
+ }
5023
+ /** Why nothing can be paid: the cart's products cannot ship together. */
5024
+ renderShippingBlocked() {
5025
+ return html`<div class="payment-error" role="alert">${_VioCheckout.SHIPPING_BLOCKED_MESSAGE}</div>`;
5026
+ }
5027
+ /**
5028
+ * The checkout an embedded widget's order belongs to. Kustom, Qliro and
5029
+ * Walley price the order when it is created, so a widget kept into a NEW
5030
+ * checkout shows — and charges — the cart it was created for.
5031
+ *
5032
+ * Closing drops the widgets, but a checkout can start again without a
5033
+ * close: in Vev the cart view opens on top of the checkout and "Til kassen"
5034
+ * calls `Vio.checkout.open()` again (Angelo, 2026-09-14: quantities raised
5035
+ * in the cart view, Qliro kept the old total). Every `open()` is a new
5036
+ * session, so a widget from an earlier one starts over.
5037
+ *
5038
+ * 0.11.3 compared the cart LINES instead, and that was wrong: within one
5039
+ * session the backend rewrites them (saving the shipping re-reads the cart),
5040
+ * so the first Qliro event after that — the customer picking a shipping —
5041
+ * tore the widget down mid-purchase (Alan, 2026-09-14). The session does
5042
+ * not change under the customer's hands.
5043
+ */
5044
+ embedSession() {
5045
+ const s = this.checkoutState;
5046
+ return `${s?.sponsorId ?? ""}|${s?.session ?? ""}`;
5047
+ }
4225
5048
  /** Klarna Payments widget panel: shipping + category chips + widget + pay button. */
5049
+ /**
5050
+ * The cart a Kustom order describes. When it changes under a mounted
5051
+ * widget, the order is UPDATED in place (Kustom's rule) — see
5052
+ * syncKustomIfStale. The order is a photo of the lines, not of the form:
5053
+ * address and shipping live inside the widget.
5054
+ */
5055
+ kustomCartKey() {
5056
+ return JSON.stringify([
5057
+ this.items.map((i) => [i.productId, i.variantId ?? "", i.quantity]),
5058
+ this.checkoutState?.subtotal ?? 0
5059
+ ]);
5060
+ }
4226
5061
  /** Mount the Kustom embedded checkout once it's the chosen method. */
4227
5062
  async mountKustomIfNeeded() {
4228
5063
  if (!this.open || !this.checkoutState) return;
4229
5064
  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;
5065
+ if (!this.mayStartEmbeddedPayment()) return;
5066
+ if (this.kustomMounting || this.kustomSyncing) return;
5067
+ if (this.kustomMountedOrderId) {
5068
+ if (this.kustomMountedFor === this.embedSession()) return;
5069
+ this.unmountKustom();
5070
+ }
5071
+ const container = this.lightContainer("vio-kustom-checkout-container", "vio-kustom");
5072
+ const mountedFor = this.embedSession();
5073
+ const mountedKey = this.kustomCartKey();
4236
5074
  this.kustomMounting = true;
5075
+ this.kustomNotice = null;
5076
+ this.paymentError = null;
4237
5077
  container.innerHTML = "";
4238
5078
  try {
4239
5079
  const order = await Vio.checkout.mountKustomCheckout(
4240
5080
  container,
4241
- this.checkoutState.sponsorId
5081
+ this.checkoutState.sponsorId,
5082
+ this.kustomHandlers()
4242
5083
  );
4243
5084
  this.kustomMountedOrderId = order.order_id;
5085
+ this.kustomMountedFor = mountedFor;
5086
+ this.kustomMountedKey = mountedKey;
5087
+ this.kustomShipping = order.shipping_name !== void 0 || order.shipping_price !== void 0 ? { name: order.shipping_name, price: order.shipping_price } : null;
5088
+ this.kustomTotal = typeof order.total_price === "number" ? order.total_price : null;
4244
5089
  } catch (err) {
4245
5090
  if (typeof console !== "undefined") {
4246
5091
  console.warn("[VioCheckout] Kustom mount failed:", err);
@@ -4251,18 +5096,69 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4251
5096
  this.kustomMounting = false;
4252
5097
  }
4253
5098
  }
5099
+ /** What the widget tells us, kept only to show — never acted on server-side. */
5100
+ kustomHandlers() {
5101
+ return {
5102
+ onOrderTotalChange: (minor) => {
5103
+ this.kustomTotal = minor / 100;
5104
+ },
5105
+ onShippingOptionChange: (o) => {
5106
+ this.kustomShipping = {
5107
+ name: o?.name,
5108
+ price: typeof o?.price === "number" ? o.price / 100 : void 0
5109
+ };
5110
+ },
5111
+ onCannotComplete: () => {
5112
+ this.kustomNotice = "Betalingen kunne ikke gjennomf\xF8res med valgt metode. Velg en annen betalingsm\xE5te.";
5113
+ },
5114
+ onNetworkError: () => {
5115
+ this.kustomNotice = "Mistet kontakt med betalingsvinduet. Last inn siden p\xE5 nytt.";
5116
+ }
5117
+ };
5118
+ }
5119
+ /**
5120
+ * The cart changed under a mounted Kustom widget: suspend it, push the
5121
+ * cart into the same order, resume it. Never a second order, never a
5122
+ * widget showing a total the backend will not charge (the validation
5123
+ * callback would refuse it). A failed sync takes the widget down.
5124
+ */
5125
+ syncKustomIfStale() {
5126
+ if (!this.kustomMountedOrderId || this.kustomMounting || this.kustomSyncing) return;
5127
+ if (this.providerReceipt || this.returningFrom) return;
5128
+ const key = this.kustomCartKey();
5129
+ if (key === this.kustomMountedKey) return;
5130
+ this.kustomSyncing = true;
5131
+ this.kustomMountedKey = key;
5132
+ void Vio.checkout.syncKustomOrder(this.checkoutState?.sponsorId).then((order) => {
5133
+ if (!order) throw new Error("no order");
5134
+ this.kustomMountedOrderId = order.order_id;
5135
+ this.kustomTotal = typeof order.total_price === "number" ? order.total_price : this.kustomTotal;
5136
+ }).catch((err) => {
5137
+ if (typeof console !== "undefined") {
5138
+ console.warn("[VioCheckout] Kustom sync failed:", err);
5139
+ }
5140
+ this.unmountKustom();
5141
+ this.kustomNotice = "Bestillingen ble endret. Last inn betalingen p\xE5 nytt.";
5142
+ }).finally(() => {
5143
+ this.kustomSyncing = false;
5144
+ });
5145
+ }
4254
5146
  unmountKustom() {
4255
5147
  this.kustomMountedOrderId = null;
4256
- const container = this.renderRoot?.querySelector(
4257
- "#vio-kustom-checkout-container"
4258
- );
4259
- if (container) container.innerHTML = "";
5148
+ this.kustomMountedFor = null;
5149
+ this.kustomMountedKey = null;
5150
+ this.kustomShipping = null;
5151
+ this.kustomTotal = null;
5152
+ Vio.checkout.destroyKustomListeners();
5153
+ this.querySelector("#vio-kustom-checkout-container")?.remove();
4260
5154
  }
4261
5155
  renderKustomPanel() {
4262
5156
  return html`
4263
5157
  <div class="kustom-panel">
4264
5158
  ${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>
5159
+ ${this.kustomSyncing ? html`<div style="font-size:13px;opacity:0.7;padding:8px 0;">Oppdaterer bestillingen…</div>` : ""}
5160
+ ${this.kustomNotice ? html`<div class="payment-notice">${this.kustomNotice}</div>` : ""}
5161
+ <slot name="vio-kustom"></slot>
4266
5162
  </div>
4267
5163
  `;
4268
5164
  }
@@ -4292,9 +5188,15 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4292
5188
  async mountQliroIfNeeded() {
4293
5189
  if (!this.open || !this.checkoutState) return;
4294
5190
  if (this.checkoutState.paymentMethod !== "qliro") return;
5191
+ if (!this.mayStartEmbeddedPayment()) return;
4295
5192
  if (this.qliroMounting) return;
4296
- const container = this.lightContainer("vio-qliro-checkout-container", "vio-qliro");
4297
- if (this.qliroMountedOrderId && container.childElementCount > 0) return;
5193
+ let container = this.lightContainer("vio-qliro-checkout-container", "vio-qliro");
5194
+ if (this.qliroMountedOrderId && container.childElementCount > 0) {
5195
+ if (this.qliroMountedFor === this.embedSession()) return;
5196
+ this.unmountQliro();
5197
+ container = this.lightContainer("vio-qliro-checkout-container", "vio-qliro");
5198
+ }
5199
+ const mountedFor = this.embedSession();
4298
5200
  this.qliroMounting = true;
4299
5201
  container.innerHTML = "";
4300
5202
  try {
@@ -4303,6 +5205,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4303
5205
  this.checkoutState.sponsorId
4304
5206
  );
4305
5207
  this.qliroMountedOrderId = order.order_id;
5208
+ this.qliroMountedFor = mountedFor;
4306
5209
  } catch (err) {
4307
5210
  if (typeof console !== "undefined") {
4308
5211
  console.warn("[VioCheckout] Qliro mount failed:", err);
@@ -4315,6 +5218,8 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4315
5218
  }
4316
5219
  unmountQliro() {
4317
5220
  this.qliroMountedOrderId = null;
5221
+ this.qliroMountedFor = null;
5222
+ this.qliroShipping = null;
4318
5223
  Vio.checkout.destroyQliroListeners();
4319
5224
  this.querySelector("#vio-qliro-checkout-container")?.remove();
4320
5225
  }
@@ -4335,9 +5240,15 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4335
5240
  async mountWalleyIfNeeded() {
4336
5241
  if (!this.open || !this.checkoutState) return;
4337
5242
  if (this.checkoutState.paymentMethod !== "walley") return;
5243
+ if (!this.mayStartEmbeddedPayment()) return;
4338
5244
  if (this.walleyMounting) return;
4339
- const container = this.lightContainer("vio-walley-checkout-container", "vio-walley");
4340
- if (this.walleyMountedOrderId && container.childElementCount > 0) return;
5245
+ let container = this.lightContainer("vio-walley-checkout-container", "vio-walley");
5246
+ if (this.walleyMountedOrderId && container.childElementCount > 0) {
5247
+ if (this.walleyMountedFor === this.embedSession()) return;
5248
+ this.unmountWalley();
5249
+ container = this.lightContainer("vio-walley-checkout-container", "vio-walley");
5250
+ }
5251
+ const mountedFor = this.embedSession();
4341
5252
  this.walleyMounting = true;
4342
5253
  container.innerHTML = "";
4343
5254
  try {
@@ -4345,23 +5256,13 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4345
5256
  container,
4346
5257
  this.checkoutState.sponsorId,
4347
5258
  () => {
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
- }
5259
+ this.showProviderReceipt("walley", this.checkoutState?.sponsorId ?? 0, {
5260
+ orderId: order.order_id
5261
+ });
4362
5262
  }
4363
5263
  );
4364
5264
  this.walleyMountedOrderId = order.order_id;
5265
+ this.walleyMountedFor = mountedFor;
4365
5266
  } catch (err) {
4366
5267
  if (typeof console !== "undefined") {
4367
5268
  console.warn("[VioCheckout] Walley mount failed:", err);
@@ -4374,6 +5275,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4374
5275
  }
4375
5276
  unmountWalley() {
4376
5277
  this.walleyMountedOrderId = null;
5278
+ this.walleyMountedFor = null;
4377
5279
  this.querySelector("#vio-walley-checkout-container")?.remove();
4378
5280
  }
4379
5281
  renderWalleyPanel() {
@@ -4384,6 +5286,496 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4384
5286
  </div>
4385
5287
  `;
4386
5288
  }
5289
+ /**
5290
+ * Mount the Nexi embedded checkout once it's the chosen method. Nexi's own
5291
+ * JS SDK renders into a LIGHT-DOM container (it refuses a shadow root);
5292
+ * completion is its `payment-completed` event — Nexi neither redirects nor
5293
+ * shows a receipt, so success lands in Vio's confirmation drawer. Shipping
5294
+ * is re-priced on `address-changed` (see the manager).
5295
+ */
5296
+ async mountNexiIfNeeded() {
5297
+ if (!this.open || !this.checkoutState) return;
5298
+ if (this.checkoutState.paymentMethod !== "nexi") return;
5299
+ if (!this.mayStartEmbeddedPayment()) return;
5300
+ if (this.nexiMounting) return;
5301
+ let container = this.lightContainer("vio-nexi-checkout-container", "vio-nexi");
5302
+ if (this.nexiMountedOrderId && container.childElementCount > 0) {
5303
+ if (this.nexiMountedFor === this.embedSession()) return;
5304
+ this.unmountNexi();
5305
+ container = this.lightContainer("vio-nexi-checkout-container", "vio-nexi");
5306
+ }
5307
+ const mountedFor = this.embedSession();
5308
+ const resumePaymentId = this.nexiResumePaymentId;
5309
+ this.nexiResumePaymentId = null;
5310
+ this.nexiMounting = true;
5311
+ this.nexiShippingNotice = null;
5312
+ container.innerHTML = "";
5313
+ try {
5314
+ const order = await Vio.checkout.mountNexiCheckout(container, this.checkoutState.sponsorId, {
5315
+ onCompleted: (paid) => this.onNexiCompleted(paid.order_id),
5316
+ onShipping: (result, err) => {
5317
+ const wanted = this.nexiShipping?.shipping_id;
5318
+ const pickedNow = this.nexiPicking;
5319
+ this.nexiPicking = false;
5320
+ if (err || !result) {
5321
+ this.nexiShipping = null;
5322
+ this.nexiShippingNotice = "Kunne ikke beregne frakt. Pr\xF8v \xE5 endre adressen.";
5323
+ } else if (result.reason === "AWAITING_ADDRESS") {
5324
+ this.nexiShipping = result;
5325
+ this.nexiShippingNotice = null;
5326
+ } else if (!result.ok) {
5327
+ this.nexiShipping = null;
5328
+ this.nexiShippingNotice = "Vi kan dessverre ikke sende til dette landet.";
5329
+ } else {
5330
+ this.nexiShipping = result;
5331
+ if (result.repriced_at_pay) {
5332
+ this.nexiShippingNotice = "Frakten er oppdatert for leveringsadressen din. Sjekk totalen og trykk \xABBetal\xBB igjen.";
5333
+ } else {
5334
+ this.nexiShippingNotice = !pickedNow && wanted && result.shipping_id !== wanted ? `Valgt frakt leveres ikke til denne adressen. Vi har valgt ${result.shipping_name ?? "en annen"}.` : null;
5335
+ }
5336
+ }
5337
+ }
5338
+ }, resumePaymentId ? { paymentId: resumePaymentId } : void 0);
5339
+ this.nexiMountedOrderId = order.order_id;
5340
+ this.nexiMountedFor = mountedFor;
5341
+ } catch (err) {
5342
+ if (typeof console !== "undefined") {
5343
+ console.warn("[VioCheckout] Nexi mount failed:", err);
5344
+ }
5345
+ this.paymentError = `Kunne ikke laste Nexi: ${err instanceof Error ? err.message : String(err)}`;
5346
+ Vio.checkout.selectPaymentMethod("");
5347
+ } finally {
5348
+ this.nexiMounting = false;
5349
+ }
5350
+ }
5351
+ /** payment-completed: the reservation exists — confirm like the redirect methods do. */
5352
+ onNexiCompleted(paymentId) {
5353
+ const spId = this.checkoutState?.sponsorId ?? 0;
5354
+ const checkoutId = this.checkoutState?.checkoutId ?? "";
5355
+ const charged = this.nexiShipping?.ok ? this.nexiShipping.total_price : void 0;
5356
+ this.confirmOrder("nexi", spId, { chargedTotal: charged });
5357
+ if (this.confirmedOrder) {
5358
+ this.confirmedOrder = {
5359
+ ...this.confirmedOrder,
5360
+ providerRef: { label: "Nexi betalings-ID", value: paymentId }
5361
+ };
5362
+ }
5363
+ this.unmountNexi();
5364
+ if (checkoutId) void this.addNexiPaymentDetails(checkoutId, spId, paymentId);
5365
+ }
5366
+ /**
5367
+ * How it was paid and where the receipt goes — Nexi knows, we ask. Its
5368
+ * payment details can lag the completion event by a moment, hence the
5369
+ * few retries. Best effort: the confirmation stands without them.
5370
+ */
5371
+ async addNexiPaymentDetails(checkoutId, sponsorId, paymentId) {
5372
+ for (const delay of _VioCheckout.NEXI_DETAILS_DELAYS_MS) {
5373
+ if (delay) await new Promise((r) => setTimeout(r, delay));
5374
+ const o = await Vio.checkout.getNexiOrder(checkoutId, sponsorId).catch(() => null);
5375
+ const current = this.confirmedOrder;
5376
+ if (!current || current.providerRef?.value !== paymentId) return;
5377
+ if (!o) continue;
5378
+ const paidWith = o.payment_method ? o.card_last4 ? `${o.payment_method} \u2022\u2022\u2022\u2022 ${o.card_last4}` : o.payment_method : void 0;
5379
+ this.confirmedOrder = {
5380
+ ...current,
5381
+ ...paidWith ? { paidWith } : {},
5382
+ ...o.email ? { email: o.email } : {}
5383
+ };
5384
+ if (paidWith) return;
5385
+ }
5386
+ }
5387
+ static {
5388
+ this.NEXI_DETAILS_DELAYS_MS = [0, 1500, 3e3];
5389
+ }
5390
+ unmountNexi() {
5391
+ this.nexiMountedOrderId = null;
5392
+ this.nexiMountedFor = null;
5393
+ this.nexiShippingNotice = null;
5394
+ this.nexiShipping = null;
5395
+ this.nexiPicking = false;
5396
+ Vio.checkout.destroyNexi();
5397
+ this.querySelector("#vio-nexi-checkout-container")?.remove();
5398
+ }
5399
+ /**
5400
+ * Nexi has no shipping picker: once an address is typed in the widget, the
5401
+ * rates that reach it are listed here and the pick is pushed to the payment.
5402
+ * With a single rate there is nothing to choose, so nothing is shown.
5403
+ */
5404
+ renderNexiShippingChoice() {
5405
+ const options = this.nexiShipping?.options ?? [];
5406
+ if (options.length < 2) return "";
5407
+ const currency = this.checkoutState?.currency || getGlobalCurrency();
5408
+ const current = this.nexiShipping?.shipping_id;
5409
+ return html`
5410
+ <div class="ship-select nexi-ship" role="radiogroup" aria-label="Frakt">
5411
+ ${options.map(
5412
+ (o) => html`
5413
+ <button
5414
+ class="ship-opt ${current === o.id ? "active" : ""}"
5415
+ role="radio"
5416
+ aria-checked=${current === o.id}
5417
+ ?disabled=${this.nexiPicking}
5418
+ @click=${() => void this.onPickNexiShipping(o.id)}
5419
+ >
5420
+ <span class="ship-meta"><b>${o.name}</b></span>
5421
+ <span class="ship-price">${formatPrice(o.price, currency)}</span>
5422
+ </button>
5423
+ `
5424
+ )}
5425
+ </div>
5426
+ `;
5427
+ }
5428
+ async onPickNexiShipping(id) {
5429
+ if (this.nexiPicking || id === this.nexiShipping?.shipping_id) return;
5430
+ this.nexiPicking = true;
5431
+ try {
5432
+ await Vio.checkout.pickNexiShipping(id);
5433
+ } finally {
5434
+ this.nexiPicking = false;
5435
+ }
5436
+ }
5437
+ /* ── Adyen: our form first, then Adyen's Drop-in on a session ─────────── */
5438
+ /**
5439
+ * What an embedded payment is a photo OF — an Adyen session, a Stripe
5440
+ * PaymentIntent. When any of it changes underneath, that photo no longer
5441
+ * describes the purchase: it is dropped, and the shopper asks for the
5442
+ * payment step again.
5443
+ */
5444
+ embedPurchaseKey() {
5445
+ return JSON.stringify([
5446
+ this.embedFormKey(),
5447
+ this.items.map((i) => [i.productId, i.variantId ?? "", i.quantity]),
5448
+ this.checkoutState?.subtotal ?? 0
5449
+ ]);
5450
+ }
5451
+ /** The part of the photo the shopper types and picks. */
5452
+ embedFormKey() {
5453
+ const f = this.form;
5454
+ return JSON.stringify([
5455
+ f.firstName,
5456
+ f.lastName,
5457
+ f.email,
5458
+ f.address,
5459
+ f.postalCode,
5460
+ f.city,
5461
+ f.country ?? "",
5462
+ this.selectedShipping
5463
+ ]);
5464
+ }
5465
+ /** A mounted session whose purchase changed is taken down — never patched. */
5466
+ dropStaleAdyen() {
5467
+ if (!this.adyenMounted || this.adyenMounting) return;
5468
+ if (this.adyenMountedKey === this.embedPurchaseKey()) return;
5469
+ this.unmountAdyen();
5470
+ this.adyenStale = true;
5471
+ }
5472
+ async mountAdyenIfNeeded() {
5473
+ if (!this.open || !this.checkoutState) return;
5474
+ if (this.checkoutState.paymentMethod !== "adyen") return;
5475
+ if (!this.mayStartEmbeddedPayment() || this.adyenSettled) return;
5476
+ if (this.adyenMounting) return;
5477
+ if (this.adyenMounted) {
5478
+ if (this.adyenMountedFor === this.embedSession()) return;
5479
+ this.unmountAdyen();
5480
+ }
5481
+ if (!this.adyenRequested || !this.isAddressValid || !this.hasSelectedShipping) return;
5482
+ this.adyenRequested = false;
5483
+ this.adyenMounting = true;
5484
+ this.adyenStale = false;
5485
+ this.paymentError = null;
5486
+ const spId = this.checkoutState.sponsorId;
5487
+ const mountedFor = this.embedSession();
5488
+ const formKey = this.embedFormKey();
5489
+ try {
5490
+ const selectedOpt = this.shippingList.find((o) => o.id === this.selectedShipping);
5491
+ if (selectedOpt?.supplierId) {
5492
+ await Vio.checkout.updateShippingsBySupplier(
5493
+ [{ shipping_id: selectedOpt.id, supplier_id: Number(selectedOpt.supplierId) }],
5494
+ spId
5495
+ );
5496
+ }
5497
+ await this.updateComplete;
5498
+ const container = this.lightContainer("vio-adyen-checkout-container", "vio-adyen");
5499
+ container.innerHTML = "";
5500
+ await Vio.checkout.mountAdyenCheckout(container, spId, this.form, {
5501
+ onCompleted: (payment, session) => this.onAdyenCompleted(spId, payment, session),
5502
+ onFailed: () => {
5503
+ this.unmountAdyen();
5504
+ this.paymentError = "Betalingen ble ikke gjennomf\xF8rt. Pr\xF8v igjen, eller velg en annen betalingsm\xE5te.";
5505
+ },
5506
+ onReplaced: (reason) => {
5507
+ this.unmountAdyen();
5508
+ this.adyenStale = true;
5509
+ if (reason === "ALREADY_PAID") {
5510
+ this.adyenSettled = true;
5511
+ this.paymentNotice = "Denne bestillingen er allerede betalt.";
5512
+ }
5513
+ },
5514
+ onError: (error) => {
5515
+ if (typeof console !== "undefined") console.warn("[VioCheckout] Adyen error:", error);
5516
+ }
5517
+ });
5518
+ if (this.embedSession() !== mountedFor || this.checkoutState?.paymentMethod !== "adyen" || !this.open) {
5519
+ this.unmountAdyen();
5520
+ return;
5521
+ }
5522
+ if (this.embedFormKey() !== formKey) {
5523
+ this.unmountAdyen();
5524
+ this.adyenStale = true;
5525
+ return;
5526
+ }
5527
+ this.adyenMounted = true;
5528
+ this.adyenMountedFor = mountedFor;
5529
+ this.adyenMountedKey = this.embedPurchaseKey();
5530
+ } catch (err) {
5531
+ this.unmountAdyen();
5532
+ this.paymentError = err instanceof AdyenOriginError ? "Betaling er ikke aktivert for denne siden enn\xE5. Ta kontakt med butikken." : `Kunne ikke laste betaling: ${err instanceof Error ? err.message : String(err)}`;
5533
+ if (typeof console !== "undefined") console.warn("[VioCheckout] Adyen mount failed:", err);
5534
+ } finally {
5535
+ this.adyenMounting = false;
5536
+ }
5537
+ }
5538
+ onAdyenCompleted(sponsorId, payment, session) {
5539
+ const outcome = adyenReturnOutcome(payment);
5540
+ this.unmountAdyen();
5541
+ if (outcome === "paid") {
5542
+ this.adyenSettled = true;
5543
+ this.confirmOrder("adyen", sponsorId, {
5544
+ chargedTotal: payment?.total_price ?? session.total_price
5545
+ });
5546
+ this.addAdyenPaymentDetails(payment);
5547
+ } else if (outcome === "pending") {
5548
+ this.adyenSettled = true;
5549
+ this.paymentNotice = "Betalingen behandles fortsatt. Du f\xE5r ordrebekreftelse p\xE5 e-post n\xE5r den er gjennomf\xF8rt. Handlekurven er uendret.";
5550
+ } else {
5551
+ this.paymentError = "Betalingen ble ikke gjennomf\xF8rt. Pr\xF8v igjen, eller velg en annen betalingsm\xE5te.";
5552
+ }
5553
+ }
5554
+ /** What Adyen told us about the payment, on Vio's confirmation. */
5555
+ addAdyenPaymentDetails(payment) {
5556
+ if (!this.confirmedOrder || !payment) return;
5557
+ this.confirmedOrder = {
5558
+ ...this.confirmedOrder,
5559
+ ...payment.psp_reference ? { providerRef: { label: "Betalingsreferanse", value: payment.psp_reference } } : {},
5560
+ ...payment.payment_method ? { paidWith: payment.payment_method } : {},
5561
+ ...payment.email && !this.confirmedOrder.email ? { email: payment.email } : {},
5562
+ ...payment.shipping_name && !this.confirmedOrder.shipping ? { shipping: { name: payment.shipping_name, price: payment.shipping_price ?? 0 } } : {}
5563
+ };
5564
+ }
5565
+ unmountAdyen() {
5566
+ Vio.checkout.destroyAdyen();
5567
+ this.adyenMounted = false;
5568
+ this.adyenMountedFor = null;
5569
+ this.adyenMountedKey = null;
5570
+ this.adyenRequested = false;
5571
+ this.adyenStale = false;
5572
+ this.querySelector("#vio-adyen-checkout-container")?.remove();
5573
+ }
5574
+ renderAdyenPanel() {
5575
+ const ready = this.isAddressValid && this.hasSelectedShipping;
5576
+ const showButton = !this.adyenMounted && !this.adyenMounting && !this.adyenSettled;
5577
+ return html`
5578
+ <div class="adyen-panel">
5579
+ ${this.adyenMounting ? html`<div style="font-size:13px;opacity:0.7;padding:8px 0;">Laster betaling…</div>` : ""}
5580
+ ${this.adyenStale && showButton ? html`<div class="payment-notice">Bestillingen ble endret. Oppdater betalingen for å fortsette.</div>` : ""}
5581
+ ${showButton ? html`
5582
+ <button
5583
+ class="payment-btn primary complete-cta"
5584
+ ?disabled=${!ready}
5585
+ title=${!this.isAddressValid ? "Vennligst fyll ut leveringsadresse" : !this.hasSelectedShipping ? "Vennligst velg en fraktmetode" : ""}
5586
+ @click=${() => {
5587
+ this.adyenRequested = true;
5588
+ }}
5589
+ >
5590
+ ${this.adyenStale ? "Oppdater betaling" : `G\xE5 til betaling \xB7 ${this.payTotalLabel()}`}
5591
+ </button>
5592
+ ` : ""}
5593
+ <slot name="vio-adyen"></slot>
5594
+ </div>
5595
+ `;
5596
+ }
5597
+ /* ── Stripe, native: the Payment Element on our own page ──────────────── */
5598
+ /** A mounted Element whose purchase changed is taken down — never patched. */
5599
+ dropStaleStripe() {
5600
+ if (!this.stripeMounted || this.stripeMounting) return;
5601
+ if (this.stripeMountedKey === this.embedPurchaseKey()) return;
5602
+ this.unmountStripe();
5603
+ this.stripeStale = true;
5604
+ }
5605
+ async mountStripeIfNeeded() {
5606
+ if (!this.open || !this.checkoutState) return;
5607
+ if (this.checkoutState.paymentMethod !== "stripe" || this.stripeMode !== "native") return;
5608
+ if (!this.mayStartEmbeddedPayment() || this.stripeSettled) return;
5609
+ if (this.stripeMounting) return;
5610
+ if (this.stripeMounted) {
5611
+ if (this.stripeMountedFor === this.embedSession()) return;
5612
+ this.unmountStripe();
5613
+ }
5614
+ if (!this.stripeRequested || !this.isAddressValid || !this.hasSelectedShipping) return;
5615
+ this.stripeRequested = false;
5616
+ this.stripeMounting = true;
5617
+ this.stripeStale = false;
5618
+ this.paymentError = null;
5619
+ const spId = this.checkoutState.sponsorId;
5620
+ const mountedFor = this.embedSession();
5621
+ const formKey = this.embedFormKey();
5622
+ try {
5623
+ const selectedOpt = this.shippingList.find((o) => o.id === this.selectedShipping);
5624
+ if (selectedOpt?.supplierId) {
5625
+ await Vio.checkout.updateShippingsBySupplier(
5626
+ [{ shipping_id: selectedOpt.id, supplier_id: Number(selectedOpt.supplierId) }],
5627
+ spId
5628
+ );
5629
+ }
5630
+ await this.updateComplete;
5631
+ const container = this.lightContainer("vio-stripe-checkout-container", "vio-stripe");
5632
+ container.innerHTML = "";
5633
+ this.stripeReady = false;
5634
+ const mounted = await Vio.checkout.mountStripeCheckout(container, spId, this.form, {
5635
+ // `mount()` resolves before Stripe paints anything — seconds before,
5636
+ // on a cold cache. Its own "ready" is what turns the spinner off.
5637
+ onReady: () => {
5638
+ this.stripeReady = true;
5639
+ },
5640
+ onLoadError: (message) => {
5641
+ this.unmountStripe();
5642
+ this.paymentError = `Kunne ikke laste betaling: ${message}`;
5643
+ }
5644
+ });
5645
+ if (this.embedSession() !== mountedFor || this.checkoutState?.paymentMethod !== "stripe" || !this.open) {
5646
+ this.unmountStripe();
5647
+ return;
5648
+ }
5649
+ if (this.embedFormKey() !== formKey) {
5650
+ this.unmountStripe();
5651
+ this.stripeStale = true;
5652
+ return;
5653
+ }
5654
+ this.stripeElement = mounted.handle;
5655
+ this.stripeCheckoutId = mounted.checkoutId;
5656
+ this.stripeMounted = true;
5657
+ this.stripeMountedFor = mountedFor;
5658
+ this.stripeMountedKey = this.embedPurchaseKey();
5659
+ } catch (err) {
5660
+ this.unmountStripe();
5661
+ this.paymentError = `Kunne ikke laste betaling: ${err instanceof Error ? err.message : String(err)}`;
5662
+ if (typeof console !== "undefined") console.warn("[VioCheckout] Stripe mount failed:", err);
5663
+ } finally {
5664
+ this.stripeMounting = false;
5665
+ }
5666
+ }
5667
+ /**
5668
+ * Pay with the mounted Element. Stripe answers the BROWSER; the order is
5669
+ * created server-side by the `payment_intent.succeeded` webhook, so a
5670
+ * success is then waited for on the checkout itself.
5671
+ */
5672
+ async onStripePay() {
5673
+ if (!this.stripeElement || this.stripePaying) return;
5674
+ const spId = this.checkoutState?.sponsorId ?? 0;
5675
+ const checkoutId = this.stripeCheckoutId;
5676
+ const charged = (this.checkoutState?.subtotal ?? 0) + this.shippingMajor();
5677
+ this.stripePaying = true;
5678
+ this.paymentError = null;
5679
+ try {
5680
+ const outcome = await this.stripeElement.confirm();
5681
+ if (outcome.status === "failed") {
5682
+ this.paymentError = outcome.message || "Betalingen ble ikke gjennomf\xF8rt. Pr\xF8v igjen, eller velg en annen betalingsm\xE5te.";
5683
+ return;
5684
+ }
5685
+ this.stripeSettled = true;
5686
+ this.unmountStripe();
5687
+ const settled = checkoutId ? await this.waitForStripeOrder(checkoutId, spId) : "pending";
5688
+ if (settled === "paid") {
5689
+ this.confirmOrder("stripe", spId, { chargedTotal: charged });
5690
+ } else if (settled === "failed") {
5691
+ this.stripeSettled = false;
5692
+ this.paymentError = "Betalingen ble avbrutt eller feilet. Vennligst pr\xF8v igjen.";
5693
+ } else {
5694
+ this.paymentNotice = "Betalingen behandles fortsatt. Du f\xE5r ordrebekreftelse p\xE5 e-post n\xE5r den er gjennomf\xF8rt. Handlekurven er uendret.";
5695
+ }
5696
+ } catch (err) {
5697
+ this.paymentError = `Betalingen kunne ikke fullf\xF8res: ${err instanceof Error ? err.message : String(err)}`;
5698
+ if (typeof console !== "undefined") console.warn("[VioCheckout] Stripe confirm failed:", err);
5699
+ } finally {
5700
+ this.stripePaying = false;
5701
+ }
5702
+ }
5703
+ /**
5704
+ * Wait for Stripe's webhook to make the checkout paid — the same window the
5705
+ * redirect return uses, and ONLY the status: a Stripe checkout carries
5706
+ * `origin_payment_id` from the moment the intent is created, so that
5707
+ * pointer would read as paid before anyone paid.
5708
+ */
5709
+ async waitForStripeOrder(checkoutId, sponsorId) {
5710
+ for (const delayMs of _VioCheckout.RETURN_VERIFY_DELAYS_MS) {
5711
+ if (delayMs > 0) await new Promise((r) => setTimeout(r, delayMs));
5712
+ try {
5713
+ const checkout = await Vio.checkout.getCheckout(checkoutId, sponsorId || void 0);
5714
+ const status = String(checkout?.status ?? "").toLowerCase();
5715
+ if (_VioCheckout.FAILED_STATUSES.has(status)) return "failed";
5716
+ if (_VioCheckout.PAID_STATUSES.has(status)) return "paid";
5717
+ } catch (err) {
5718
+ if (typeof console !== "undefined") {
5719
+ console.warn("[VioCheckout] Stripe order check failed:", err);
5720
+ }
5721
+ }
5722
+ }
5723
+ return "pending";
5724
+ }
5725
+ unmountStripe() {
5726
+ Vio.checkout.destroyStripe();
5727
+ this.stripeReady = false;
5728
+ this.stripeElement = null;
5729
+ this.stripeCheckoutId = null;
5730
+ this.stripeMounted = false;
5731
+ this.stripeMountedFor = null;
5732
+ this.stripeMountedKey = null;
5733
+ this.stripeRequested = false;
5734
+ this.stripeStale = false;
5735
+ this.querySelector("#vio-stripe-checkout-container")?.remove();
5736
+ }
5737
+ renderStripePanel() {
5738
+ const ready = this.isAddressValid && this.hasSelectedShipping;
5739
+ const showButton = !this.stripeMounted && !this.stripeMounting && !this.stripeSettled;
5740
+ return html`
5741
+ <div class="stripe-panel">
5742
+ ${this.stripeMounting || this.stripeMounted && !this.stripeReady ? html`<div style="font-size:13px;opacity:0.7;padding:8px 0;">Laster betaling…</div>` : ""}
5743
+ ${this.stripeStale && showButton ? html`<div class="payment-notice">Bestillingen ble endret. Oppdater betalingen for å fortsette.</div>` : ""}
5744
+ ${showButton ? html`
5745
+ <button
5746
+ class="payment-btn primary complete-cta"
5747
+ ?disabled=${!ready}
5748
+ title=${!this.isAddressValid ? "Vennligst fyll ut leveringsadresse" : !this.hasSelectedShipping ? "Vennligst velg en fraktmetode" : ""}
5749
+ @click=${() => {
5750
+ this.stripeRequested = true;
5751
+ }}
5752
+ >
5753
+ ${this.stripeStale ? "Oppdater betaling" : `G\xE5 til betaling \xB7 ${this.payTotalLabel()}`}
5754
+ </button>
5755
+ ` : ""}
5756
+ <slot name="vio-stripe"></slot>
5757
+ ${this.stripeMounted ? html`
5758
+ <button
5759
+ class="payment-btn primary complete-cta"
5760
+ ?disabled=${this.stripePaying || !this.stripeReady}
5761
+ @click=${() => void this.onStripePay()}
5762
+ >
5763
+ ${this.stripePaying ? "Betaler\u2026" : `Betal ${this.payTotalLabel()}`}
5764
+ </button>
5765
+ ` : ""}
5766
+ </div>
5767
+ `;
5768
+ }
5769
+ renderNexiPanel() {
5770
+ return html`
5771
+ <div class="nexi-panel">
5772
+ ${this.nexiMounting ? html`<div style="font-size:13px;opacity:0.7;padding:8px 0;">Laster Nexi…</div>` : ""}
5773
+ ${this.renderNexiShippingChoice()}
5774
+ <slot name="vio-nexi"></slot>
5775
+ ${this.nexiShippingNotice ? html`<div class="payment-notice">${this.nexiShippingNotice}</div>` : ""}
5776
+ </div>
5777
+ `;
5778
+ }
4387
5779
  renderKlarnaPanel() {
4388
5780
  const currency = this.checkoutState?.currency || getGlobalCurrency();
4389
5781
  return html`
@@ -4433,7 +5825,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4433
5825
  <button
4434
5826
  class="payment-btn primary complete-cta"
4435
5827
  @click=${() => void this.onKlarnaAuthorize()}
4436
- ?disabled=${this.klarnaAuthorizing || !this.klarnaHandle || this.klarnaMounting}
5828
+ ?disabled=${this.klarnaAuthorizing || !this.klarnaHandle || this.klarnaMounting || !this.express && (!this.isAddressValid || !this.hasSelectedShipping)}
4437
5829
  >
4438
5830
  ${this.klarnaAuthorizing ? "Behandler\u2026" : this.klarnaMounting ? "Klargj\xF8r Klarna\u2026" : `Betal ${this.payTotalLabel()} med Klarna`}
4439
5831
  </button>
@@ -4483,7 +5875,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4483
5875
  </div>
4484
5876
  </div>
4485
5877
  ` : ""}
4486
- ${this.renderKlarnaPanel()}
5878
+ ${this.shippingBlocked ? this.renderShippingBlocked() : this.renderKlarnaPanel()}
4487
5879
  ${this.paymentNotice ? html`<div class="payment-notice">${this.paymentNotice}</div>` : ""}
4488
5880
  ${this.paymentError ? html`<div class="payment-error">${this.paymentError}</div>` : ""}
4489
5881
  </section>
@@ -4493,10 +5885,10 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4493
5885
  const method = this.checkoutState?.paymentMethod;
4494
5886
  const showCompleteCta = !!method && !this.isExpressMethod(method);
4495
5887
  const isVipps = method === "vipps";
4496
- const isKustom = method === "kustom" || method === "qliro" || method === "walley";
5888
+ const needsOurForm = !!method && !collectsOwnAddress(method);
4497
5889
  return html`
4498
- ${!isVipps && !isKustom ? html`
4499
- <section class="section">
5890
+ ${needsOurForm ? html`
5891
+ <section class="section address-section">
4500
5892
  <div class="section-label">Steg 1</div>
4501
5893
  <h3 class="section-heading">Leveringsadresse</h3>
4502
5894
  <div class="form-row">
@@ -4612,32 +6004,41 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4612
6004
  ` : ""}
4613
6005
 
4614
6006
  <section class="section">
4615
- <div class="section-label">${isVipps || isKustom ? "Betaling" : "Steg 2"}</div>
4616
- ${method ? html`
6007
+ <div class="section-label">
6008
+ ${needsOurForm ? "Steg 2" : "Betaling"}
6009
+ </div>
6010
+ ${this.shippingBlocked ? this.renderShippingBlocked() : method ? html`
4617
6011
  <div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px;">
4618
6012
  <h3 class="section-heading" style="margin:0;">
4619
6013
  Betalingsmåte: <b>${this.methodLabel(method)}</b>
4620
6014
  </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=${() => {
6015
+ ${(this.availableMethods?.length ?? 2) > 1 ? html`
6016
+ <button
6017
+ type="button"
6018
+ style="background:none; border:none; color:var(--vio-color-accent, #c14a3b); text-decoration:underline; font-size:13px; cursor:pointer;"
6019
+ @click=${() => {
4625
6020
  this.unmountKlarna();
4626
6021
  this.unmountKustom();
4627
6022
  this.unmountQliro();
4628
6023
  this.unmountWalley();
6024
+ this.unmountNexi();
6025
+ this.unmountAdyen();
4629
6026
  Vio.checkout.selectPaymentMethod("");
4630
6027
  }}
4631
- >
4632
- Endre
4633
- </button>
6028
+ >
6029
+ Endre
6030
+ </button>
6031
+ ` : ""}
4634
6032
  </div>
4635
6033
 
4636
6034
  ${method === "klarna" ? this.renderKlarnaPanel() : ""}
4637
6035
  ${method === "kustom" ? this.renderKustomPanel() : ""}
4638
6036
  ${method === "qliro" ? this.renderQliroPanel() : ""}
4639
6037
  ${method === "walley" ? this.renderWalleyPanel() : ""}
4640
- ${method === "stripe" ? html`
6038
+ ${method === "nexi" ? this.renderNexiPanel() : ""}
6039
+ ${method === "adyen" ? this.renderAdyenPanel() : ""}
6040
+ ${method === "stripe" && this.stripeMode === "native" ? this.renderStripePanel() : ""}
6041
+ ${method === "stripe" && this.stripeMode !== "native" ? html`
4641
6042
  <button
4642
6043
  class="payment-btn primary complete-cta"
4643
6044
  @click=${() => void this.onPay("stripe")}
@@ -4676,19 +6077,25 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4676
6077
  Betal ${this.payTotalLabel()} med ${this.methodLabel(method ?? null)}
4677
6078
  </button>
4678
6079
  ` : ""}
4679
- ${!isVipps && !this.isAddressValid ? html`
6080
+ ${needsOurForm && !this.isAddressValid ? html`
4680
6081
  <div style="font-size: 12px; color: var(--vio-color-accent, #c14a3b); text-align: center; margin-top: 8px;">
4681
6082
  Vennligst fyll ut leveringsadresse for å fullføre betalingen
4682
6083
  </div>
4683
- ` : !isVipps && !this.hasSelectedShipping ? html`
6084
+ ` : needsOurForm && !this.hasSelectedShipping ? html`
4684
6085
  <div style="font-size: 12px; color: var(--vio-color-accent, #c14a3b); text-align: center; margin-top: 8px;">
4685
6086
  Vennligst velg en fraktmetode for å fullføre betalingen
4686
6087
  </div>
4687
6088
  ` : ""}
4688
- ` : html`
6089
+ ` : !this.paymentMethodsResolved ? html`
6090
+ <h3 class="section-heading">Velg betalingsmåte</h3>
6091
+ <div class="payment-loading">
6092
+ <span class="payment-spinner" aria-hidden="true"></span>
6093
+ <span>Henter betalingsmåter…</span>
6094
+ </div>
6095
+ ` : html`
4689
6096
  <h3 class="section-heading">Velg betalingsmåte</h3>
4690
6097
  <div class="payment-grid">
4691
- ${this.methodEnabled("apple-pay") && this.applePayAvailable ? html`
6098
+ ${this.methodEnabled("apple-pay") && this.applePayAvailable && !this.stripeOwnsWallets() ? html`
4692
6099
  <button
4693
6100
  class="payment-btn primary"
4694
6101
  @click=${() => this.onPay("apple-pay")}
@@ -4726,15 +6133,26 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4726
6133
  <span style="font-weight:800;font-size:16px;letter-spacing:-0.01em">Walley</span>
4727
6134
  </button>
4728
6135
  ` : ""}
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>
6136
+ ${this.methodEnabled("nexi") ? html`
6137
+ <button class="payment-btn" @click=${() => this.onPay("nexi")}>
6138
+ <span style="font-weight:800;font-size:16px;letter-spacing:-0.01em">Nexi</span>
4732
6139
  </button>
4733
6140
  ` : ""}
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>
6141
+ ${this.methodEnabled("adyen") ? html`
6142
+ <button class="payment-btn" @click=${() => this.onPay("adyen")}>
6143
+ <span style="font-weight:700;font-size:15px;letter-spacing:-0.01em">${this.methodLabel("adyen")}</span>
6144
+ </button>
6145
+ ` : ""}
6146
+ ${this.methodEnabled("vipps") ? html`
6147
+ <button class="payment-btn" @click=${() => this.onPay("vipps")} aria-label="Vipps">
6148
+ ${this.vippsOfficial ? html`<vipps-mobilepay-button
6149
+ brand="vipps"
6150
+ variant="primary"
6151
+ rounded="true"
6152
+ compact="true"
6153
+ style="pointer-events:none"
6154
+ ></vipps-mobilepay-button>` : html`<span style="color:#ff5b24;font-weight:800;font-size:17px;letter-spacing:-0.02em">vipps</span>`}
6155
+ </button>
4738
6156
  ` : ""}
4739
6157
  </div>
4740
6158
  `}
@@ -4765,7 +6183,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4765
6183
  <span>Sum</span><span>${this.orderTotal()}</span>
4766
6184
  </div>
4767
6185
  <div class="order-row">
4768
- <span>Frakt${this.shippingMajor() > 0 && this.shippingOption ? ` \u2013 ${this.shippingOption.method || this.shippingOption.name}` : ""}</span>
6186
+ <span>Frakt${this.shippingMajor() > 0 && this.summaryShippingName() ? ` \u2013 ${this.summaryShippingName()}` : ""}</span>
4769
6187
  <span>${this.shippingMajor() > 0 ? formatPrice(this.shippingMajor(), this.checkoutState?.currency) : this.shippingLabel}</span>
4770
6188
  </div>
4771
6189
  <div class="order-row total">
@@ -4808,6 +6226,9 @@ __decorateClass([
4808
6226
  __decorateClass([
4809
6227
  state()
4810
6228
  ], _VioCheckout.prototype, "orderConfirmed", 2);
6229
+ __decorateClass([
6230
+ state()
6231
+ ], _VioCheckout.prototype, "providerReceipt", 2);
4811
6232
  __decorateClass([
4812
6233
  state()
4813
6234
  ], _VioCheckout.prototype, "confirmedMethod", 2);
@@ -4817,6 +6238,9 @@ __decorateClass([
4817
6238
  __decorateClass([
4818
6239
  state()
4819
6240
  ], _VioCheckout.prototype, "paymentNotice", 2);
6241
+ __decorateClass([
6242
+ state()
6243
+ ], _VioCheckout.prototype, "verifyingReturn", 2);
4820
6244
  __decorateClass([
4821
6245
  state()
4822
6246
  ], _VioCheckout.prototype, "confirmedOrder", 2);
@@ -4838,15 +6262,84 @@ __decorateClass([
4838
6262
  __decorateClass([
4839
6263
  state()
4840
6264
  ], _VioCheckout.prototype, "availableMethods", 2);
6265
+ __decorateClass([
6266
+ state()
6267
+ ], _VioCheckout.prototype, "vippsOfficial", 2);
4841
6268
  __decorateClass([
4842
6269
  state()
4843
6270
  ], _VioCheckout.prototype, "kustomMounting", 2);
6271
+ __decorateClass([
6272
+ state()
6273
+ ], _VioCheckout.prototype, "kustomSyncing", 2);
6274
+ __decorateClass([
6275
+ state()
6276
+ ], _VioCheckout.prototype, "kustomShipping", 2);
6277
+ __decorateClass([
6278
+ state()
6279
+ ], _VioCheckout.prototype, "kustomTotal", 2);
6280
+ __decorateClass([
6281
+ state()
6282
+ ], _VioCheckout.prototype, "kustomNotice", 2);
4844
6283
  __decorateClass([
4845
6284
  state()
4846
6285
  ], _VioCheckout.prototype, "qliroMounting", 2);
4847
6286
  __decorateClass([
4848
6287
  state()
4849
6288
  ], _VioCheckout.prototype, "walleyMounting", 2);
6289
+ __decorateClass([
6290
+ state()
6291
+ ], _VioCheckout.prototype, "nexiMounting", 2);
6292
+ __decorateClass([
6293
+ state()
6294
+ ], _VioCheckout.prototype, "nexiShippingNotice", 2);
6295
+ __decorateClass([
6296
+ state()
6297
+ ], _VioCheckout.prototype, "nexiShipping", 2);
6298
+ __decorateClass([
6299
+ state()
6300
+ ], _VioCheckout.prototype, "nexiPicking", 2);
6301
+ __decorateClass([
6302
+ state()
6303
+ ], _VioCheckout.prototype, "adyenMounting", 2);
6304
+ __decorateClass([
6305
+ state()
6306
+ ], _VioCheckout.prototype, "adyenRequested", 2);
6307
+ __decorateClass([
6308
+ state()
6309
+ ], _VioCheckout.prototype, "adyenMounted", 2);
6310
+ __decorateClass([
6311
+ state()
6312
+ ], _VioCheckout.prototype, "adyenStale", 2);
6313
+ __decorateClass([
6314
+ state()
6315
+ ], _VioCheckout.prototype, "adyenSettled", 2);
6316
+ __decorateClass([
6317
+ state()
6318
+ ], _VioCheckout.prototype, "stripeMode", 2);
6319
+ __decorateClass([
6320
+ state()
6321
+ ], _VioCheckout.prototype, "stripeRequested", 2);
6322
+ __decorateClass([
6323
+ state()
6324
+ ], _VioCheckout.prototype, "stripeMounting", 2);
6325
+ __decorateClass([
6326
+ state()
6327
+ ], _VioCheckout.prototype, "stripeMounted", 2);
6328
+ __decorateClass([
6329
+ state()
6330
+ ], _VioCheckout.prototype, "stripeStale", 2);
6331
+ __decorateClass([
6332
+ state()
6333
+ ], _VioCheckout.prototype, "stripeSettled", 2);
6334
+ __decorateClass([
6335
+ state()
6336
+ ], _VioCheckout.prototype, "stripePaying", 2);
6337
+ __decorateClass([
6338
+ state()
6339
+ ], _VioCheckout.prototype, "stripeReady", 2);
6340
+ __decorateClass([
6341
+ state()
6342
+ ], _VioCheckout.prototype, "qliroShipping", 2);
4850
6343
  __decorateClass([
4851
6344
  state()
4852
6345
  ], _VioCheckout.prototype, "stripeLoading", 2);
@@ -4856,6 +6349,15 @@ __decorateClass([
4856
6349
  __decorateClass([
4857
6350
  state()
4858
6351
  ], _VioCheckout.prototype, "form", 2);
6352
+ __decorateClass([
6353
+ state()
6354
+ ], _VioCheckout.prototype, "paymentMethodsResolved", 2);
6355
+ __decorateClass([
6356
+ state()
6357
+ ], _VioCheckout.prototype, "shippingBlocked", 2);
6358
+ __decorateClass([
6359
+ state()
6360
+ ], _VioCheckout.prototype, "shippingsReadyFor", 2);
4859
6361
  var VioCheckout = _VioCheckout;
4860
6362
 
4861
6363
  // src/ui/elements.ts
@@ -4875,5 +6377,5 @@ function registerVioElements() {
4875
6377
  }
4876
6378
 
4877
6379
  export { VioCart, VioCheckout, VioProduct, VioProductCarousel, VioProductDetail, VioTokens, applyVioTheme, registerVioElements };
4878
- //# sourceMappingURL=chunk-7TGCXO5J.js.map
4879
- //# sourceMappingURL=chunk-7TGCXO5J.js.map
6380
+ //# sourceMappingURL=chunk-NUNGU6LL.js.map
6381
+ //# sourceMappingURL=chunk-NUNGU6LL.js.map