@vio-live/web-sdk 0.11.0 → 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.
- package/README.md +9 -0
- package/dist/{chunk-U2QG4IVJ.js → chunk-CMNTHF66.js} +1321 -57
- package/dist/chunk-CMNTHF66.js.map +1 -0
- package/dist/{chunk-XU5LGUBU.js → chunk-NUNGU6LL.js} +1774 -269
- package/dist/chunk-NUNGU6LL.js.map +1 -0
- package/dist/chunk-PZUOMOLF.js +3 -0
- package/dist/{chunk-QYQQ5J6E.js.map → chunk-PZUOMOLF.js.map} +1 -1
- package/dist/core/index.d.ts +623 -120
- package/dist/core/index.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -2
- package/dist/react/index.d.ts +2 -2
- package/dist/react/index.js +3 -3
- package/dist/{tokens-D6PBisUm.d.ts → tokens-IrVSMVcA.d.ts} +384 -1
- package/dist/ui/index.d.ts +1 -1
- package/dist/ui/index.js +3 -3
- package/package.json +2 -1
- package/dist/chunk-QYQQ5J6E.js +0 -3
- package/dist/chunk-U2QG4IVJ.js.map +0 -1
- package/dist/chunk-XU5LGUBU.js.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Vio, __decorateClass, primaryImageUrl, displayPrice, getGlobalCurrency, formatPrice } from './chunk-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
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.
|
|
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);
|
|
@@ -2458,10 +2631,13 @@ var VioCart = class extends LitElement {
|
|
|
2458
2631
|
this.open = false;
|
|
2459
2632
|
}
|
|
2460
2633
|
show() {
|
|
2634
|
+
const wasClosed = !this.open;
|
|
2461
2635
|
this.open = true;
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2636
|
+
if (wasClosed) {
|
|
2637
|
+
this.dispatchEvent(
|
|
2638
|
+
new CustomEvent("vio:open-cart", { bubbles: true, composed: true })
|
|
2639
|
+
);
|
|
2640
|
+
}
|
|
2465
2641
|
void this.loadAvailablePaymentMethods();
|
|
2466
2642
|
}
|
|
2467
2643
|
/** Which payment buttons to render — backend-configured per sponsor.
|
|
@@ -2491,10 +2667,8 @@ var VioCart = class extends LitElement {
|
|
|
2491
2667
|
}
|
|
2492
2668
|
}
|
|
2493
2669
|
/** Backend names arrive as e.g. "Apple Pay" — compare letters only. */
|
|
2494
|
-
methodEnabled(
|
|
2495
|
-
|
|
2496
|
-
const norm = (s) => s.toLowerCase().replace(/[^a-z]/g, "");
|
|
2497
|
-
return this.availableMethods.some((m) => norm(m) === norm(name));
|
|
2670
|
+
methodEnabled(...names) {
|
|
2671
|
+
return isMethodEnabled(this.availableMethods, ...names);
|
|
2498
2672
|
}
|
|
2499
2673
|
updated(changed) {
|
|
2500
2674
|
if (changed.has("open") && this.open) {
|
|
@@ -2582,15 +2756,59 @@ var VioCart = class extends LitElement {
|
|
|
2582
2756
|
);
|
|
2583
2757
|
}
|
|
2584
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
|
+
*/
|
|
2585
2785
|
onVipps() {
|
|
2586
2786
|
if (this.itemCount === 0) return;
|
|
2587
2787
|
const firstSponsorId = [...this.carts.keys()][0];
|
|
2588
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;
|
|
2589
2807
|
this.dispatchEvent(
|
|
2590
2808
|
new CustomEvent("vio:checkout-open", {
|
|
2591
2809
|
bubbles: true,
|
|
2592
2810
|
composed: true,
|
|
2593
|
-
detail: { sponsorId
|
|
2811
|
+
detail: { sponsorId, paymentMethod: "vipps", express: false }
|
|
2594
2812
|
})
|
|
2595
2813
|
);
|
|
2596
2814
|
}
|
|
@@ -2702,6 +2920,13 @@ var VioCart = class extends LitElement {
|
|
|
2702
2920
|
${formatPrice(this.totalAcrossSponsors, this.primaryCurrency)}
|
|
2703
2921
|
</span>
|
|
2704
2922
|
</div>
|
|
2923
|
+
<button
|
|
2924
|
+
class="checkout-btn"
|
|
2925
|
+
@click=${this.onCheckout}
|
|
2926
|
+
aria-label="Til kassen"
|
|
2927
|
+
>
|
|
2928
|
+
Til kassen
|
|
2929
|
+
</button>
|
|
2705
2930
|
${this.methodEnabled("apple-pay") && this.applePayAvailable ? html`
|
|
2706
2931
|
<button
|
|
2707
2932
|
class="applepay-btn"
|
|
@@ -2735,15 +2960,28 @@ var VioCart = class extends LitElement {
|
|
|
2735
2960
|
/>
|
|
2736
2961
|
</button>
|
|
2737
2962
|
` : ""}
|
|
2738
|
-
${this.methodEnabled("vipps") ? html`
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
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
|
+
` : ""}
|
|
2747
2985
|
</div>
|
|
2748
2986
|
`}
|
|
2749
2987
|
</aside>
|
|
@@ -2800,6 +3038,9 @@ __decorateClass([
|
|
|
2800
3038
|
__decorateClass([
|
|
2801
3039
|
state()
|
|
2802
3040
|
], VioCart.prototype, "availableMethods", 2);
|
|
3041
|
+
__decorateClass([
|
|
3042
|
+
state()
|
|
3043
|
+
], VioCart.prototype, "vippsOfficial", 2);
|
|
2803
3044
|
__decorateClass([
|
|
2804
3045
|
state()
|
|
2805
3046
|
], VioCart.prototype, "confirmedOrder", 2);
|
|
@@ -2819,9 +3060,13 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
2819
3060
|
this.applePayInProgress = false;
|
|
2820
3061
|
this.klarnaAvailable = false;
|
|
2821
3062
|
this.orderConfirmed = false;
|
|
3063
|
+
this.providerReceipt = null;
|
|
2822
3064
|
this.confirmedMethod = null;
|
|
2823
3065
|
this.paymentError = null;
|
|
2824
3066
|
this.paymentNotice = null;
|
|
3067
|
+
this.verifyingReturn = null;
|
|
3068
|
+
/** The checkout whose confirmation is on screen — see fillOrderNumber. */
|
|
3069
|
+
this.confirmedCheckoutId = null;
|
|
2825
3070
|
this.confirmedOrder = null;
|
|
2826
3071
|
/** Active Klarna Payments widget handle + the subtotal it was mounted for. */
|
|
2827
3072
|
this.klarnaHandle = null;
|
|
@@ -2834,12 +3079,70 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
2834
3079
|
this.selectedShipping = "";
|
|
2835
3080
|
this.availableShippingsList = [];
|
|
2836
3081
|
this.availableMethods = null;
|
|
3082
|
+
this.vippsOfficial = false;
|
|
2837
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;
|
|
2838
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;
|
|
2839
3096
|
this.qliroMounting = false;
|
|
2840
3097
|
this.qliroMountedOrderId = null;
|
|
2841
3098
|
this.walleyMounting = false;
|
|
2842
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;
|
|
2843
3146
|
this.stripeLoading = false;
|
|
2844
3147
|
this.vippsLoading = false;
|
|
2845
3148
|
this.form = {
|
|
@@ -2852,10 +3155,31 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
2852
3155
|
};
|
|
2853
3156
|
/** In-flight guard for loadAvailablePaymentMethods. */
|
|
2854
3157
|
this.loadingPaymentMethods = false;
|
|
3158
|
+
this.paymentMethodsResolved = false;
|
|
3159
|
+
/** Guards `autoSelectSoleMethod` to ONE attempt per opening — see there. */
|
|
3160
|
+
this.autoSelectAttempted = false;
|
|
2855
3161
|
/** In-flight + attempted guards for loadAvailableShippings (change events
|
|
2856
3162
|
* fire per keystroke — without these, typing an address hammers the API). */
|
|
2857
3163
|
this.loadingShippings = false;
|
|
2858
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
|
+
};
|
|
2859
3183
|
this.boundOnCheckoutChange = (e) => {
|
|
2860
3184
|
const detail = e.detail;
|
|
2861
3185
|
const prevSponsorId = this.checkoutState?.sponsorId;
|
|
@@ -2863,7 +3187,8 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
2863
3187
|
this.checkoutState = detail.state;
|
|
2864
3188
|
this.items = Vio.checkout.items;
|
|
2865
3189
|
if (this.checkoutState) {
|
|
2866
|
-
if (sponsorChanged || this.
|
|
3190
|
+
if (sponsorChanged || !this.paymentMethodsResolved) {
|
|
3191
|
+
if (sponsorChanged) this.paymentMethodsResolved = false;
|
|
2867
3192
|
void this.refreshApplePay();
|
|
2868
3193
|
void this.refreshKlarna();
|
|
2869
3194
|
void this.loadAvailablePaymentMethods();
|
|
@@ -2875,13 +3200,28 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
2875
3200
|
this.applePayAvailable = false;
|
|
2876
3201
|
this.klarnaAvailable = false;
|
|
2877
3202
|
this.orderConfirmed = false;
|
|
3203
|
+
this.providerReceipt = null;
|
|
2878
3204
|
this.confirmedMethod = null;
|
|
2879
3205
|
this.confirmedOrder = null;
|
|
2880
3206
|
this.paymentError = null;
|
|
2881
3207
|
this.paymentNotice = null;
|
|
3208
|
+
this.verifyingReturn = null;
|
|
3209
|
+
this.confirmedCheckoutId = null;
|
|
2882
3210
|
this.express = false;
|
|
2883
3211
|
this.availableMethods = null;
|
|
3212
|
+
this.paymentMethodsResolved = false;
|
|
3213
|
+
this.autoSelectAttempted = false;
|
|
3214
|
+
this.returningFrom = null;
|
|
3215
|
+
this.shippingBlocked = false;
|
|
2884
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;
|
|
2885
3225
|
}
|
|
2886
3226
|
};
|
|
2887
3227
|
this.boundOnPaymentComplete = (e) => {
|
|
@@ -2963,10 +3303,12 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
2963
3303
|
:host([open]) .modal.as-side { transform: translateX(0); }
|
|
2964
3304
|
|
|
2965
3305
|
/* Desktop: the checkout ALWAYS lives as a right-side panel (like the cart,
|
|
2966
|
-
product detail and express drawer) — never a full-width overlay.
|
|
2967
|
-
|
|
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). */
|
|
2968
3310
|
@media (min-width: 601px) {
|
|
2969
|
-
.modal
|
|
3311
|
+
.modal {
|
|
2970
3312
|
inset: 0 0 0 auto;
|
|
2971
3313
|
width: min(440px, 100%);
|
|
2972
3314
|
max-width: 440px;
|
|
@@ -2974,7 +3316,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
2974
3316
|
border-radius: 0;
|
|
2975
3317
|
box-shadow: -8px 0 24px rgba(0, 0, 0, 0.1);
|
|
2976
3318
|
}
|
|
2977
|
-
:host([open]) .modal
|
|
3319
|
+
:host([open]) .modal { transform: translateX(0); }
|
|
2978
3320
|
}
|
|
2979
3321
|
@media (max-width: 600px) {
|
|
2980
3322
|
/* On phones, keep it a bottom sheet for reachability. */
|
|
@@ -2989,37 +3331,12 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
2989
3331
|
:host([open]) .modal.as-side { transform: translateY(0); }
|
|
2990
3332
|
}
|
|
2991
3333
|
|
|
2992
|
-
/*
|
|
2993
|
-
mobile on desktop). Falls back to a bottom sheet on phones. */
|
|
2994
|
-
.modal.as-confirm {
|
|
2995
|
-
inset: auto;
|
|
2996
|
-
top: 50%;
|
|
2997
|
-
left: 50%;
|
|
2998
|
-
width: min(92vw, 440px);
|
|
2999
|
-
max-width: 440px;
|
|
3000
|
-
height: auto;
|
|
3001
|
-
max-height: 88vh;
|
|
3002
|
-
border-radius: var(--vio-radius-xl, 16px);
|
|
3003
|
-
box-shadow: 0 30px 80px -24px rgba(0, 0, 0, 0.4);
|
|
3004
|
-
transform: translate(-50%, -50%) scale(0.96);
|
|
3005
|
-
opacity: 0;
|
|
3006
|
-
}
|
|
3007
|
-
:host([open]) .modal.as-confirm {
|
|
3008
|
-
transform: translate(-50%, -50%) scale(1);
|
|
3009
|
-
opacity: 1;
|
|
3010
|
-
}
|
|
3334
|
+
/* Phones: the confirmation is a compact bottom sheet, as tall as it needs. */
|
|
3011
3335
|
@media (max-width: 600px) {
|
|
3012
3336
|
.modal.as-confirm {
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
left: 0;
|
|
3016
|
-
width: 100%;
|
|
3017
|
-
max-width: none;
|
|
3018
|
-
border-radius: var(--vio-radius-xl, 16px) var(--vio-radius-xl, 16px) 0 0;
|
|
3019
|
-
transform: translateY(100%) scale(1);
|
|
3020
|
-
opacity: 1;
|
|
3337
|
+
height: auto;
|
|
3338
|
+
max-height: 88vh;
|
|
3021
3339
|
}
|
|
3022
|
-
:host([open]) .modal.as-confirm { transform: translateY(0); }
|
|
3023
3340
|
}
|
|
3024
3341
|
|
|
3025
3342
|
.handle { display: none; }
|
|
@@ -3136,6 +3453,61 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
3136
3453
|
grid-template-columns: 1fr 1fr;
|
|
3137
3454
|
gap: var(--vio-space-md, 16px);
|
|
3138
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
|
+
}
|
|
3139
3511
|
.payment-btn {
|
|
3140
3512
|
padding: 20px;
|
|
3141
3513
|
background: #fff;
|
|
@@ -3172,13 +3544,6 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
3172
3544
|
.payment-btn[aria-pressed='true'] {
|
|
3173
3545
|
border-color: var(--vio-color-accent, #c14a3b);
|
|
3174
3546
|
}
|
|
3175
|
-
.apple-pay-note {
|
|
3176
|
-
grid-column: 1 / -1;
|
|
3177
|
-
font-size: 12px;
|
|
3178
|
-
color: var(--vio-color-text-tertiary, #999);
|
|
3179
|
-
text-align: center;
|
|
3180
|
-
margin-top: 4px;
|
|
3181
|
-
}
|
|
3182
3547
|
/* Klarna Payments — inline widget panel, shown once Klarna is selected. */
|
|
3183
3548
|
.klarna-panel {
|
|
3184
3549
|
grid-column: 1 / -1;
|
|
@@ -3220,6 +3585,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
3220
3585
|
}
|
|
3221
3586
|
/* Shipping selector (express) — Standard / Express options. */
|
|
3222
3587
|
.ship-select { display: flex; flex-direction: column; gap: 8px; }
|
|
3588
|
+
.nexi-ship { margin-bottom: 12px; }
|
|
3223
3589
|
.ship-opt {
|
|
3224
3590
|
display: flex; align-items: center; justify-content: space-between; gap: 12px;
|
|
3225
3591
|
padding: 12px 14px; border: 1px solid var(--vio-color-border, #ddd);
|
|
@@ -3269,37 +3635,86 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
3269
3635
|
|
|
3270
3636
|
/* Order confirmation — compact, shown inside the drawer. */
|
|
3271
3637
|
.confirmation {
|
|
3272
|
-
|
|
3273
|
-
padding: 24px 24px 28px;
|
|
3638
|
+
padding: 8px 0 28px;
|
|
3274
3639
|
display: flex;
|
|
3275
3640
|
flex-direction: column;
|
|
3641
|
+
gap: 20px;
|
|
3642
|
+
}
|
|
3643
|
+
.confirm-head {
|
|
3644
|
+
display: flex;
|
|
3276
3645
|
align-items: center;
|
|
3277
|
-
gap:
|
|
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;
|
|
3278
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; }
|
|
3279
3694
|
.confirm-check {
|
|
3280
|
-
|
|
3281
|
-
|
|
3695
|
+
flex-shrink: 0;
|
|
3696
|
+
width: 48px;
|
|
3697
|
+
height: 48px;
|
|
3282
3698
|
border-radius: 50%;
|
|
3283
3699
|
background: #2e7d32;
|
|
3284
3700
|
color: #fff;
|
|
3285
3701
|
display: flex;
|
|
3286
3702
|
align-items: center;
|
|
3287
3703
|
justify-content: center;
|
|
3288
|
-
font-size:
|
|
3704
|
+
font-size: 24px;
|
|
3289
3705
|
line-height: 1;
|
|
3290
3706
|
}
|
|
3291
3707
|
.confirm-title {
|
|
3292
3708
|
font-family: var(--vio-font-serif, Georgia, serif);
|
|
3293
|
-
font-size:
|
|
3709
|
+
font-size: 24px;
|
|
3294
3710
|
font-weight: 400;
|
|
3295
3711
|
margin: 0;
|
|
3296
3712
|
}
|
|
3297
3713
|
.confirm-text {
|
|
3298
3714
|
font-size: 14px;
|
|
3299
3715
|
color: var(--vio-color-text-secondary, #666);
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
margin: 0;
|
|
3716
|
+
line-height: 1.5;
|
|
3717
|
+
margin: 4px 0 0;
|
|
3303
3718
|
}
|
|
3304
3719
|
.confirm-order-id {
|
|
3305
3720
|
font-size: 13px;
|
|
@@ -3310,17 +3725,13 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
3310
3725
|
font-variant-numeric: tabular-nums;
|
|
3311
3726
|
}
|
|
3312
3727
|
.confirm-summary {
|
|
3313
|
-
width: 100%;
|
|
3314
|
-
max-width: 360px;
|
|
3315
|
-
border-top: 1px solid var(--vio-color-border, #eee);
|
|
3316
|
-
padding-top: 14px;
|
|
3317
3728
|
display: flex;
|
|
3318
3729
|
flex-direction: column;
|
|
3319
|
-
gap:
|
|
3320
|
-
text-align: left;
|
|
3730
|
+
gap: 12px;
|
|
3321
3731
|
font-size: 13px;
|
|
3322
3732
|
}
|
|
3323
3733
|
.confirm-close {
|
|
3734
|
+
width: 100%;
|
|
3324
3735
|
border: none;
|
|
3325
3736
|
background: var(--vio-color-text, #0a0a0a);
|
|
3326
3737
|
color: var(--vio-color-text-on-primary, #fff);
|
|
@@ -3385,11 +3796,13 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
3385
3796
|
}
|
|
3386
3797
|
connectedCallback() {
|
|
3387
3798
|
super.connectedCallback();
|
|
3799
|
+
void this.loadVippsButton();
|
|
3388
3800
|
this.checkoutState = Vio.checkout.currentState;
|
|
3389
3801
|
this.items = Vio.checkout.items;
|
|
3390
3802
|
Vio.checkout.addEventListener("change", this.boundOnCheckoutChange);
|
|
3391
3803
|
Vio.checkout.addEventListener("payment-complete", this.boundOnPaymentComplete);
|
|
3392
3804
|
Vio.checkout.addEventListener("payment-error", this.boundOnPaymentError);
|
|
3805
|
+
Vio.checkout.addEventListener("qliro-event", this.boundOnQliroEvent);
|
|
3393
3806
|
if (this.checkoutState) {
|
|
3394
3807
|
void this.refreshApplePay();
|
|
3395
3808
|
void this.refreshKlarna();
|
|
@@ -3397,6 +3810,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
3397
3810
|
}
|
|
3398
3811
|
void Vio.checkout.resumeKlarnaReturn();
|
|
3399
3812
|
void this.checkReturnPaymentStatus();
|
|
3813
|
+
this.requestUpdate();
|
|
3400
3814
|
}
|
|
3401
3815
|
static {
|
|
3402
3816
|
/**
|
|
@@ -3432,6 +3846,18 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
3432
3846
|
"unpaid"
|
|
3433
3847
|
]);
|
|
3434
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
|
+
}
|
|
3435
3861
|
static {
|
|
3436
3862
|
/** Waits between GetCheckout attempts — PSP webhooks (Vipps especially) can
|
|
3437
3863
|
* land seconds after the shopper is redirected back, so a single immediate
|
|
@@ -3454,6 +3880,10 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
3454
3880
|
* anything else (including an unrecognized state) is treated as failed —
|
|
3455
3881
|
* fail-closed by default rather than guessing.
|
|
3456
3882
|
*/
|
|
3883
|
+
/** Vipps' own button when its script answers; our orange one otherwise. */
|
|
3884
|
+
async loadVippsButton() {
|
|
3885
|
+
this.vippsOfficial = await ensureVippsButton();
|
|
3886
|
+
}
|
|
3457
3887
|
async pollVippsStatus(checkoutId, sponsorId) {
|
|
3458
3888
|
let sawStatus = false;
|
|
3459
3889
|
for (let attempt = 0; attempt < _VioCheckout.VIPPS_STATUS_MAX_ATTEMPTS; attempt++) {
|
|
@@ -3483,12 +3913,12 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
3483
3913
|
this.confirmedOrder = {
|
|
3484
3914
|
items: [],
|
|
3485
3915
|
currency: getGlobalCurrency(),
|
|
3486
|
-
total: 0
|
|
3487
|
-
orderId: checkoutId || void 0
|
|
3916
|
+
total: 0
|
|
3488
3917
|
};
|
|
3489
3918
|
this.confirmedMethod = vioMethod;
|
|
3490
3919
|
this.orderConfirmed = true;
|
|
3491
3920
|
this.open = true;
|
|
3921
|
+
void this.fillOrderNumber(checkoutId, sponsorId);
|
|
3492
3922
|
this.dispatchEvent(
|
|
3493
3923
|
new CustomEvent("vio:payment-success", {
|
|
3494
3924
|
bubbles: true,
|
|
@@ -3503,7 +3933,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
3503
3933
|
this.paymentNotice = "Betalingen behandles fortsatt. Du f\xE5r ordrebekreftelse p\xE5 e-post n\xE5r den er gjennomf\xF8rt. Handlekurven er uendret.";
|
|
3504
3934
|
this.open = true;
|
|
3505
3935
|
} else {
|
|
3506
|
-
this.paymentError =
|
|
3936
|
+
this.paymentError = _VioCheckout.RETURN_UNVERIFIED_MESSAGE;
|
|
3507
3937
|
this.open = true;
|
|
3508
3938
|
}
|
|
3509
3939
|
}
|
|
@@ -3515,9 +3945,33 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
3515
3945
|
const vioMethod = urlParams.get("vio_method") || "stripe";
|
|
3516
3946
|
const sponsorId = Number(urlParams.get("vio_sponsor") || 0);
|
|
3517
3947
|
const checkoutId = urlParams.get("checkout_id") || "";
|
|
3518
|
-
|
|
3519
|
-
const
|
|
3520
|
-
if (
|
|
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";
|
|
3521
3975
|
if (!this.checkoutState) {
|
|
3522
3976
|
const spIdToUse = sponsorId || [...Vio.cart.getAllCarts().keys()][0] || 0;
|
|
3523
3977
|
if (spIdToUse) {
|
|
@@ -3532,49 +3986,35 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
3532
3986
|
} catch {
|
|
3533
3987
|
}
|
|
3534
3988
|
this.open = true;
|
|
3535
|
-
this.
|
|
3989
|
+
this.verifyingReturn = "Bekrefter betalingen\u2026";
|
|
3990
|
+
this.cleanReturnQueryParams([...KUSTOM_RETURN_QUERY_KEYS]);
|
|
3536
3991
|
try {
|
|
3537
3992
|
const order = await Vio.checkout.getKustomOrder(
|
|
3538
3993
|
kustomOrderId,
|
|
3539
3994
|
this.checkoutState?.sponsorId
|
|
3540
3995
|
);
|
|
3541
|
-
this.
|
|
3542
|
-
if (order?.html_snippet) {
|
|
3543
|
-
this.
|
|
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);
|
|
3544
4000
|
await this.updateComplete;
|
|
3545
|
-
const container = this.
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
Vio.checkout.renderKustomSnippet(container, order.html_snippet);
|
|
3550
|
-
}
|
|
3551
|
-
if (order.status === "checkout_complete") {
|
|
3552
|
-
this.dispatchEvent(
|
|
3553
|
-
new CustomEvent("vio:payment-success", {
|
|
3554
|
-
detail: { method: "kustom", orderId: order.order_id },
|
|
3555
|
-
bubbles: true,
|
|
3556
|
-
composed: true
|
|
3557
|
-
})
|
|
3558
|
-
);
|
|
3559
|
-
const spId = this.checkoutState?.sponsorId;
|
|
3560
|
-
if (spId) {
|
|
3561
|
-
try {
|
|
3562
|
-
Vio.cart.clearSponsorCart(spId);
|
|
3563
|
-
} catch {
|
|
3564
|
-
}
|
|
3565
|
-
}
|
|
3566
|
-
}
|
|
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;
|
|
3567
4005
|
}
|
|
3568
4006
|
} catch (err) {
|
|
3569
|
-
this.
|
|
4007
|
+
this.verifyingReturn = null;
|
|
4008
|
+
this.paymentError = _VioCheckout.RETURN_UNVERIFIED_MESSAGE;
|
|
3570
4009
|
if (typeof console !== "undefined") {
|
|
3571
4010
|
console.warn("[VioCheckout] Kustom confirmation read failed:", err);
|
|
3572
4011
|
}
|
|
3573
4012
|
}
|
|
3574
|
-
this.cleanReturnQueryParams(["order_id", "payment_processor"]);
|
|
3575
4013
|
return;
|
|
3576
4014
|
}
|
|
4015
|
+
if (!vioPayment && !checkoutId) return;
|
|
3577
4016
|
if (urlParams.get("payment_processor") === "QLIRO" && checkoutId) {
|
|
4017
|
+
this.returningFrom = "qliro";
|
|
3578
4018
|
if (!this.checkoutState) {
|
|
3579
4019
|
const spIdToUse = sponsorId || [...Vio.cart.getAllCarts().keys()][0] || 0;
|
|
3580
4020
|
if (spIdToUse) {
|
|
@@ -3588,40 +4028,38 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
3588
4028
|
Vio.checkout.selectPaymentMethod("qliro");
|
|
3589
4029
|
} catch {
|
|
3590
4030
|
}
|
|
4031
|
+
this.autoSelectAttempted = true;
|
|
3591
4032
|
this.open = true;
|
|
3592
|
-
this.
|
|
4033
|
+
this.verifyingReturn = "Bekrefter betalingen\u2026";
|
|
3593
4034
|
try {
|
|
3594
4035
|
const order = await Vio.checkout.getQliroOrder(
|
|
3595
4036
|
checkoutId,
|
|
3596
4037
|
this.checkoutState?.sponsorId
|
|
3597
4038
|
);
|
|
3598
|
-
this.
|
|
3599
|
-
if (order?.
|
|
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) {
|
|
3600
4053
|
this.qliroMountedOrderId = order.order_id;
|
|
3601
4054
|
await this.updateComplete;
|
|
3602
4055
|
const container = this.lightContainer("vio-qliro-checkout-container", "vio-qliro");
|
|
3603
4056
|
if (container) {
|
|
3604
4057
|
Vio.checkout.renderKustomSnippet(container, order.html_snippet);
|
|
3605
4058
|
}
|
|
3606
|
-
if (order.status === "Completed") {
|
|
3607
|
-
this.dispatchEvent(
|
|
3608
|
-
new CustomEvent("vio:payment-success", {
|
|
3609
|
-
detail: { method: "qliro", orderId: order.order_id },
|
|
3610
|
-
bubbles: true,
|
|
3611
|
-
composed: true
|
|
3612
|
-
})
|
|
3613
|
-
);
|
|
3614
|
-
const spId = this.checkoutState?.sponsorId;
|
|
3615
|
-
if (spId) {
|
|
3616
|
-
try {
|
|
3617
|
-
Vio.cart.clearSponsorCart(spId);
|
|
3618
|
-
} catch {
|
|
3619
|
-
}
|
|
3620
|
-
}
|
|
3621
|
-
}
|
|
3622
4059
|
}
|
|
3623
4060
|
} catch (err) {
|
|
3624
|
-
this.
|
|
4061
|
+
this.verifyingReturn = null;
|
|
4062
|
+
this.paymentError = _VioCheckout.RETURN_UNVERIFIED_MESSAGE;
|
|
3625
4063
|
if (typeof console !== "undefined") {
|
|
3626
4064
|
console.warn("[VioCheckout] Qliro confirmation read failed:", err);
|
|
3627
4065
|
}
|
|
@@ -3630,6 +4068,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
3630
4068
|
return;
|
|
3631
4069
|
}
|
|
3632
4070
|
if (urlParams.get("payment_processor") === "WALLEY" && checkoutId) {
|
|
4071
|
+
this.returningFrom = "walley";
|
|
3633
4072
|
if (!this.checkoutState) {
|
|
3634
4073
|
const spIdToUse = sponsorId || [...Vio.cart.getAllCarts().keys()][0] || 0;
|
|
3635
4074
|
if (spIdToUse) {
|
|
@@ -3644,13 +4083,13 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
3644
4083
|
} catch {
|
|
3645
4084
|
}
|
|
3646
4085
|
this.open = true;
|
|
3647
|
-
this.
|
|
4086
|
+
this.verifyingReturn = "Bekrefter betalingen\u2026";
|
|
3648
4087
|
try {
|
|
3649
4088
|
const order = await Vio.checkout.getWalleyOrder(
|
|
3650
4089
|
checkoutId,
|
|
3651
4090
|
this.checkoutState?.sponsorId
|
|
3652
4091
|
);
|
|
3653
|
-
this.
|
|
4092
|
+
this.verifyingReturn = null;
|
|
3654
4093
|
if (order?.html_snippet) {
|
|
3655
4094
|
this.walleyMountedOrderId = order.order_id;
|
|
3656
4095
|
await this.updateComplete;
|
|
@@ -3659,24 +4098,14 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
3659
4098
|
Vio.checkout.renderKustomSnippet(container, order.html_snippet);
|
|
3660
4099
|
}
|
|
3661
4100
|
if (order.status === "PurchaseCompleted") {
|
|
3662
|
-
this.
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
bubbles: true,
|
|
3666
|
-
composed: true
|
|
3667
|
-
})
|
|
3668
|
-
);
|
|
3669
|
-
const spId = this.checkoutState?.sponsorId;
|
|
3670
|
-
if (spId) {
|
|
3671
|
-
try {
|
|
3672
|
-
Vio.cart.clearSponsorCart(spId);
|
|
3673
|
-
} catch {
|
|
3674
|
-
}
|
|
3675
|
-
}
|
|
4101
|
+
this.showProviderReceipt("walley", this.checkoutState?.sponsorId ?? 0, {
|
|
4102
|
+
orderId: order.order_id
|
|
4103
|
+
});
|
|
3676
4104
|
}
|
|
3677
4105
|
}
|
|
3678
4106
|
} catch (err) {
|
|
3679
|
-
this.
|
|
4107
|
+
this.verifyingReturn = null;
|
|
4108
|
+
this.paymentError = _VioCheckout.RETURN_UNVERIFIED_MESSAGE;
|
|
3680
4109
|
if (typeof console !== "undefined") {
|
|
3681
4110
|
console.warn("[VioCheckout] Walley confirmation read failed:", err);
|
|
3682
4111
|
}
|
|
@@ -3688,29 +4117,79 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
3688
4117
|
this.cleanReturnQueryParams(["vio_payment", "vio_method", "vio_sponsor", "checkout_id"]);
|
|
3689
4118
|
return;
|
|
3690
4119
|
}
|
|
3691
|
-
if (vioMethod ===
|
|
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;
|
|
3692
4127
|
if (!this.checkoutState) {
|
|
3693
4128
|
const spIdToUse = sponsorId || [...Vio.cart.getAllCarts().keys()][0] || 0;
|
|
3694
4129
|
if (spIdToUse) {
|
|
3695
4130
|
try {
|
|
3696
4131
|
Vio.checkout.open(spIdToUse);
|
|
3697
|
-
Vio.checkout.selectPaymentMethod("
|
|
4132
|
+
Vio.checkout.selectPaymentMethod("adyen");
|
|
3698
4133
|
} catch {
|
|
3699
4134
|
}
|
|
3700
4135
|
}
|
|
3701
4136
|
}
|
|
3702
4137
|
this.open = true;
|
|
3703
|
-
this.
|
|
3704
|
-
|
|
3705
|
-
this.paymentNotice = null;
|
|
3706
|
-
this.applyReturnOutcome(outcome, sawStatus, vioMethod, sponsorId, checkoutId);
|
|
3707
|
-
this.cleanReturnQueryParams(["vio_payment", "vio_method", "vio_sponsor", "checkout_id"]);
|
|
3708
|
-
} else if (vioPayment === "success") {
|
|
3709
|
-
let outcome = "pending";
|
|
4138
|
+
this.verifyingReturn = "Bekrefter betalingen\u2026";
|
|
4139
|
+
let payment = null;
|
|
3710
4140
|
let sawStatus = false;
|
|
3711
|
-
|
|
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) {
|
|
3712
4191
|
this.open = true;
|
|
3713
|
-
this.
|
|
4192
|
+
this.verifyingReturn = "Bekrefter betalingen\u2026";
|
|
3714
4193
|
for (const delayMs of _VioCheckout.RETURN_VERIFY_DELAYS_MS) {
|
|
3715
4194
|
if (delayMs > 0) await new Promise((r) => setTimeout(r, delayMs));
|
|
3716
4195
|
try {
|
|
@@ -3721,7 +4200,11 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
3721
4200
|
outcome = "failed";
|
|
3722
4201
|
break;
|
|
3723
4202
|
}
|
|
3724
|
-
if (_VioCheckout.PAID_STATUSES.has(status) ||
|
|
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)) {
|
|
3725
4208
|
outcome = "paid";
|
|
3726
4209
|
break;
|
|
3727
4210
|
}
|
|
@@ -3731,7 +4214,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
3731
4214
|
}
|
|
3732
4215
|
}
|
|
3733
4216
|
}
|
|
3734
|
-
this.
|
|
4217
|
+
this.verifyingReturn = null;
|
|
3735
4218
|
}
|
|
3736
4219
|
this.applyReturnOutcome(outcome, sawStatus, vioMethod, sponsorId, checkoutId);
|
|
3737
4220
|
this.cleanReturnQueryParams(["vio_payment", "vio_method", "vio_sponsor", "checkout_id"]);
|
|
@@ -3770,17 +4253,44 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
3770
4253
|
Vio.checkout.removeEventListener("change", this.boundOnCheckoutChange);
|
|
3771
4254
|
Vio.checkout.removeEventListener("payment-complete", this.boundOnPaymentComplete);
|
|
3772
4255
|
Vio.checkout.removeEventListener("payment-error", this.boundOnPaymentError);
|
|
4256
|
+
Vio.checkout.removeEventListener("qliro-event", this.boundOnQliroEvent);
|
|
3773
4257
|
this.unmountKlarna();
|
|
4258
|
+
this.unmountNexi();
|
|
4259
|
+
this.unmountAdyen();
|
|
3774
4260
|
super.disconnectedCallback();
|
|
3775
4261
|
}
|
|
3776
4262
|
updated(changed) {
|
|
3777
4263
|
if (changed.has("open") && this.open && this.items.length > 0 && this.availableShippingsList.length === 0) {
|
|
3778
4264
|
void this.loadAvailableShippings();
|
|
3779
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;
|
|
3780
4284
|
void this.mountKlarnaIfNeeded();
|
|
3781
4285
|
void this.mountKustomIfNeeded();
|
|
4286
|
+
this.syncKustomIfStale();
|
|
3782
4287
|
void this.mountQliroIfNeeded();
|
|
3783
4288
|
void this.mountWalleyIfNeeded();
|
|
4289
|
+
void this.mountNexiIfNeeded();
|
|
4290
|
+
this.dropStaleAdyen();
|
|
4291
|
+
void this.mountAdyenIfNeeded();
|
|
4292
|
+
this.dropStaleStripe();
|
|
4293
|
+
void this.mountStripeIfNeeded();
|
|
3784
4294
|
}
|
|
3785
4295
|
close() {
|
|
3786
4296
|
this.open = false;
|
|
@@ -3793,17 +4303,30 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
3793
4303
|
void this.loadAvailableShippings();
|
|
3794
4304
|
}
|
|
3795
4305
|
}
|
|
3796
|
-
/**
|
|
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
|
+
*/
|
|
3797
4312
|
async loadAvailableShippings() {
|
|
3798
4313
|
const spId = this.checkoutState?.sponsorId;
|
|
3799
4314
|
if (!spId) return;
|
|
3800
4315
|
if (this.items.length === 0) return;
|
|
3801
4316
|
if (this.loadingShippings) return;
|
|
4317
|
+
const session = this.embedSession();
|
|
3802
4318
|
this.loadingShippings = true;
|
|
3803
4319
|
this.shippingsAttempted = true;
|
|
3804
4320
|
try {
|
|
3805
4321
|
const shippings = await Vio.checkout.fetchAvailableShippings(spId);
|
|
3806
|
-
if (
|
|
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;
|
|
3807
4330
|
this.availableShippingsList = shippings;
|
|
3808
4331
|
if (!this.selectedShipping || !shippings.some((s) => s.id === this.selectedShipping)) {
|
|
3809
4332
|
const firstOpt = shippings[0];
|
|
@@ -3822,8 +4345,24 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
3822
4345
|
}
|
|
3823
4346
|
} finally {
|
|
3824
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
|
+
}
|
|
3825
4353
|
}
|
|
3826
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
|
+
}
|
|
3827
4366
|
/** Whether all mandatory Leveringsadresse fields are filled. */
|
|
3828
4367
|
get isAddressValid() {
|
|
3829
4368
|
const f = this.form;
|
|
@@ -3845,11 +4384,13 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
3845
4384
|
if (this.loadingPaymentMethods) return;
|
|
3846
4385
|
this.loadingPaymentMethods = true;
|
|
3847
4386
|
try {
|
|
4387
|
+
const spId = this.checkoutState.sponsorId;
|
|
3848
4388
|
const methods = await Vio.checkout.getAvailablePaymentMethods(
|
|
3849
|
-
|
|
4389
|
+
spId
|
|
3850
4390
|
);
|
|
3851
4391
|
if (Array.isArray(methods)) {
|
|
3852
4392
|
this.availableMethods = methods.map((m) => m.name);
|
|
4393
|
+
this.stripeMode = await Vio.checkout.getStripeMode(spId);
|
|
3853
4394
|
} else {
|
|
3854
4395
|
this.availableMethods = null;
|
|
3855
4396
|
}
|
|
@@ -3860,13 +4401,70 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
3860
4401
|
this.availableMethods = null;
|
|
3861
4402
|
} finally {
|
|
3862
4403
|
this.loadingPaymentMethods = false;
|
|
4404
|
+
this.paymentMethodsResolved = true;
|
|
4405
|
+
this.autoSelectSoleMethod();
|
|
3863
4406
|
}
|
|
3864
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
|
+
}
|
|
3865
4448
|
/** Backend names arrive as e.g. "Apple Pay" — compare letters only. */
|
|
3866
|
-
methodEnabled(
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
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"));
|
|
3870
4468
|
}
|
|
3871
4469
|
async refreshApplePay() {
|
|
3872
4470
|
Vio.checkout.clearApplePayCache();
|
|
@@ -3883,10 +4481,12 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
3883
4481
|
async mountKlarnaIfNeeded() {
|
|
3884
4482
|
if (!this.klarnaAvailable || !this.open || !this.checkoutState) return;
|
|
3885
4483
|
if (this.checkoutState.paymentMethod !== "klarna" && !this.express) return;
|
|
4484
|
+
if (!this.express && (!this.isAddressValid || !this.hasSelectedShipping)) return;
|
|
3886
4485
|
if (this.klarnaMounting) return;
|
|
3887
4486
|
if (this.items.length > 0 && this.availableShippingsList.length === 0 && !this.shippingsAttempted) {
|
|
3888
4487
|
await this.loadAvailableShippings();
|
|
3889
4488
|
}
|
|
4489
|
+
if (this.shippingBlocked) return;
|
|
3890
4490
|
const amount = this.checkoutState.subtotal;
|
|
3891
4491
|
if (this.klarnaHandle && this.klarnaMountedAmount === amount && this.klarnaMountedShipping === this.selectedShipping) {
|
|
3892
4492
|
return;
|
|
@@ -3930,7 +4530,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
3930
4530
|
}
|
|
3931
4531
|
this.klarnaMountedAmount = null;
|
|
3932
4532
|
this.klarnaMountedShipping = null;
|
|
3933
|
-
this.klarnaCategories = [];
|
|
4533
|
+
if (this.klarnaCategories.length > 0) this.klarnaCategories = [];
|
|
3934
4534
|
this.klarnaSelectedCat = "";
|
|
3935
4535
|
this.klarnaAuthorizing = false;
|
|
3936
4536
|
}
|
|
@@ -3979,7 +4579,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
3979
4579
|
}
|
|
3980
4580
|
/** Authorize the selected Klarna method → backend creates the order. */
|
|
3981
4581
|
async onKlarnaAuthorize() {
|
|
3982
|
-
if (!this.klarnaHandle || this.klarnaAuthorizing) return;
|
|
4582
|
+
if (!this.klarnaHandle || this.klarnaAuthorizing || this.shippingBlocked) return;
|
|
3983
4583
|
this.paymentError = null;
|
|
3984
4584
|
this.paymentNotice = null;
|
|
3985
4585
|
this.klarnaAuthorizing = true;
|
|
@@ -3997,6 +4597,33 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
3997
4597
|
this.form = { ...this.form, [field]: value };
|
|
3998
4598
|
Vio.checkout.setAddress(this.form);
|
|
3999
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
|
+
}
|
|
4000
4627
|
async onApplePay() {
|
|
4001
4628
|
if (this.applePayInProgress) return;
|
|
4002
4629
|
this.applePayInProgress = true;
|
|
@@ -4011,6 +4638,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
4011
4638
|
}
|
|
4012
4639
|
}
|
|
4013
4640
|
async onPay(method) {
|
|
4641
|
+
if (this.shippingBlocked) return;
|
|
4014
4642
|
this.paymentError = null;
|
|
4015
4643
|
this.paymentNotice = null;
|
|
4016
4644
|
if (method === "apple-pay") {
|
|
@@ -4029,24 +4657,35 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
4029
4657
|
Vio.checkout.selectPaymentMethod("walley");
|
|
4030
4658
|
return;
|
|
4031
4659
|
}
|
|
4032
|
-
if (method === "
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
this.paymentError =
|
|
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;
|
|
4045
4673
|
return;
|
|
4046
4674
|
}
|
|
4047
4675
|
}
|
|
4048
4676
|
Vio.checkout.selectPaymentMethod(method);
|
|
4677
|
+
if (method === "adyen") {
|
|
4678
|
+
this.adyenRequested = true;
|
|
4679
|
+
return;
|
|
4680
|
+
}
|
|
4049
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
|
+
}
|
|
4050
4689
|
const selectedOpt = this.shippingList.find((s) => s.id === this.selectedShipping);
|
|
4051
4690
|
if (selectedOpt?.supplierId) {
|
|
4052
4691
|
try {
|
|
@@ -4069,7 +4708,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
4069
4708
|
}
|
|
4070
4709
|
if (method === "vipps") {
|
|
4071
4710
|
this.vippsLoading = true;
|
|
4072
|
-
void
|
|
4711
|
+
void this.startVippsFromCheckout().catch((err) => {
|
|
4073
4712
|
this.paymentError = `Kunne ikke starte Vipps-betaling: ${err instanceof Error ? err.message : String(err)}`;
|
|
4074
4713
|
}).finally(() => {
|
|
4075
4714
|
this.vippsLoading = false;
|
|
@@ -4084,6 +4723,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
4084
4723
|
onCompleteOrder() {
|
|
4085
4724
|
const s = this.checkoutState;
|
|
4086
4725
|
if (!s || !s.paymentMethod) return;
|
|
4726
|
+
if (this.shippingBlocked) return;
|
|
4087
4727
|
if (s.paymentMethod !== "vipps" && !this.isAddressValid) {
|
|
4088
4728
|
this.paymentError = "Vennligst fyll ut alle feltene i leveringsadresse.";
|
|
4089
4729
|
return;
|
|
@@ -4106,11 +4746,17 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
4106
4746
|
*/
|
|
4107
4747
|
confirmOrder(method, sponsorId, result) {
|
|
4108
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) : "";
|
|
4109
4753
|
this.confirmedOrder = {
|
|
4110
4754
|
items: [...this.items],
|
|
4111
4755
|
currency: this.checkoutState?.currency ?? "",
|
|
4112
4756
|
total: typeof r.chargedTotal === "number" ? r.chargedTotal : this.checkoutState?.subtotal ?? 0,
|
|
4113
|
-
|
|
4757
|
+
...providerOrderId ? { providerRef: { label: `Ordre hos ${this.methodLabel(method)}`, value: providerOrderId } } : {},
|
|
4758
|
+
...shippingPrice > 0 ? { shipping: { name: this.summaryShippingName() || "Frakt", price: shippingPrice } } : {},
|
|
4759
|
+
...email ? { email } : {}
|
|
4114
4760
|
};
|
|
4115
4761
|
this.confirmedMethod = method;
|
|
4116
4762
|
this.orderConfirmed = true;
|
|
@@ -4123,6 +4769,33 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
4123
4769
|
})
|
|
4124
4770
|
);
|
|
4125
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
|
+
}
|
|
4126
4799
|
}
|
|
4127
4800
|
methodLabel(method) {
|
|
4128
4801
|
switch (method) {
|
|
@@ -4136,6 +4809,11 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
4136
4809
|
return "Qliro";
|
|
4137
4810
|
case "walley":
|
|
4138
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";
|
|
4139
4817
|
case "vipps":
|
|
4140
4818
|
return "Vipps";
|
|
4141
4819
|
case "stripe":
|
|
@@ -4148,7 +4826,8 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
4148
4826
|
}
|
|
4149
4827
|
/** True for methods that complete via a separate sheet/widget, not the CTA. */
|
|
4150
4828
|
isExpressMethod(method) {
|
|
4151
|
-
return method === "apple-pay" || method === "klarna" || method
|
|
4829
|
+
return method === "apple-pay" || method === "klarna" || isEmbeddedMethod(method ?? "") || // Adyen's Drop-in brings its own Pay button.
|
|
4830
|
+
isFormFirstWidgetMethod(method ?? "");
|
|
4152
4831
|
}
|
|
4153
4832
|
orderTotal() {
|
|
4154
4833
|
const s = this.checkoutState;
|
|
@@ -4159,11 +4838,33 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
4159
4838
|
* express always has one; the normal flow has one once the backend
|
|
4160
4839
|
* shippings loaded (the choice IS persisted on the cart and charged). */
|
|
4161
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
|
+
}
|
|
4162
4850
|
if (!this.express && this.availableShippingsList.length === 0) return 0;
|
|
4163
4851
|
const o = this.shippingOption;
|
|
4164
4852
|
if (!o) return 0;
|
|
4165
4853
|
return o.priceMajor ?? o.price / 100;
|
|
4166
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
|
+
}
|
|
4167
4868
|
/** Total to charge — items + shipping (express). */
|
|
4168
4869
|
payTotalLabel() {
|
|
4169
4870
|
const s = this.checkoutState;
|
|
@@ -4185,59 +4886,206 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
4185
4886
|
</div>
|
|
4186
4887
|
|
|
4187
4888
|
<div class="body">
|
|
4188
|
-
${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()}
|
|
4189
4890
|
</div>
|
|
4190
4891
|
</div>
|
|
4191
4892
|
`;
|
|
4192
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
|
+
}
|
|
4193
4945
|
renderConfirmation() {
|
|
4194
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]);
|
|
4195
4953
|
return html`
|
|
4196
4954
|
<section class="confirmation">
|
|
4197
|
-
<
|
|
4198
|
-
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
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
|
+
|
|
4203
4971
|
${o && o.items.length > 0 ? html`
|
|
4204
4972
|
<div class="confirm-summary">
|
|
4973
|
+
<div class="confirm-label">Din bestilling</div>
|
|
4205
4974
|
${o.items.map(
|
|
4206
4975
|
(it) => html`
|
|
4207
|
-
<div class="
|
|
4208
|
-
|
|
4209
|
-
<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>
|
|
4210
4986
|
</div>
|
|
4211
4987
|
`
|
|
4212
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
|
+
` : ""}
|
|
4213
4995
|
<div class="order-row total">
|
|
4214
4996
|
<span>Totalt</span><span>${formatPrice(o.total, o.currency)}</span>
|
|
4215
4997
|
</div>
|
|
4216
4998
|
</div>
|
|
4217
4999
|
` : ""}
|
|
4218
|
-
<button class="confirm-close" @click=${this.close}>
|
|
5000
|
+
<button class="confirm-close" @click=${this.close}>Fortsett å handle</button>
|
|
4219
5001
|
</section>
|
|
4220
5002
|
`;
|
|
4221
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
|
+
}
|
|
4222
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
|
+
}
|
|
4223
5061
|
/** Mount the Kustom embedded checkout once it's the chosen method. */
|
|
4224
5062
|
async mountKustomIfNeeded() {
|
|
4225
5063
|
if (!this.open || !this.checkoutState) return;
|
|
4226
5064
|
if (this.checkoutState.paymentMethod !== "kustom") return;
|
|
4227
|
-
if (this.
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
|
|
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();
|
|
4233
5074
|
this.kustomMounting = true;
|
|
5075
|
+
this.kustomNotice = null;
|
|
5076
|
+
this.paymentError = null;
|
|
4234
5077
|
container.innerHTML = "";
|
|
4235
5078
|
try {
|
|
4236
5079
|
const order = await Vio.checkout.mountKustomCheckout(
|
|
4237
5080
|
container,
|
|
4238
|
-
this.checkoutState.sponsorId
|
|
5081
|
+
this.checkoutState.sponsorId,
|
|
5082
|
+
this.kustomHandlers()
|
|
4239
5083
|
);
|
|
4240
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;
|
|
4241
5089
|
} catch (err) {
|
|
4242
5090
|
if (typeof console !== "undefined") {
|
|
4243
5091
|
console.warn("[VioCheckout] Kustom mount failed:", err);
|
|
@@ -4248,18 +5096,69 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
4248
5096
|
this.kustomMounting = false;
|
|
4249
5097
|
}
|
|
4250
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
|
+
}
|
|
4251
5146
|
unmountKustom() {
|
|
4252
5147
|
this.kustomMountedOrderId = null;
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
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();
|
|
4257
5154
|
}
|
|
4258
5155
|
renderKustomPanel() {
|
|
4259
5156
|
return html`
|
|
4260
5157
|
<div class="kustom-panel">
|
|
4261
5158
|
${this.kustomMounting ? html`<div style="font-size:13px;opacity:0.7;padding:8px 0;">Laster Kustom…</div>` : ""}
|
|
4262
|
-
|
|
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>
|
|
4263
5162
|
</div>
|
|
4264
5163
|
`;
|
|
4265
5164
|
}
|
|
@@ -4289,9 +5188,15 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
4289
5188
|
async mountQliroIfNeeded() {
|
|
4290
5189
|
if (!this.open || !this.checkoutState) return;
|
|
4291
5190
|
if (this.checkoutState.paymentMethod !== "qliro") return;
|
|
5191
|
+
if (!this.mayStartEmbeddedPayment()) return;
|
|
4292
5192
|
if (this.qliroMounting) return;
|
|
4293
|
-
|
|
4294
|
-
if (this.qliroMountedOrderId && container.childElementCount > 0)
|
|
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();
|
|
4295
5200
|
this.qliroMounting = true;
|
|
4296
5201
|
container.innerHTML = "";
|
|
4297
5202
|
try {
|
|
@@ -4300,6 +5205,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
4300
5205
|
this.checkoutState.sponsorId
|
|
4301
5206
|
);
|
|
4302
5207
|
this.qliroMountedOrderId = order.order_id;
|
|
5208
|
+
this.qliroMountedFor = mountedFor;
|
|
4303
5209
|
} catch (err) {
|
|
4304
5210
|
if (typeof console !== "undefined") {
|
|
4305
5211
|
console.warn("[VioCheckout] Qliro mount failed:", err);
|
|
@@ -4312,6 +5218,8 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
4312
5218
|
}
|
|
4313
5219
|
unmountQliro() {
|
|
4314
5220
|
this.qliroMountedOrderId = null;
|
|
5221
|
+
this.qliroMountedFor = null;
|
|
5222
|
+
this.qliroShipping = null;
|
|
4315
5223
|
Vio.checkout.destroyQliroListeners();
|
|
4316
5224
|
this.querySelector("#vio-qliro-checkout-container")?.remove();
|
|
4317
5225
|
}
|
|
@@ -4332,9 +5240,15 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
4332
5240
|
async mountWalleyIfNeeded() {
|
|
4333
5241
|
if (!this.open || !this.checkoutState) return;
|
|
4334
5242
|
if (this.checkoutState.paymentMethod !== "walley") return;
|
|
5243
|
+
if (!this.mayStartEmbeddedPayment()) return;
|
|
4335
5244
|
if (this.walleyMounting) return;
|
|
4336
|
-
|
|
4337
|
-
if (this.walleyMountedOrderId && container.childElementCount > 0)
|
|
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();
|
|
4338
5252
|
this.walleyMounting = true;
|
|
4339
5253
|
container.innerHTML = "";
|
|
4340
5254
|
try {
|
|
@@ -4342,23 +5256,13 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
4342
5256
|
container,
|
|
4343
5257
|
this.checkoutState.sponsorId,
|
|
4344
5258
|
() => {
|
|
4345
|
-
this.
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
bubbles: true,
|
|
4349
|
-
composed: true
|
|
4350
|
-
})
|
|
4351
|
-
);
|
|
4352
|
-
const spId = this.checkoutState?.sponsorId;
|
|
4353
|
-
if (spId) {
|
|
4354
|
-
try {
|
|
4355
|
-
Vio.cart.clearSponsorCart(spId);
|
|
4356
|
-
} catch {
|
|
4357
|
-
}
|
|
4358
|
-
}
|
|
5259
|
+
this.showProviderReceipt("walley", this.checkoutState?.sponsorId ?? 0, {
|
|
5260
|
+
orderId: order.order_id
|
|
5261
|
+
});
|
|
4359
5262
|
}
|
|
4360
5263
|
);
|
|
4361
5264
|
this.walleyMountedOrderId = order.order_id;
|
|
5265
|
+
this.walleyMountedFor = mountedFor;
|
|
4362
5266
|
} catch (err) {
|
|
4363
5267
|
if (typeof console !== "undefined") {
|
|
4364
5268
|
console.warn("[VioCheckout] Walley mount failed:", err);
|
|
@@ -4371,6 +5275,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
4371
5275
|
}
|
|
4372
5276
|
unmountWalley() {
|
|
4373
5277
|
this.walleyMountedOrderId = null;
|
|
5278
|
+
this.walleyMountedFor = null;
|
|
4374
5279
|
this.querySelector("#vio-walley-checkout-container")?.remove();
|
|
4375
5280
|
}
|
|
4376
5281
|
renderWalleyPanel() {
|
|
@@ -4381,6 +5286,496 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
4381
5286
|
</div>
|
|
4382
5287
|
`;
|
|
4383
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
|
+
}
|
|
4384
5779
|
renderKlarnaPanel() {
|
|
4385
5780
|
const currency = this.checkoutState?.currency || getGlobalCurrency();
|
|
4386
5781
|
return html`
|
|
@@ -4430,7 +5825,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
4430
5825
|
<button
|
|
4431
5826
|
class="payment-btn primary complete-cta"
|
|
4432
5827
|
@click=${() => void this.onKlarnaAuthorize()}
|
|
4433
|
-
?disabled=${this.klarnaAuthorizing || !this.klarnaHandle || this.klarnaMounting}
|
|
5828
|
+
?disabled=${this.klarnaAuthorizing || !this.klarnaHandle || this.klarnaMounting || !this.express && (!this.isAddressValid || !this.hasSelectedShipping)}
|
|
4434
5829
|
>
|
|
4435
5830
|
${this.klarnaAuthorizing ? "Behandler\u2026" : this.klarnaMounting ? "Klargj\xF8r Klarna\u2026" : `Betal ${this.payTotalLabel()} med Klarna`}
|
|
4436
5831
|
</button>
|
|
@@ -4480,7 +5875,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
4480
5875
|
</div>
|
|
4481
5876
|
</div>
|
|
4482
5877
|
` : ""}
|
|
4483
|
-
${this.renderKlarnaPanel()}
|
|
5878
|
+
${this.shippingBlocked ? this.renderShippingBlocked() : this.renderKlarnaPanel()}
|
|
4484
5879
|
${this.paymentNotice ? html`<div class="payment-notice">${this.paymentNotice}</div>` : ""}
|
|
4485
5880
|
${this.paymentError ? html`<div class="payment-error">${this.paymentError}</div>` : ""}
|
|
4486
5881
|
</section>
|
|
@@ -4490,10 +5885,10 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
4490
5885
|
const method = this.checkoutState?.paymentMethod;
|
|
4491
5886
|
const showCompleteCta = !!method && !this.isExpressMethod(method);
|
|
4492
5887
|
const isVipps = method === "vipps";
|
|
4493
|
-
const
|
|
5888
|
+
const needsOurForm = !!method && !collectsOwnAddress(method);
|
|
4494
5889
|
return html`
|
|
4495
|
-
${
|
|
4496
|
-
<section class="section">
|
|
5890
|
+
${needsOurForm ? html`
|
|
5891
|
+
<section class="section address-section">
|
|
4497
5892
|
<div class="section-label">Steg 1</div>
|
|
4498
5893
|
<h3 class="section-heading">Leveringsadresse</h3>
|
|
4499
5894
|
<div class="form-row">
|
|
@@ -4609,32 +6004,41 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
4609
6004
|
` : ""}
|
|
4610
6005
|
|
|
4611
6006
|
<section class="section">
|
|
4612
|
-
<div class="section-label"
|
|
4613
|
-
|
|
6007
|
+
<div class="section-label">
|
|
6008
|
+
${needsOurForm ? "Steg 2" : "Betaling"}
|
|
6009
|
+
</div>
|
|
6010
|
+
${this.shippingBlocked ? this.renderShippingBlocked() : method ? html`
|
|
4614
6011
|
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px;">
|
|
4615
6012
|
<h3 class="section-heading" style="margin:0;">
|
|
4616
6013
|
Betalingsmåte: <b>${this.methodLabel(method)}</b>
|
|
4617
6014
|
</h3>
|
|
4618
|
-
|
|
4619
|
-
|
|
4620
|
-
|
|
4621
|
-
|
|
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=${() => {
|
|
4622
6020
|
this.unmountKlarna();
|
|
4623
6021
|
this.unmountKustom();
|
|
4624
6022
|
this.unmountQliro();
|
|
4625
6023
|
this.unmountWalley();
|
|
6024
|
+
this.unmountNexi();
|
|
6025
|
+
this.unmountAdyen();
|
|
4626
6026
|
Vio.checkout.selectPaymentMethod("");
|
|
4627
6027
|
}}
|
|
4628
|
-
|
|
4629
|
-
|
|
4630
|
-
|
|
6028
|
+
>
|
|
6029
|
+
Endre
|
|
6030
|
+
</button>
|
|
6031
|
+
` : ""}
|
|
4631
6032
|
</div>
|
|
4632
6033
|
|
|
4633
6034
|
${method === "klarna" ? this.renderKlarnaPanel() : ""}
|
|
4634
6035
|
${method === "kustom" ? this.renderKustomPanel() : ""}
|
|
4635
6036
|
${method === "qliro" ? this.renderQliroPanel() : ""}
|
|
4636
6037
|
${method === "walley" ? this.renderWalleyPanel() : ""}
|
|
4637
|
-
${method === "
|
|
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`
|
|
4638
6042
|
<button
|
|
4639
6043
|
class="payment-btn primary complete-cta"
|
|
4640
6044
|
@click=${() => void this.onPay("stripe")}
|
|
@@ -4673,19 +6077,25 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
4673
6077
|
Betal ${this.payTotalLabel()} med ${this.methodLabel(method ?? null)}
|
|
4674
6078
|
</button>
|
|
4675
6079
|
` : ""}
|
|
4676
|
-
${
|
|
6080
|
+
${needsOurForm && !this.isAddressValid ? html`
|
|
4677
6081
|
<div style="font-size: 12px; color: var(--vio-color-accent, #c14a3b); text-align: center; margin-top: 8px;">
|
|
4678
6082
|
Vennligst fyll ut leveringsadresse for å fullføre betalingen
|
|
4679
6083
|
</div>
|
|
4680
|
-
` :
|
|
6084
|
+
` : needsOurForm && !this.hasSelectedShipping ? html`
|
|
4681
6085
|
<div style="font-size: 12px; color: var(--vio-color-accent, #c14a3b); text-align: center; margin-top: 8px;">
|
|
4682
6086
|
Vennligst velg en fraktmetode for å fullføre betalingen
|
|
4683
6087
|
</div>
|
|
4684
6088
|
` : ""}
|
|
4685
|
-
` : 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`
|
|
4686
6096
|
<h3 class="section-heading">Velg betalingsmåte</h3>
|
|
4687
6097
|
<div class="payment-grid">
|
|
4688
|
-
${this.methodEnabled("apple-pay") && this.applePayAvailable ? html`
|
|
6098
|
+
${this.methodEnabled("apple-pay") && this.applePayAvailable && !this.stripeOwnsWallets() ? html`
|
|
4689
6099
|
<button
|
|
4690
6100
|
class="payment-btn primary"
|
|
4691
6101
|
@click=${() => this.onPay("apple-pay")}
|
|
@@ -4723,15 +6133,26 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
4723
6133
|
<span style="font-weight:800;font-size:16px;letter-spacing:-0.01em">Walley</span>
|
|
4724
6134
|
</button>
|
|
4725
6135
|
` : ""}
|
|
4726
|
-
${this.methodEnabled("
|
|
4727
|
-
<button class="payment-btn" @click=${() => this.onPay("
|
|
4728
|
-
<span style="
|
|
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>
|
|
4729
6139
|
</button>
|
|
4730
6140
|
` : ""}
|
|
4731
|
-
${this.methodEnabled("
|
|
4732
|
-
<
|
|
4733
|
-
|
|
4734
|
-
</
|
|
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>
|
|
4735
6156
|
` : ""}
|
|
4736
6157
|
</div>
|
|
4737
6158
|
`}
|
|
@@ -4762,7 +6183,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
4762
6183
|
<span>Sum</span><span>${this.orderTotal()}</span>
|
|
4763
6184
|
</div>
|
|
4764
6185
|
<div class="order-row">
|
|
4765
|
-
<span>Frakt${this.shippingMajor() > 0 && this.
|
|
6186
|
+
<span>Frakt${this.shippingMajor() > 0 && this.summaryShippingName() ? ` \u2013 ${this.summaryShippingName()}` : ""}</span>
|
|
4766
6187
|
<span>${this.shippingMajor() > 0 ? formatPrice(this.shippingMajor(), this.checkoutState?.currency) : this.shippingLabel}</span>
|
|
4767
6188
|
</div>
|
|
4768
6189
|
<div class="order-row total">
|
|
@@ -4805,6 +6226,9 @@ __decorateClass([
|
|
|
4805
6226
|
__decorateClass([
|
|
4806
6227
|
state()
|
|
4807
6228
|
], _VioCheckout.prototype, "orderConfirmed", 2);
|
|
6229
|
+
__decorateClass([
|
|
6230
|
+
state()
|
|
6231
|
+
], _VioCheckout.prototype, "providerReceipt", 2);
|
|
4808
6232
|
__decorateClass([
|
|
4809
6233
|
state()
|
|
4810
6234
|
], _VioCheckout.prototype, "confirmedMethod", 2);
|
|
@@ -4814,6 +6238,9 @@ __decorateClass([
|
|
|
4814
6238
|
__decorateClass([
|
|
4815
6239
|
state()
|
|
4816
6240
|
], _VioCheckout.prototype, "paymentNotice", 2);
|
|
6241
|
+
__decorateClass([
|
|
6242
|
+
state()
|
|
6243
|
+
], _VioCheckout.prototype, "verifyingReturn", 2);
|
|
4817
6244
|
__decorateClass([
|
|
4818
6245
|
state()
|
|
4819
6246
|
], _VioCheckout.prototype, "confirmedOrder", 2);
|
|
@@ -4835,15 +6262,84 @@ __decorateClass([
|
|
|
4835
6262
|
__decorateClass([
|
|
4836
6263
|
state()
|
|
4837
6264
|
], _VioCheckout.prototype, "availableMethods", 2);
|
|
6265
|
+
__decorateClass([
|
|
6266
|
+
state()
|
|
6267
|
+
], _VioCheckout.prototype, "vippsOfficial", 2);
|
|
4838
6268
|
__decorateClass([
|
|
4839
6269
|
state()
|
|
4840
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);
|
|
4841
6283
|
__decorateClass([
|
|
4842
6284
|
state()
|
|
4843
6285
|
], _VioCheckout.prototype, "qliroMounting", 2);
|
|
4844
6286
|
__decorateClass([
|
|
4845
6287
|
state()
|
|
4846
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);
|
|
4847
6343
|
__decorateClass([
|
|
4848
6344
|
state()
|
|
4849
6345
|
], _VioCheckout.prototype, "stripeLoading", 2);
|
|
@@ -4853,6 +6349,15 @@ __decorateClass([
|
|
|
4853
6349
|
__decorateClass([
|
|
4854
6350
|
state()
|
|
4855
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);
|
|
4856
6361
|
var VioCheckout = _VioCheckout;
|
|
4857
6362
|
|
|
4858
6363
|
// src/ui/elements.ts
|
|
@@ -4872,5 +6377,5 @@ function registerVioElements() {
|
|
|
4872
6377
|
}
|
|
4873
6378
|
|
|
4874
6379
|
export { VioCart, VioCheckout, VioProduct, VioProductCarousel, VioProductDetail, VioTokens, applyVioTheme, registerVioElements };
|
|
4875
|
-
//# sourceMappingURL=chunk-
|
|
4876
|
-
//# sourceMappingURL=chunk-
|
|
6380
|
+
//# sourceMappingURL=chunk-NUNGU6LL.js.map
|
|
6381
|
+
//# sourceMappingURL=chunk-NUNGU6LL.js.map
|