@vio-live/web-sdk 0.2.0 → 0.3.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/dist/chunk-NP2KCM7X.js +3859 -0
- package/dist/chunk-NP2KCM7X.js.map +1 -0
- package/dist/{chunk-7JSSYPRN.js → chunk-NWORYWIL.js} +1266 -168
- package/dist/chunk-NWORYWIL.js.map +1 -0
- package/dist/chunk-PBQSHIID.js +3 -0
- package/dist/{chunk-GGM2AIK5.js.map → chunk-PBQSHIID.js.map} +1 -1
- package/dist/core/index.d.ts +314 -12
- 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 +1 -1
- package/dist/react/index.js +2 -2
- package/dist/ui/index.d.ts +1 -1
- package/dist/ui/index.js +3 -3
- package/dist/{vio-checkout-CrC5tvmh.d.ts → vio-checkout-T9R5hpDx.d.ts} +137 -9
- package/package.json +1 -1
- package/dist/chunk-7JSSYPRN.js.map +0 -1
- package/dist/chunk-GGM2AIK5.js +0 -3
- package/dist/chunk-SOO2G6CF.js +0 -1618
- package/dist/chunk-SOO2G6CF.js.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Vio, __decorateClass, primaryImageUrl, displayPrice, formatPrice, KLARNA_SHIPPING_OPTIONS } from './chunk-
|
|
1
|
+
import { Vio, __decorateClass, primaryImageUrl, displayPrice, getGlobalCurrency, formatPrice, KLARNA_SHIPPING_OPTIONS } from './chunk-NP2KCM7X.js';
|
|
2
2
|
import { LitElement, css, html } from 'lit';
|
|
3
3
|
import { property, state, query } from 'lit/decorators.js';
|
|
4
4
|
|
|
@@ -13,6 +13,9 @@ var VioProduct = class extends LitElement {
|
|
|
13
13
|
this.productId = "";
|
|
14
14
|
this.sponsorId = "";
|
|
15
15
|
this.hasVariants = false;
|
|
16
|
+
this.variants = void 0;
|
|
17
|
+
this.options = void 0;
|
|
18
|
+
this.hideMeta = false;
|
|
16
19
|
this.inCart = false;
|
|
17
20
|
this.cartQty = 0;
|
|
18
21
|
this.justAdded = false;
|
|
@@ -185,7 +188,7 @@ var VioProduct = class extends LitElement {
|
|
|
185
188
|
* `vio:product-add` so the host adds it (bumping quantity on repeat taps). */
|
|
186
189
|
onAdd(e) {
|
|
187
190
|
e.stopPropagation();
|
|
188
|
-
if (this.hasVariants) {
|
|
191
|
+
if (this.hasVariants || Array.isArray(this.variants) && this.variants.length > 1 || Array.isArray(this.options) && this.options.length > 0) {
|
|
189
192
|
this.onClick();
|
|
190
193
|
return;
|
|
191
194
|
}
|
|
@@ -236,12 +239,13 @@ var VioProduct = class extends LitElement {
|
|
|
236
239
|
</svg>`}
|
|
237
240
|
</button>
|
|
238
241
|
</div>
|
|
242
|
+
${this.hideMeta ? "" : html`
|
|
239
243
|
<div class="meta">
|
|
240
244
|
${this.brand ? html`<div class="brand">${this.brand}</div>` : ""}
|
|
241
245
|
${this.name ? html`<div class="name">${this.name}</div>` : ""}
|
|
242
246
|
${this.price ? html`<div class="price">${this.price}</div>` : ""}
|
|
243
247
|
${this.retailer ? html`<div class="retailer">${this.retailer}</div>` : ""}
|
|
244
|
-
</div
|
|
248
|
+
</div>`}
|
|
245
249
|
</div>
|
|
246
250
|
`;
|
|
247
251
|
}
|
|
@@ -270,6 +274,15 @@ __decorateClass([
|
|
|
270
274
|
__decorateClass([
|
|
271
275
|
property({ type: Boolean, attribute: "has-variants" })
|
|
272
276
|
], VioProduct.prototype, "hasVariants", 2);
|
|
277
|
+
__decorateClass([
|
|
278
|
+
property({ attribute: false })
|
|
279
|
+
], VioProduct.prototype, "variants", 2);
|
|
280
|
+
__decorateClass([
|
|
281
|
+
property({ attribute: false })
|
|
282
|
+
], VioProduct.prototype, "options", 2);
|
|
283
|
+
__decorateClass([
|
|
284
|
+
property({ type: Boolean, attribute: "hide-meta" })
|
|
285
|
+
], VioProduct.prototype, "hideMeta", 2);
|
|
273
286
|
__decorateClass([
|
|
274
287
|
state()
|
|
275
288
|
], VioProduct.prototype, "inCart", 2);
|
|
@@ -625,6 +638,9 @@ function sponsorNameFromBootstrap(sponsorId) {
|
|
|
625
638
|
if (boot.primarySponsor?.id === sponsorId) return boot.primarySponsor.name;
|
|
626
639
|
return boot.secondarySponsors?.find((s) => s.id === sponsorId)?.name;
|
|
627
640
|
}
|
|
641
|
+
var STRIPE_LOGO_SRC = "data:image/svg+xml;utf8," + encodeURIComponent(
|
|
642
|
+
'<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>'
|
|
643
|
+
);
|
|
628
644
|
var VioProductDetail = class extends LitElement {
|
|
629
645
|
constructor() {
|
|
630
646
|
super(...arguments);
|
|
@@ -641,6 +657,12 @@ var VioProductDetail = class extends LitElement {
|
|
|
641
657
|
this.activeImageIndex = 0;
|
|
642
658
|
this.adding = false;
|
|
643
659
|
this.applePayOk = false;
|
|
660
|
+
this.availableMethods = null;
|
|
661
|
+
/** Race guard: only the latest fetch may write state (two quick taps on
|
|
662
|
+
* different cards would otherwise let the slow response win). */
|
|
663
|
+
this.fetchSeq = 0;
|
|
664
|
+
/** Memoised option maps per variant object (recomputed per product). */
|
|
665
|
+
this.optMapCache = /* @__PURE__ */ new WeakMap();
|
|
644
666
|
/** Pre-prepared Apple Pay handle — show() fires synchronously on tap. */
|
|
645
667
|
this.applePayHandle = null;
|
|
646
668
|
this.boundProductClick = (e) => {
|
|
@@ -652,6 +674,7 @@ var VioProductDetail = class extends LitElement {
|
|
|
652
674
|
if (Number.isNaN(sponsorIdNum) || sponsorIdNum <= 0) return;
|
|
653
675
|
this.productId = ev.detail.productId;
|
|
654
676
|
this.sponsorId = sponsorIdNum;
|
|
677
|
+
this.currency = ev.detail.currency || getGlobalCurrency();
|
|
655
678
|
this.show();
|
|
656
679
|
void this.fetchProduct();
|
|
657
680
|
};
|
|
@@ -890,6 +913,13 @@ var VioProductDetail = class extends LitElement {
|
|
|
890
913
|
background: var(--vio-color-text, #0a0a0a);
|
|
891
914
|
color: var(--vio-color-text-on-primary, #fff);
|
|
892
915
|
}
|
|
916
|
+
.option-pill.unavailable {
|
|
917
|
+
opacity: 0.4;
|
|
918
|
+
cursor: not-allowed;
|
|
919
|
+
text-decoration: line-through;
|
|
920
|
+
background: repeating-linear-gradient(-45deg, #fafafa, #fafafa 5px, #f0f0f0 5px, #f0f0f0 10px);
|
|
921
|
+
}
|
|
922
|
+
.option-pill.unavailable:hover { border-color: var(--vio-color-border, #e5e5e5); }
|
|
893
923
|
|
|
894
924
|
/* Qty + CTA row */
|
|
895
925
|
.qty-cta-row {
|
|
@@ -960,6 +990,35 @@ var VioProductDetail = class extends LitElement {
|
|
|
960
990
|
.buy-klarna .klarna-badge { height: 22px; width: auto; display: block; }
|
|
961
991
|
.buy-klarna:hover:not(:disabled) { background: #ffa0b9; }
|
|
962
992
|
.buy-klarna:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
993
|
+
.buy-applepay .ap-logo { width: 20px; height: 20px; }
|
|
994
|
+
/* Vipps — brand orange buy button. */
|
|
995
|
+
.buy-vipps {
|
|
996
|
+
width: 100%;
|
|
997
|
+
margin-top: 10px;
|
|
998
|
+
display: flex;
|
|
999
|
+
align-items: center;
|
|
1000
|
+
justify-content: center;
|
|
1001
|
+
gap: 8px;
|
|
1002
|
+
background: #ff5b24;
|
|
1003
|
+
color: #fff;
|
|
1004
|
+
border: none;
|
|
1005
|
+
padding: 14px 24px;
|
|
1006
|
+
font-size: 12px;
|
|
1007
|
+
text-transform: uppercase;
|
|
1008
|
+
letter-spacing: var(--vio-tracking-label, 0.1em);
|
|
1009
|
+
font-weight: 700;
|
|
1010
|
+
cursor: pointer;
|
|
1011
|
+
font-family: inherit;
|
|
1012
|
+
transition: background 0.15s;
|
|
1013
|
+
}
|
|
1014
|
+
.buy-vipps .vipps-badge {
|
|
1015
|
+
font-size: 20px;
|
|
1016
|
+
font-weight: 800;
|
|
1017
|
+
letter-spacing: -0.02em;
|
|
1018
|
+
text-transform: none;
|
|
1019
|
+
}
|
|
1020
|
+
.buy-vipps:hover:not(:disabled) { background: #ec4f1c; }
|
|
1021
|
+
.buy-vipps:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
963
1022
|
/* Apple Pay express button — black, per Apple's button guidelines. Only
|
|
964
1023
|
shown in Safari (canApplePay). The glyph is the Apple logo (Safari). */
|
|
965
1024
|
.buy-applepay {
|
|
@@ -1071,49 +1130,262 @@ var VioProductDetail = class extends LitElement {
|
|
|
1071
1130
|
this.product = null;
|
|
1072
1131
|
return;
|
|
1073
1132
|
}
|
|
1133
|
+
const seq = ++this.fetchSeq;
|
|
1074
1134
|
this.isLoading = true;
|
|
1075
1135
|
this.fetchError = "";
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
const values = normalizeValues(opt.values);
|
|
1088
|
-
if (values.length > 0) initial[opt.name] = values[0];
|
|
1089
|
-
}
|
|
1090
|
-
this.selectedOptions = initial;
|
|
1091
|
-
void Vio.checkout.prepareApplePayFor(this.unitPrice * this.quantity, this.currency).then((handle) => {
|
|
1092
|
-
this.applePayHandle = handle;
|
|
1093
|
-
this.applePayOk = handle.available;
|
|
1094
|
-
}).catch(() => {
|
|
1095
|
-
this.applePayHandle = null;
|
|
1096
|
-
this.applePayOk = false;
|
|
1136
|
+
let products = null;
|
|
1137
|
+
let lastError = null;
|
|
1138
|
+
for (let attempt = 0; attempt < 4; attempt++) {
|
|
1139
|
+
try {
|
|
1140
|
+
const activeCurrency = getGlobalCurrency() || this.currency;
|
|
1141
|
+
this.currency = activeCurrency;
|
|
1142
|
+
const commerce = Vio.commerceFor(this.sponsorId);
|
|
1143
|
+
products = await commerce.channel.product.getByIds({
|
|
1144
|
+
product_ids: [productIdNum],
|
|
1145
|
+
currency: activeCurrency,
|
|
1146
|
+
image_size: "large"
|
|
1097
1147
|
});
|
|
1148
|
+
lastError = null;
|
|
1149
|
+
break;
|
|
1150
|
+
} catch (err) {
|
|
1151
|
+
lastError = err;
|
|
1152
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
1098
1153
|
}
|
|
1099
|
-
}
|
|
1100
|
-
|
|
1101
|
-
|
|
1154
|
+
}
|
|
1155
|
+
if (seq !== this.fetchSeq) return;
|
|
1156
|
+
if (lastError) {
|
|
1157
|
+
this.fetchError = lastError instanceof Error ? lastError.message : String(lastError);
|
|
1158
|
+
console.warn("[VioDetail] fetch failed after retries \u2192", this.fetchError);
|
|
1102
1159
|
this.isLoading = false;
|
|
1160
|
+
return;
|
|
1161
|
+
}
|
|
1162
|
+
this.optMapCache = /* @__PURE__ */ new WeakMap();
|
|
1163
|
+
this.product = products?.[0] ?? null;
|
|
1164
|
+
if (!this.product) {
|
|
1165
|
+
this.fetchError = "Fant ikke produktet.";
|
|
1166
|
+
}
|
|
1167
|
+
if (this.product) {
|
|
1168
|
+
const initial = {};
|
|
1169
|
+
for (const opt of this.product.options ?? []) {
|
|
1170
|
+
const values = normalizeValues(opt.values);
|
|
1171
|
+
if (values.length > 0) initial[opt.name] = values[0];
|
|
1172
|
+
}
|
|
1173
|
+
this.selectedOptions = initial;
|
|
1174
|
+
void Vio.checkout.prepareApplePayFor(this.unitPrice * this.quantity, this.currency).then((handle) => {
|
|
1175
|
+
this.applePayHandle = handle;
|
|
1176
|
+
this.applePayOk = handle.available;
|
|
1177
|
+
}).catch(() => {
|
|
1178
|
+
this.applePayHandle = null;
|
|
1179
|
+
this.applePayOk = false;
|
|
1180
|
+
});
|
|
1181
|
+
void this.loadAvailablePaymentMethods();
|
|
1182
|
+
}
|
|
1183
|
+
this.isLoading = false;
|
|
1184
|
+
}
|
|
1185
|
+
/** Which buy buttons to render — backend-configured per sponsor. */
|
|
1186
|
+
async loadAvailablePaymentMethods() {
|
|
1187
|
+
const spId = this.sponsorId || globalThis.__VIO_SPONSOR_ID__;
|
|
1188
|
+
if (!spId) return;
|
|
1189
|
+
try {
|
|
1190
|
+
const methods = await Vio.checkout.getAvailablePaymentMethods(spId);
|
|
1191
|
+
if (Array.isArray(methods)) {
|
|
1192
|
+
this.availableMethods = methods.map((m) => m.name);
|
|
1193
|
+
} else {
|
|
1194
|
+
this.availableMethods = null;
|
|
1195
|
+
}
|
|
1196
|
+
} catch (err) {
|
|
1197
|
+
if (typeof console !== "undefined") {
|
|
1198
|
+
console.warn("[VioProductDetail] Failed to load payment methods:", err);
|
|
1199
|
+
}
|
|
1200
|
+
this.availableMethods = null;
|
|
1103
1201
|
}
|
|
1104
1202
|
}
|
|
1105
|
-
/**
|
|
1203
|
+
/** Backend names arrive as e.g. "Apple Pay" — compare letters only. */
|
|
1204
|
+
methodEnabled(...names) {
|
|
1205
|
+
if (this.availableMethods === null) return true;
|
|
1206
|
+
const norm = (s) => s.toLowerCase().replace(/[^a-z]/g, "");
|
|
1207
|
+
return this.availableMethods.some((m) => names.some((n) => norm(m) === norm(n)));
|
|
1208
|
+
}
|
|
1209
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
1210
|
+
/**
|
|
1211
|
+
* Build an option-name → value map for a variant, from every shape the feeds
|
|
1212
|
+
* ship: option1/2/3 columns, options/selected_options/variant arrays, plain
|
|
1213
|
+
* object maps, and finally the title (value scan first, position fallback).
|
|
1214
|
+
*/
|
|
1215
|
+
getVariantOptionMap(v, productOptions) {
|
|
1216
|
+
if (v && typeof v === "object") {
|
|
1217
|
+
const cached = this.optMapCache.get(v);
|
|
1218
|
+
if (cached) return cached;
|
|
1219
|
+
}
|
|
1220
|
+
const map = {};
|
|
1221
|
+
if (!v) return { map, titleParts: [] };
|
|
1222
|
+
if (v.option1 != null && productOptions?.[0]) {
|
|
1223
|
+
map[productOptions[0].name.toLowerCase()] = String(v.option1).trim().toLowerCase();
|
|
1224
|
+
}
|
|
1225
|
+
if (v.option2 != null && productOptions?.[1]) {
|
|
1226
|
+
map[productOptions[1].name.toLowerCase()] = String(v.option2).trim().toLowerCase();
|
|
1227
|
+
}
|
|
1228
|
+
if (v.option3 != null && productOptions?.[2]) {
|
|
1229
|
+
map[productOptions[2].name.toLowerCase()] = String(v.option3).trim().toLowerCase();
|
|
1230
|
+
}
|
|
1231
|
+
const optArray = Array.isArray(v.options) ? v.options : Array.isArray(v.selected_options) ? v.selected_options : Array.isArray(v.variant) ? v.variant : null;
|
|
1232
|
+
if (optArray) {
|
|
1233
|
+
for (const item of optArray) {
|
|
1234
|
+
const key = (item.name || item.option || "").trim().toLowerCase();
|
|
1235
|
+
const val = (item.value || "").trim().toLowerCase();
|
|
1236
|
+
if (key && val) map[key] = val;
|
|
1237
|
+
}
|
|
1238
|
+
} else if (v.options && typeof v.options === "object") {
|
|
1239
|
+
for (const [k, val] of Object.entries(v.options)) {
|
|
1240
|
+
map[k.trim().toLowerCase()] = String(val).trim().toLowerCase();
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
const title = String(v.title ?? "").trim();
|
|
1244
|
+
const titleLower = title.toLowerCase();
|
|
1245
|
+
if (Array.isArray(productOptions)) {
|
|
1246
|
+
for (const opt of productOptions) {
|
|
1247
|
+
const key = opt.name.toLowerCase();
|
|
1248
|
+
if (!map[key]) {
|
|
1249
|
+
const rawValues = normalizeValues(opt.values).filter(
|
|
1250
|
+
(val) => !productOptions.some(
|
|
1251
|
+
(other) => other !== opt && normalizeValues(other.values).some(
|
|
1252
|
+
(ov) => ov.trim().toLowerCase() === val.trim().toLowerCase()
|
|
1253
|
+
)
|
|
1254
|
+
)
|
|
1255
|
+
);
|
|
1256
|
+
const sortedVals = [...rawValues].sort((a, b) => b.length - a.length);
|
|
1257
|
+
for (const valStr of sortedVals) {
|
|
1258
|
+
const valLower = valStr.trim().toLowerCase();
|
|
1259
|
+
const escaped = valLower.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1260
|
+
const regex = new RegExp(`(?:^|[/|\\-,;\\s])${escaped}(?:$|[/|\\-,;\\s])`, "i");
|
|
1261
|
+
if (regex.test(titleLower) || titleLower === valLower) {
|
|
1262
|
+
map[key] = valLower;
|
|
1263
|
+
break;
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
const titleParts = title.split(/\s*[/|,]\s*|\s+-\s+/).map((s) => s.trim()).filter(Boolean);
|
|
1270
|
+
if (Array.isArray(productOptions)) {
|
|
1271
|
+
productOptions.forEach((opt, idx) => {
|
|
1272
|
+
const key = opt.name.toLowerCase();
|
|
1273
|
+
if (!map[key] && titleParts[idx] != null) {
|
|
1274
|
+
map[key] = titleParts[idx].toLowerCase();
|
|
1275
|
+
}
|
|
1276
|
+
});
|
|
1277
|
+
}
|
|
1278
|
+
const result = { map, titleParts };
|
|
1279
|
+
if (v && typeof v === "object") this.optMapCache.set(v, result);
|
|
1280
|
+
return result;
|
|
1281
|
+
}
|
|
1282
|
+
/** Option values a variant represents (map values ∪ title parts). */
|
|
1283
|
+
variantOptionValues(v) {
|
|
1284
|
+
const { map, titleParts } = this.getVariantOptionMap(v, this.product?.options);
|
|
1285
|
+
const set = new Set(titleParts.map((s) => s.toLowerCase()));
|
|
1286
|
+
for (const val of Object.values(map)) set.add(val);
|
|
1287
|
+
return Array.from(set);
|
|
1288
|
+
}
|
|
1289
|
+
/** Does a variant part represent this option value? Exact match, plus a
|
|
1290
|
+
* whole-token match ONLY for numeric values ("30" matches "30 ml") — word
|
|
1291
|
+
* subsets of text values are not matches ("Large" must NOT match
|
|
1292
|
+
* "Extra Large"). */
|
|
1293
|
+
partMatchesValue(part, value) {
|
|
1294
|
+
if (!part || !value) return false;
|
|
1295
|
+
const p = String(part).trim().toLowerCase();
|
|
1296
|
+
const v = String(value).trim().toLowerCase();
|
|
1297
|
+
if (p === v) return true;
|
|
1298
|
+
if (!/^[\d.,]+$/.test(v)) return false;
|
|
1299
|
+
return p.split(/\s+/).includes(v);
|
|
1300
|
+
}
|
|
1301
|
+
/** Does a variant satisfy every currently-selected option value? */
|
|
1302
|
+
variantMatchesSelection(v, selection) {
|
|
1303
|
+
if (!v || !selection) return false;
|
|
1304
|
+
const entries = Object.entries(selection);
|
|
1305
|
+
if (entries.length === 0) return true;
|
|
1306
|
+
const { map, titleParts } = this.getVariantOptionMap(v, this.product?.options);
|
|
1307
|
+
const titlePartsLower = titleParts.map((t) => t.toLowerCase());
|
|
1308
|
+
return entries.every(([optName, val]) => {
|
|
1309
|
+
const key = optName.trim().toLowerCase();
|
|
1310
|
+
const targetVal = String(val).trim().toLowerCase();
|
|
1311
|
+
if (map[key]) {
|
|
1312
|
+
return this.partMatchesValue(map[key], targetVal);
|
|
1313
|
+
}
|
|
1314
|
+
return titlePartsLower.some((part) => this.partMatchesValue(part, targetVal));
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
/** Sellable quantity for a variant, across the stock field zoo. Boolean
|
|
1318
|
+
* availability flags win; a numeric field wins next; `null` = the feed does
|
|
1319
|
+
* not report stock for this variant (callers fall back to product stock —
|
|
1320
|
+
* never assume "in stock" from absent data). */
|
|
1321
|
+
getVariantQty(v) {
|
|
1322
|
+
if (!v) return 0;
|
|
1323
|
+
if (v.available === false || v.in_stock === false || v.is_available === false) return 0;
|
|
1324
|
+
if (v.available === true || v.in_stock === true || v.is_available === true) return 999;
|
|
1325
|
+
const rawQty = v.quantity ?? v.inventory_quantity ?? v.stock ?? v.available_quantity ?? v.stock_quantity;
|
|
1326
|
+
if (rawQty !== null && rawQty !== void 0 && rawQty !== "") {
|
|
1327
|
+
const parsed = Number(rawQty);
|
|
1328
|
+
if (!Number.isNaN(parsed)) return parsed;
|
|
1329
|
+
}
|
|
1330
|
+
return null;
|
|
1331
|
+
}
|
|
1332
|
+
/** Variant stock with the product-level fallback applied (0 = not sellable). */
|
|
1333
|
+
variantSellableQty(v) {
|
|
1334
|
+
const q = this.getVariantQty(v);
|
|
1335
|
+
if (q !== null) return q;
|
|
1336
|
+
return this.product?.quantity ?? 0;
|
|
1337
|
+
}
|
|
1338
|
+
/** Resolve the active variant: full in-stock match → full match → best score
|
|
1339
|
+
* (2 per mapped option match, 1 per title match, +0.5 if in stock). */
|
|
1106
1340
|
selectedVariant() {
|
|
1107
|
-
if (!this.product || this.product.variants.length === 0)
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
const
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
)
|
|
1341
|
+
if (!this.product || !Array.isArray(this.product.variants) || this.product.variants.length === 0) {
|
|
1342
|
+
return null;
|
|
1343
|
+
}
|
|
1344
|
+
const sel = this.selectedOptions;
|
|
1345
|
+
const entries = Object.entries(sel);
|
|
1346
|
+
if (entries.length === 0) return this.product.variants[0] ?? null;
|
|
1347
|
+
const fullInStock = this.product.variants.find(
|
|
1348
|
+
(v) => this.variantMatchesSelection(v, sel) && this.variantSellableQty(v) > 0
|
|
1349
|
+
);
|
|
1350
|
+
if (fullInStock) return fullInStock;
|
|
1351
|
+
const full = this.product.variants.find((v) => this.variantMatchesSelection(v, sel));
|
|
1352
|
+
if (full) return full;
|
|
1353
|
+
let bestVariant = null;
|
|
1354
|
+
let maxScore = -1;
|
|
1355
|
+
for (const v of this.product.variants) {
|
|
1356
|
+
const { map, titleParts } = this.getVariantOptionMap(v, this.product?.options);
|
|
1357
|
+
const titlePartsLower = titleParts.map((t) => t.toLowerCase());
|
|
1358
|
+
let score = 0;
|
|
1359
|
+
for (const [optName, val] of entries) {
|
|
1360
|
+
const key = optName.trim().toLowerCase();
|
|
1361
|
+
const targetVal = String(val).trim().toLowerCase();
|
|
1362
|
+
if (map[key] && this.partMatchesValue(map[key], targetVal)) {
|
|
1363
|
+
score += 2;
|
|
1364
|
+
} else if (titlePartsLower.some((part) => this.partMatchesValue(part, targetVal))) {
|
|
1365
|
+
score += 1;
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
if (this.variantSellableQty(v) > 0) score += 0.5;
|
|
1369
|
+
if (score > maxScore) {
|
|
1370
|
+
maxScore = score;
|
|
1371
|
+
bestVariant = v;
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
return bestVariant ?? this.product.variants[0] ?? null;
|
|
1375
|
+
}
|
|
1376
|
+
/** True if some in-stock variant carries this option value. */
|
|
1377
|
+
isValueAvailable(optionName, value) {
|
|
1378
|
+
if (!this.product || !Array.isArray(this.product.variants) || this.product.variants.length === 0) {
|
|
1379
|
+
return true;
|
|
1380
|
+
}
|
|
1381
|
+
const valLower = String(value).trim().toLowerCase();
|
|
1382
|
+
const key = optionName.trim().toLowerCase();
|
|
1383
|
+
return this.product.variants.some((v) => {
|
|
1384
|
+
const { map, titleParts } = this.getVariantOptionMap(v, this.product?.options);
|
|
1385
|
+
if (this.variantSellableQty(v) <= 0) return false;
|
|
1386
|
+
if (map[key]) return this.partMatchesValue(map[key], valLower);
|
|
1387
|
+
return titleParts.some((part) => this.partMatchesValue(part.toLowerCase(), valLower));
|
|
1115
1388
|
});
|
|
1116
|
-
return found ?? this.product.variants[0] ?? null;
|
|
1117
1389
|
}
|
|
1118
1390
|
get unitPrice() {
|
|
1119
1391
|
const v = this.selectedVariant();
|
|
@@ -1127,7 +1399,7 @@ var VioProductDetail = class extends LitElement {
|
|
|
1127
1399
|
}
|
|
1128
1400
|
get availableQuantity() {
|
|
1129
1401
|
const v = this.selectedVariant();
|
|
1130
|
-
if (v) return v
|
|
1402
|
+
if (v) return this.variantSellableQty(v);
|
|
1131
1403
|
return this.product?.quantity ?? 0;
|
|
1132
1404
|
}
|
|
1133
1405
|
stockLabel() {
|
|
@@ -1136,9 +1408,50 @@ var VioProductDetail = class extends LitElement {
|
|
|
1136
1408
|
if (qty <= 3) return { text: `F\xE5 igjen \u2014 ${qty} stk`, cls: "low-stock" };
|
|
1137
1409
|
return { text: "P\xE5 lager", cls: "in-stock" };
|
|
1138
1410
|
}
|
|
1411
|
+
/** Pick an option value. If the exact combination doesn't exist, jump to an
|
|
1412
|
+
* in-stock variant that carries the value and adopt its full option map
|
|
1413
|
+
* (keeps the selection always resolvable to a real variant). */
|
|
1139
1414
|
selectOption(optionName, value) {
|
|
1140
|
-
|
|
1415
|
+
const newSelection = { ...this.selectedOptions, [optionName]: value };
|
|
1416
|
+
const exactInStock = this.product?.variants?.find(
|
|
1417
|
+
(v) => this.variantMatchesSelection(v, newSelection) && this.variantSellableQty(v) > 0
|
|
1418
|
+
);
|
|
1419
|
+
if (exactInStock) {
|
|
1420
|
+
this.selectedOptions = newSelection;
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1423
|
+
const exactAnyStock = this.product?.variants?.find(
|
|
1424
|
+
(v) => this.variantMatchesSelection(v, newSelection)
|
|
1425
|
+
);
|
|
1426
|
+
if (exactAnyStock) {
|
|
1427
|
+
this.selectedOptions = newSelection;
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1430
|
+
const valLower = String(value).trim().toLowerCase();
|
|
1431
|
+
const key = optionName.trim().toLowerCase();
|
|
1432
|
+
const anyInStockWithVal = this.product?.variants?.find((v) => {
|
|
1433
|
+
const { map, titleParts } = this.getVariantOptionMap(v, this.product?.options);
|
|
1434
|
+
if (this.variantSellableQty(v) <= 0) return false;
|
|
1435
|
+
if (map[key]) return this.partMatchesValue(map[key], valLower);
|
|
1436
|
+
return titleParts.some((p) => this.partMatchesValue(p.toLowerCase(), valLower));
|
|
1437
|
+
});
|
|
1438
|
+
if (anyInStockWithVal) {
|
|
1439
|
+
const { map } = this.getVariantOptionMap(anyInStockWithVal, this.product?.options);
|
|
1440
|
+
const updated = { ...newSelection };
|
|
1441
|
+
for (const opt of this.product?.options ?? []) {
|
|
1442
|
+
const k = opt.name.toLowerCase();
|
|
1443
|
+
if (map[k]) {
|
|
1444
|
+
const optValues = normalizeValues(opt.values);
|
|
1445
|
+
const origCase = optValues.find((val) => val.toLowerCase() === map[k]) || map[k];
|
|
1446
|
+
updated[opt.name] = origCase;
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
this.selectedOptions = updated;
|
|
1450
|
+
} else {
|
|
1451
|
+
this.selectedOptions = newSelection;
|
|
1452
|
+
}
|
|
1141
1453
|
}
|
|
1454
|
+
/* eslint-enable @typescript-eslint/no-explicit-any */
|
|
1142
1455
|
allImages() {
|
|
1143
1456
|
if (!this.product) return [];
|
|
1144
1457
|
const variantImages = this.selectedVariant()?.images ?? [];
|
|
@@ -1209,6 +1522,32 @@ var VioProductDetail = class extends LitElement {
|
|
|
1209
1522
|
this.adding = false;
|
|
1210
1523
|
}
|
|
1211
1524
|
}
|
|
1525
|
+
/** Buy with Vipps — opens the checkout with Vipps preselected. */
|
|
1526
|
+
buyWithVipps() {
|
|
1527
|
+
if (!this.product || this.availableQuantity <= 0 || this.adding) return;
|
|
1528
|
+
this.addCurrentToCart();
|
|
1529
|
+
this.close();
|
|
1530
|
+
this.dispatchEvent(
|
|
1531
|
+
new CustomEvent("vio:checkout-open", {
|
|
1532
|
+
bubbles: true,
|
|
1533
|
+
composed: true,
|
|
1534
|
+
detail: { sponsorId: this.sponsorId, paymentMethod: "vipps", express: false }
|
|
1535
|
+
})
|
|
1536
|
+
);
|
|
1537
|
+
}
|
|
1538
|
+
/** Buy with Stripe — opens the checkout with Stripe preselected. */
|
|
1539
|
+
buyWithStripe() {
|
|
1540
|
+
if (!this.product || this.availableQuantity <= 0 || this.adding) return;
|
|
1541
|
+
this.addCurrentToCart();
|
|
1542
|
+
this.close();
|
|
1543
|
+
this.dispatchEvent(
|
|
1544
|
+
new CustomEvent("vio:checkout-open", {
|
|
1545
|
+
bubbles: true,
|
|
1546
|
+
composed: true,
|
|
1547
|
+
detail: { sponsorId: this.sponsorId, paymentMethod: "stripe", express: false }
|
|
1548
|
+
})
|
|
1549
|
+
);
|
|
1550
|
+
}
|
|
1212
1551
|
/**
|
|
1213
1552
|
* Express buy with Apple Pay. Runs synchronously inside the click so the
|
|
1214
1553
|
* Apple Pay sheet keeps the user gesture: add to cart → open checkout (sets
|
|
@@ -1333,17 +1672,35 @@ var VioProductDetail = class extends LitElement {
|
|
|
1333
1672
|
</button>
|
|
1334
1673
|
</div>
|
|
1335
1674
|
|
|
1336
|
-
${this.availableQuantity > 0 && this.applePayOk ? html`
|
|
1675
|
+
${this.availableQuantity > 0 && this.applePayOk && this.methodEnabled("apple-pay", "applepay") ? html`
|
|
1337
1676
|
<button
|
|
1338
1677
|
class="buy-applepay"
|
|
1339
1678
|
@click=${this.buyWithApplePay}
|
|
1340
1679
|
?disabled=${this.adding}
|
|
1341
1680
|
aria-label="Kjøp nå med Apple Pay"
|
|
1342
1681
|
>
|
|
1343
|
-
Kjøp nå med
|
|
1682
|
+
<span>Kjøp nå med</span>
|
|
1683
|
+
<svg class="ap-logo" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M17.05 20.28c-.98.95-2.05.8-3.08.35-1.09-.46-2.09-.48-3.24 0-1.44.62-2.2.44-3.06-.35C2.79 15.25 3.51 7.59 9.05 7.31c1.35.07 2.29.74 3.08.8 1.18-.24 2.31-.93 3.57-.84 1.51.12 2.65.72 3.4 1.8-3.12 1.87-2.38 5.98.48 7.13-.57 1.5-1.31 2.99-2.54 4.09l.01-.01zM12.03 7.25c-.15-2.23 1.66-4.07 3.74-4.25.29 2.58-2.34 4.5-3.74 4.25z"/></svg>
|
|
1684
|
+
<span>Apple Pay</span>
|
|
1685
|
+
</button>
|
|
1686
|
+
` : ""}
|
|
1687
|
+
${this.availableQuantity > 0 && this.methodEnabled("stripe") ? html`
|
|
1688
|
+
<button
|
|
1689
|
+
class="buy-stripe"
|
|
1690
|
+
@click=${this.buyWithStripe}
|
|
1691
|
+
?disabled=${this.adding}
|
|
1692
|
+
aria-label="Kjøp nå med Stripe"
|
|
1693
|
+
style="width: 100%; margin-top: 10px; display: flex; align-items: center; justify-content: center; gap: 6px; background: #635bff; color: #fff; border: none; padding: 14px 24px; font-size: 15px; font-weight: 600; cursor: pointer; font-family: inherit;"
|
|
1694
|
+
>
|
|
1695
|
+
<span>Kjøp nå med</span>
|
|
1696
|
+
<img
|
|
1697
|
+
src=${STRIPE_LOGO_SRC}
|
|
1698
|
+
style="height: 20px; display: block; filter: brightness(0) invert(1);"
|
|
1699
|
+
alt="Stripe"
|
|
1700
|
+
/>
|
|
1344
1701
|
</button>
|
|
1345
1702
|
` : ""}
|
|
1346
|
-
${this.availableQuantity > 0 ? html`
|
|
1703
|
+
${this.availableQuantity > 0 && this.methodEnabled("klarna") ? html`
|
|
1347
1704
|
<button
|
|
1348
1705
|
class="buy-klarna"
|
|
1349
1706
|
@click=${this.buyWithKlarna}
|
|
@@ -1358,6 +1715,18 @@ var VioProductDetail = class extends LitElement {
|
|
|
1358
1715
|
</button>
|
|
1359
1716
|
` : ""}
|
|
1360
1717
|
|
|
1718
|
+
${this.availableQuantity > 0 && this.methodEnabled("vipps") ? html`
|
|
1719
|
+
<button
|
|
1720
|
+
class="buy-vipps"
|
|
1721
|
+
@click=${this.buyWithVipps}
|
|
1722
|
+
?disabled=${this.adding}
|
|
1723
|
+
aria-label="Kjøp nå med Vipps"
|
|
1724
|
+
>
|
|
1725
|
+
<span>Kjøp nå med</span>
|
|
1726
|
+
<span class="vipps-badge">vipps</span>
|
|
1727
|
+
</button>
|
|
1728
|
+
` : ""}
|
|
1729
|
+
|
|
1361
1730
|
${p.description ? html`
|
|
1362
1731
|
<div>
|
|
1363
1732
|
<div class="description-label">Beskrivelse</div>
|
|
@@ -1377,17 +1746,22 @@ var VioProductDetail = class extends LitElement {
|
|
|
1377
1746
|
<div class="option-group">
|
|
1378
1747
|
<div class="option-label">${opt.name}</div>
|
|
1379
1748
|
<div class="option-values">
|
|
1380
|
-
${values.map(
|
|
1381
|
-
(val)
|
|
1749
|
+
${values.map((val) => {
|
|
1750
|
+
const available = this.isValueAvailable(opt.name, val);
|
|
1751
|
+
const currentVal = this.selectedOptions[opt.name] ?? "";
|
|
1752
|
+
const isSelected = String(currentVal).trim().toLowerCase() === String(val).trim().toLowerCase();
|
|
1753
|
+
return html`
|
|
1382
1754
|
<button
|
|
1383
|
-
class="option-pill"
|
|
1755
|
+
class="option-pill ${available ? "" : "unavailable"}"
|
|
1384
1756
|
@click=${() => this.selectOption(opt.name, val)}
|
|
1385
|
-
aria-pressed=${
|
|
1757
|
+
aria-pressed=${isSelected}
|
|
1758
|
+
?disabled=${!available}
|
|
1759
|
+
title=${available ? "" : "Utsolgt"}
|
|
1386
1760
|
>
|
|
1387
1761
|
${val}
|
|
1388
1762
|
</button>
|
|
1389
|
-
|
|
1390
|
-
)}
|
|
1763
|
+
`;
|
|
1764
|
+
})}
|
|
1391
1765
|
</div>
|
|
1392
1766
|
</div>
|
|
1393
1767
|
`;
|
|
@@ -1437,6 +1811,9 @@ __decorateClass([
|
|
|
1437
1811
|
__decorateClass([
|
|
1438
1812
|
state()
|
|
1439
1813
|
], VioProductDetail.prototype, "applePayOk", 2);
|
|
1814
|
+
__decorateClass([
|
|
1815
|
+
state()
|
|
1816
|
+
], VioProductDetail.prototype, "availableMethods", 2);
|
|
1440
1817
|
function normalizeValues(raw) {
|
|
1441
1818
|
if (Array.isArray(raw)) {
|
|
1442
1819
|
return raw.map((v) => String(v).trim()).filter((v) => v.length > 0);
|
|
@@ -1446,6 +1823,9 @@ function normalizeValues(raw) {
|
|
|
1446
1823
|
}
|
|
1447
1824
|
return [];
|
|
1448
1825
|
}
|
|
1826
|
+
var STRIPE_LOGO_SRC2 = "data:image/svg+xml;utf8," + encodeURIComponent(
|
|
1827
|
+
'<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>'
|
|
1828
|
+
);
|
|
1449
1829
|
var VioCart = class extends LitElement {
|
|
1450
1830
|
constructor() {
|
|
1451
1831
|
super(...arguments);
|
|
@@ -1456,15 +1836,21 @@ var VioCart = class extends LitElement {
|
|
|
1456
1836
|
this.carts = /* @__PURE__ */ new Map();
|
|
1457
1837
|
this.itemCount = 0;
|
|
1458
1838
|
this.applePayAvailable = false;
|
|
1839
|
+
this.availableMethods = null;
|
|
1459
1840
|
this.confirmedOrder = null;
|
|
1460
1841
|
/** Pre-resolved Apple Pay handle; `show()` fires the sheet synchronously. */
|
|
1461
1842
|
this.applePayHandle = null;
|
|
1462
1843
|
/** Cart total Apple Pay was last prepared for (avoids redundant prep). */
|
|
1463
1844
|
this.preparedTotal = -1;
|
|
1845
|
+
/** In-flight guard for loadAvailablePaymentMethods. */
|
|
1846
|
+
this.loadingPaymentMethods = false;
|
|
1464
1847
|
this.boundOnCartChange = (e) => {
|
|
1465
1848
|
const detail = e.detail;
|
|
1466
1849
|
this.carts = detail.cartsBySponsor;
|
|
1467
1850
|
this.itemCount = detail.itemCount;
|
|
1851
|
+
if (this.availableMethods === null) {
|
|
1852
|
+
void this.loadAvailablePaymentMethods();
|
|
1853
|
+
}
|
|
1468
1854
|
};
|
|
1469
1855
|
}
|
|
1470
1856
|
static {
|
|
@@ -1672,7 +2058,7 @@ var VioCart = class extends LitElement {
|
|
|
1672
2058
|
transition: opacity 0.15s;
|
|
1673
2059
|
}
|
|
1674
2060
|
.applepay-btn:hover { opacity: 0.86; }
|
|
1675
|
-
.applepay-btn .ap-logo {
|
|
2061
|
+
.applepay-btn .ap-logo { width: 20px; height: 20px; }
|
|
1676
2062
|
/* Klarna express — pink brand button, same express row as Apple Pay. */
|
|
1677
2063
|
.klarna-btn {
|
|
1678
2064
|
width: 100%;
|
|
@@ -1694,6 +2080,50 @@ var VioCart = class extends LitElement {
|
|
|
1694
2080
|
}
|
|
1695
2081
|
.klarna-btn:hover { background: #ffa1ba; }
|
|
1696
2082
|
.klarna-btn .klarna-logo { height: 15px; width: auto; display: inline-block; }
|
|
2083
|
+
/* Vipps — brand orange button, same express row. */
|
|
2084
|
+
.vipps-btn {
|
|
2085
|
+
width: 100%;
|
|
2086
|
+
padding: 15px;
|
|
2087
|
+
margin-bottom: 10px;
|
|
2088
|
+
background: #ff5b24;
|
|
2089
|
+
color: #fff;
|
|
2090
|
+
border: none;
|
|
2091
|
+
border-radius: 8px;
|
|
2092
|
+
cursor: pointer;
|
|
2093
|
+
display: flex;
|
|
2094
|
+
align-items: center;
|
|
2095
|
+
justify-content: center;
|
|
2096
|
+
font-family: var(--vio-font-sans, -apple-system, sans-serif);
|
|
2097
|
+
transition: background 0.15s;
|
|
2098
|
+
}
|
|
2099
|
+
.vipps-btn:hover { background: #ec4f1c; }
|
|
2100
|
+
.vipps-btn .vipps-logo {
|
|
2101
|
+
font-size: 20px;
|
|
2102
|
+
font-weight: 800;
|
|
2103
|
+
letter-spacing: -0.02em;
|
|
2104
|
+
line-height: 1;
|
|
2105
|
+
}
|
|
2106
|
+
/* Stripe — brand blurple button, same express row. */
|
|
2107
|
+
.stripe-btn {
|
|
2108
|
+
width: 100%;
|
|
2109
|
+
padding: 15px;
|
|
2110
|
+
margin-bottom: 10px;
|
|
2111
|
+
background: #635bff;
|
|
2112
|
+
color: #fff;
|
|
2113
|
+
border: none;
|
|
2114
|
+
border-radius: 8px;
|
|
2115
|
+
font-size: 16px;
|
|
2116
|
+
font-weight: 600;
|
|
2117
|
+
cursor: pointer;
|
|
2118
|
+
display: flex;
|
|
2119
|
+
align-items: center;
|
|
2120
|
+
justify-content: center;
|
|
2121
|
+
gap: 6px;
|
|
2122
|
+
font-family: var(--vio-font-sans, -apple-system, sans-serif);
|
|
2123
|
+
transition: background 0.15s;
|
|
2124
|
+
}
|
|
2125
|
+
.stripe-btn:hover { background: #5248ff; }
|
|
2126
|
+
.stripe-btn .stripe-logo { height: 20px; width: auto; display: inline-block; filter: brightness(0) invert(1); }
|
|
1697
2127
|
|
|
1698
2128
|
/* In-drawer confirmation after Apple Pay (nothing else is shown). */
|
|
1699
2129
|
.confirm {
|
|
@@ -1768,6 +2198,7 @@ var VioCart = class extends LitElement {
|
|
|
1768
2198
|
this.carts = Vio.cart.getAllCarts();
|
|
1769
2199
|
this.itemCount = Vio.cart.itemCount;
|
|
1770
2200
|
Vio.cart.addEventListener("change", this.boundOnCartChange);
|
|
2201
|
+
void this.loadAvailablePaymentMethods();
|
|
1771
2202
|
}
|
|
1772
2203
|
disconnectedCallback() {
|
|
1773
2204
|
Vio.cart.removeEventListener("change", this.boundOnCartChange);
|
|
@@ -1778,6 +2209,39 @@ var VioCart = class extends LitElement {
|
|
|
1778
2209
|
}
|
|
1779
2210
|
show() {
|
|
1780
2211
|
this.open = true;
|
|
2212
|
+
void this.loadAvailablePaymentMethods();
|
|
2213
|
+
}
|
|
2214
|
+
/** Which payment buttons to render — backend-configured per sponsor.
|
|
2215
|
+
* Falls back to showing everything if the query fails or none is set. */
|
|
2216
|
+
async loadAvailablePaymentMethods() {
|
|
2217
|
+
if (this.loadingPaymentMethods) return;
|
|
2218
|
+
this.loadingPaymentMethods = true;
|
|
2219
|
+
const firstSponsorId = [...this.carts.keys()][0] ?? globalThis.__VIO_SPONSOR_ID__;
|
|
2220
|
+
if (firstSponsorId === void 0) {
|
|
2221
|
+
this.loadingPaymentMethods = false;
|
|
2222
|
+
return;
|
|
2223
|
+
}
|
|
2224
|
+
try {
|
|
2225
|
+
const methods = await Vio.checkout.getAvailablePaymentMethods(firstSponsorId);
|
|
2226
|
+
if (Array.isArray(methods)) {
|
|
2227
|
+
this.availableMethods = methods.map((m) => m.name);
|
|
2228
|
+
} else {
|
|
2229
|
+
this.availableMethods = null;
|
|
2230
|
+
}
|
|
2231
|
+
} catch (err) {
|
|
2232
|
+
if (typeof console !== "undefined") {
|
|
2233
|
+
console.warn("[VioCart] Failed to load payment methods:", err);
|
|
2234
|
+
}
|
|
2235
|
+
this.availableMethods = null;
|
|
2236
|
+
} finally {
|
|
2237
|
+
this.loadingPaymentMethods = false;
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
/** Backend names arrive as e.g. "Apple Pay" — compare letters only. */
|
|
2241
|
+
methodEnabled(name) {
|
|
2242
|
+
if (this.availableMethods === null) return true;
|
|
2243
|
+
const norm = (s) => s.toLowerCase().replace(/[^a-z]/g, "");
|
|
2244
|
+
return this.availableMethods.some((m) => norm(m) === norm(name));
|
|
1781
2245
|
}
|
|
1782
2246
|
updated(changed) {
|
|
1783
2247
|
if (changed.has("open") && this.open) {
|
|
@@ -1864,6 +2328,32 @@ var VioCart = class extends LitElement {
|
|
|
1864
2328
|
})
|
|
1865
2329
|
);
|
|
1866
2330
|
}
|
|
2331
|
+
/** Vipps — opens the checkout with Vipps preselected. */
|
|
2332
|
+
onVipps() {
|
|
2333
|
+
if (this.itemCount === 0) return;
|
|
2334
|
+
const firstSponsorId = [...this.carts.keys()][0];
|
|
2335
|
+
if (firstSponsorId === void 0) return;
|
|
2336
|
+
this.dispatchEvent(
|
|
2337
|
+
new CustomEvent("vio:checkout-open", {
|
|
2338
|
+
bubbles: true,
|
|
2339
|
+
composed: true,
|
|
2340
|
+
detail: { sponsorId: firstSponsorId, paymentMethod: "vipps", express: false }
|
|
2341
|
+
})
|
|
2342
|
+
);
|
|
2343
|
+
}
|
|
2344
|
+
/** Stripe — opens the checkout with Stripe preselected. */
|
|
2345
|
+
onStripe() {
|
|
2346
|
+
if (this.itemCount === 0) return;
|
|
2347
|
+
const firstSponsorId = [...this.carts.keys()][0];
|
|
2348
|
+
if (firstSponsorId === void 0) return;
|
|
2349
|
+
this.dispatchEvent(
|
|
2350
|
+
new CustomEvent("vio:checkout-open", {
|
|
2351
|
+
bubbles: true,
|
|
2352
|
+
composed: true,
|
|
2353
|
+
detail: { sponsorId: firstSponsorId, paymentMethod: "stripe", express: false }
|
|
2354
|
+
})
|
|
2355
|
+
);
|
|
2356
|
+
}
|
|
1867
2357
|
get totalAcrossSponsors() {
|
|
1868
2358
|
let sum = 0;
|
|
1869
2359
|
for (const cart of this.carts.values()) {
|
|
@@ -1959,27 +2449,48 @@ var VioCart = class extends LitElement {
|
|
|
1959
2449
|
${formatPrice(this.totalAcrossSponsors, this.primaryCurrency)}
|
|
1960
2450
|
</span>
|
|
1961
2451
|
</div>
|
|
1962
|
-
${this.applePayAvailable ? html`
|
|
2452
|
+
${this.methodEnabled("apple-pay") && this.applePayAvailable ? html`
|
|
1963
2453
|
<button
|
|
1964
2454
|
class="applepay-btn"
|
|
1965
2455
|
@click=${this.onApplePay}
|
|
1966
2456
|
aria-label="Betal med Apple Pay"
|
|
1967
2457
|
>
|
|
1968
|
-
<
|
|
2458
|
+
<svg class="ap-logo" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M17.05 20.28c-.98.95-2.05.8-3.08.35-1.09-.46-2.09-.48-3.24 0-1.44.62-2.2.44-3.06-.35C2.79 15.25 3.51 7.59 9.05 7.31c1.35.07 2.29.74 3.08.8 1.18-.24 2.31-.93 3.57-.84 1.51.12 2.65.72 3.4 1.8-3.12 1.87-2.38 5.98.48 7.13-.57 1.5-1.31 2.99-2.54 4.09l.01-.01zM12.03 7.25c-.15-2.23 1.66-4.07 3.74-4.25.29 2.58-2.34 4.5-3.74 4.25z"/></svg>
|
|
2459
|
+
Apple Pay
|
|
2460
|
+
</button>
|
|
2461
|
+
` : ""}
|
|
2462
|
+
${this.methodEnabled("stripe") ? html`
|
|
2463
|
+
<button
|
|
2464
|
+
class="stripe-btn"
|
|
2465
|
+
@click=${this.onStripe}
|
|
2466
|
+
aria-label="Betal med Stripe"
|
|
2467
|
+
>
|
|
2468
|
+
<img class="stripe-logo" src=${STRIPE_LOGO_SRC2} alt="Stripe" />
|
|
2469
|
+
</button>
|
|
2470
|
+
` : ""}
|
|
2471
|
+
${this.methodEnabled("klarna") ? html`
|
|
2472
|
+
<button
|
|
2473
|
+
class="klarna-btn"
|
|
2474
|
+
@click=${this.onKlarnaExpress}
|
|
2475
|
+
aria-label="Betal med Klarna"
|
|
2476
|
+
>
|
|
2477
|
+
Kjøp med
|
|
2478
|
+
<img
|
|
2479
|
+
class="klarna-logo"
|
|
2480
|
+
src="https://x.klarnacdn.net/payment-method/assets/badges/generic/klarna.svg"
|
|
2481
|
+
alt="Klarna"
|
|
2482
|
+
/>
|
|
2483
|
+
</button>
|
|
2484
|
+
` : ""}
|
|
2485
|
+
${this.methodEnabled("vipps") ? html`
|
|
2486
|
+
<button
|
|
2487
|
+
class="vipps-btn"
|
|
2488
|
+
@click=${this.onVipps}
|
|
2489
|
+
aria-label="Betal med Vipps"
|
|
2490
|
+
>
|
|
2491
|
+
<span class="vipps-logo">vipps</span>
|
|
1969
2492
|
</button>
|
|
1970
2493
|
` : ""}
|
|
1971
|
-
<button
|
|
1972
|
-
class="klarna-btn"
|
|
1973
|
-
@click=${this.onKlarnaExpress}
|
|
1974
|
-
aria-label="Betal med Klarna"
|
|
1975
|
-
>
|
|
1976
|
-
Kjøp med
|
|
1977
|
-
<img
|
|
1978
|
-
class="klarna-logo"
|
|
1979
|
-
src="https://x.klarnacdn.net/payment-method/assets/badges/generic/klarna.svg"
|
|
1980
|
-
alt="Klarna"
|
|
1981
|
-
/>
|
|
1982
|
-
</button>
|
|
1983
2494
|
</div>
|
|
1984
2495
|
`}
|
|
1985
2496
|
</aside>
|
|
@@ -2033,10 +2544,16 @@ __decorateClass([
|
|
|
2033
2544
|
__decorateClass([
|
|
2034
2545
|
state()
|
|
2035
2546
|
], VioCart.prototype, "applePayAvailable", 2);
|
|
2547
|
+
__decorateClass([
|
|
2548
|
+
state()
|
|
2549
|
+
], VioCart.prototype, "availableMethods", 2);
|
|
2036
2550
|
__decorateClass([
|
|
2037
2551
|
state()
|
|
2038
2552
|
], VioCart.prototype, "confirmedOrder", 2);
|
|
2039
|
-
var
|
|
2553
|
+
var STRIPE_LOGO_SRC3 = "data:image/svg+xml;utf8," + encodeURIComponent(
|
|
2554
|
+
'<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>'
|
|
2555
|
+
);
|
|
2556
|
+
var _VioCheckout = class _VioCheckout extends LitElement {
|
|
2040
2557
|
constructor() {
|
|
2041
2558
|
super(...arguments);
|
|
2042
2559
|
this.open = false;
|
|
@@ -2051,6 +2568,7 @@ var VioCheckout = class extends LitElement {
|
|
|
2051
2568
|
this.orderConfirmed = false;
|
|
2052
2569
|
this.confirmedMethod = null;
|
|
2053
2570
|
this.paymentError = null;
|
|
2571
|
+
this.paymentNotice = null;
|
|
2054
2572
|
this.confirmedOrder = null;
|
|
2055
2573
|
/** Active Klarna Payments widget handle + the subtotal it was mounted for. */
|
|
2056
2574
|
this.klarnaHandle = null;
|
|
@@ -2061,6 +2579,10 @@ var VioCheckout = class extends LitElement {
|
|
|
2061
2579
|
this.klarnaSelectedCat = "";
|
|
2062
2580
|
this.klarnaAuthorizing = false;
|
|
2063
2581
|
this.selectedShipping = KLARNA_SHIPPING_OPTIONS.find((o) => o.preselected)?.id ?? KLARNA_SHIPPING_OPTIONS[0]?.id ?? "";
|
|
2582
|
+
this.availableShippingsList = [];
|
|
2583
|
+
this.availableMethods = null;
|
|
2584
|
+
this.stripeLoading = false;
|
|
2585
|
+
this.vippsLoading = false;
|
|
2064
2586
|
this.form = {
|
|
2065
2587
|
firstName: "",
|
|
2066
2588
|
lastName: "",
|
|
@@ -2069,13 +2591,27 @@ var VioCheckout = class extends LitElement {
|
|
|
2069
2591
|
postalCode: "",
|
|
2070
2592
|
city: ""
|
|
2071
2593
|
};
|
|
2594
|
+
/** In-flight guard for loadAvailablePaymentMethods. */
|
|
2595
|
+
this.loadingPaymentMethods = false;
|
|
2596
|
+
/** In-flight + attempted guards for loadAvailableShippings (change events
|
|
2597
|
+
* fire per keystroke — without these, typing an address hammers the API). */
|
|
2598
|
+
this.loadingShippings = false;
|
|
2599
|
+
this.shippingsAttempted = false;
|
|
2072
2600
|
this.boundOnCheckoutChange = (e) => {
|
|
2073
2601
|
const detail = e.detail;
|
|
2602
|
+
const prevSponsorId = this.checkoutState?.sponsorId;
|
|
2603
|
+
const sponsorChanged = !prevSponsorId || prevSponsorId !== detail.state?.sponsorId;
|
|
2074
2604
|
this.checkoutState = detail.state;
|
|
2075
2605
|
this.items = Vio.checkout.items;
|
|
2076
2606
|
if (this.checkoutState) {
|
|
2077
|
-
this.
|
|
2078
|
-
|
|
2607
|
+
if (sponsorChanged || this.availableMethods === null) {
|
|
2608
|
+
void this.refreshApplePay();
|
|
2609
|
+
void this.refreshKlarna();
|
|
2610
|
+
void this.loadAvailablePaymentMethods();
|
|
2611
|
+
void this.loadAvailableShippings();
|
|
2612
|
+
} else if (!this.shippingsAttempted) {
|
|
2613
|
+
void this.loadAvailableShippings();
|
|
2614
|
+
}
|
|
2079
2615
|
} else {
|
|
2080
2616
|
this.applePayAvailable = false;
|
|
2081
2617
|
this.klarnaAvailable = false;
|
|
@@ -2083,7 +2619,9 @@ var VioCheckout = class extends LitElement {
|
|
|
2083
2619
|
this.confirmedMethod = null;
|
|
2084
2620
|
this.confirmedOrder = null;
|
|
2085
2621
|
this.paymentError = null;
|
|
2622
|
+
this.paymentNotice = null;
|
|
2086
2623
|
this.express = false;
|
|
2624
|
+
this.availableMethods = null;
|
|
2087
2625
|
this.unmountKlarna();
|
|
2088
2626
|
}
|
|
2089
2627
|
};
|
|
@@ -2164,6 +2702,21 @@ var VioCheckout = class extends LitElement {
|
|
|
2164
2702
|
box-shadow: -8px 0 24px rgba(0, 0, 0, 0.1);
|
|
2165
2703
|
}
|
|
2166
2704
|
:host([open]) .modal.as-side { transform: translateX(0); }
|
|
2705
|
+
|
|
2706
|
+
/* Desktop: the checkout ALWAYS lives as a right-side panel (like the cart,
|
|
2707
|
+
product detail and express drawer) — never a full-width overlay. Only the
|
|
2708
|
+
compact confirmation card stays centered. */
|
|
2709
|
+
@media (min-width: 601px) {
|
|
2710
|
+
.modal:not(.as-confirm) {
|
|
2711
|
+
inset: 0 0 0 auto;
|
|
2712
|
+
width: min(440px, 100%);
|
|
2713
|
+
max-width: 440px;
|
|
2714
|
+
transform: translateX(100%);
|
|
2715
|
+
border-radius: 0;
|
|
2716
|
+
box-shadow: -8px 0 24px rgba(0, 0, 0, 0.1);
|
|
2717
|
+
}
|
|
2718
|
+
:host([open]) .modal:not(.as-confirm) { transform: translateX(0); }
|
|
2719
|
+
}
|
|
2167
2720
|
@media (max-width: 600px) {
|
|
2168
2721
|
/* On phones, keep it a bottom sheet for reachability. */
|
|
2169
2722
|
.modal.as-side {
|
|
@@ -2344,8 +2897,9 @@ var VioCheckout = class extends LitElement {
|
|
|
2344
2897
|
background: var(--vio-color-surface-hover, #fafafa);
|
|
2345
2898
|
}
|
|
2346
2899
|
.payment-btn:disabled {
|
|
2347
|
-
cursor:
|
|
2348
|
-
opacity: 0.
|
|
2900
|
+
cursor: not-allowed;
|
|
2901
|
+
opacity: 0.45;
|
|
2902
|
+
pointer-events: none;
|
|
2349
2903
|
}
|
|
2350
2904
|
.payment-btn.primary {
|
|
2351
2905
|
background: var(--vio-color-text, #0a0a0a);
|
|
@@ -2429,9 +2983,27 @@ var VioCheckout = class extends LitElement {
|
|
|
2429
2983
|
color: var(--vio-color-accent, #c14a3b);
|
|
2430
2984
|
text-align: center;
|
|
2431
2985
|
}
|
|
2986
|
+
.payment-notice {
|
|
2987
|
+
grid-column: 1 / -1;
|
|
2988
|
+
padding: 12px 16px;
|
|
2989
|
+
background: #f4f4f2;
|
|
2990
|
+
border: 1px solid var(--vio-color-border, #ddd);
|
|
2991
|
+
border-radius: 8px;
|
|
2992
|
+
font-size: 13px;
|
|
2993
|
+
color: var(--vio-color-text, #1a1a1a);
|
|
2994
|
+
text-align: center;
|
|
2995
|
+
}
|
|
2432
2996
|
/* "Betal" CTA — sits full-width below the method grid once a method is
|
|
2433
2997
|
picked. Reuses .payment-btn.primary (its grid-column spans the row). */
|
|
2434
|
-
.complete-cta {
|
|
2998
|
+
.complete-cta {
|
|
2999
|
+
width: 100% !important;
|
|
3000
|
+
box-sizing: border-box !important;
|
|
3001
|
+
margin-top: 8px;
|
|
3002
|
+
display: flex !important;
|
|
3003
|
+
align-items: center !important;
|
|
3004
|
+
justify-content: center !important;
|
|
3005
|
+
text-align: center !important;
|
|
3006
|
+
}
|
|
2435
3007
|
|
|
2436
3008
|
/* Order confirmation — compact, shown inside the drawer. */
|
|
2437
3009
|
.confirmation {
|
|
@@ -2559,6 +3131,209 @@ var VioCheckout = class extends LitElement {
|
|
|
2559
3131
|
if (this.checkoutState) {
|
|
2560
3132
|
void this.refreshApplePay();
|
|
2561
3133
|
void this.refreshKlarna();
|
|
3134
|
+
void this.loadAvailablePaymentMethods();
|
|
3135
|
+
}
|
|
3136
|
+
void Vio.checkout.resumeKlarnaReturn();
|
|
3137
|
+
void this.checkReturnPaymentStatus();
|
|
3138
|
+
}
|
|
3139
|
+
static {
|
|
3140
|
+
/**
|
|
3141
|
+
* Detect the redirect return from a hosted payment page (Stripe / Vipps /
|
|
3142
|
+
* Klarna): the return URL carries vio_payment/vio_method/vio_sponsor. On
|
|
3143
|
+
* success: clear that sponsor's cart, show the confirmation drawer, emit
|
|
3144
|
+
* vio:payment-success (analytics/host hooks) and scrub the URL params.
|
|
3145
|
+
*/
|
|
3146
|
+
/** Backend checkout statuses that mean the payment went through. */
|
|
3147
|
+
this.PAID_STATUSES = /* @__PURE__ */ new Set([
|
|
3148
|
+
"paid",
|
|
3149
|
+
"completed",
|
|
3150
|
+
"complete",
|
|
3151
|
+
"success",
|
|
3152
|
+
"succeeded",
|
|
3153
|
+
"confirmed",
|
|
3154
|
+
"captured"
|
|
3155
|
+
]);
|
|
3156
|
+
}
|
|
3157
|
+
static {
|
|
3158
|
+
/** Statuses that mean the payment definitively did NOT go through. */
|
|
3159
|
+
this.FAILED_STATUSES = /* @__PURE__ */ new Set([
|
|
3160
|
+
"failed",
|
|
3161
|
+
"cancelled",
|
|
3162
|
+
"canceled",
|
|
3163
|
+
"rejected",
|
|
3164
|
+
"declined",
|
|
3165
|
+
"expired",
|
|
3166
|
+
"aborted",
|
|
3167
|
+
"terminated",
|
|
3168
|
+
"error",
|
|
3169
|
+
"voided",
|
|
3170
|
+
"unpaid"
|
|
3171
|
+
]);
|
|
3172
|
+
}
|
|
3173
|
+
static {
|
|
3174
|
+
/** Waits between GetCheckout attempts — PSP webhooks (Vipps especially) can
|
|
3175
|
+
* land seconds after the shopper is redirected back, so a single immediate
|
|
3176
|
+
* check misreads a successful charge as unpaid. ~20s total. */
|
|
3177
|
+
this.RETURN_VERIFY_DELAYS_MS = [0, 2e3, 4e3, 6e3, 8e3];
|
|
3178
|
+
}
|
|
3179
|
+
static {
|
|
3180
|
+
/** Vipps polling cadence — up to 2 minutes. Its webhook can lag well behind
|
|
3181
|
+
* the redirect; this window was verified against a live checkout. */
|
|
3182
|
+
this.VIPPS_STATUS_MAX_ATTEMPTS = 24;
|
|
3183
|
+
}
|
|
3184
|
+
static {
|
|
3185
|
+
this.VIPPS_STATUS_DELAY_MS = 5e3;
|
|
3186
|
+
}
|
|
3187
|
+
/**
|
|
3188
|
+
* Vipps redirects to our return URL the same way on cancel as on success —
|
|
3189
|
+
* the URL alone can't tell them apart, and the generic checkout status can
|
|
3190
|
+
* lag behind Vipps' own webhook. Poll Vipps' own state directly instead:
|
|
3191
|
+
* AUTHORIZED confirms, CREATED means still processing (keep polling),
|
|
3192
|
+
* anything else (including an unrecognized state) is treated as failed —
|
|
3193
|
+
* fail-closed by default rather than guessing.
|
|
3194
|
+
*/
|
|
3195
|
+
async pollVippsStatus(checkoutId, sponsorId) {
|
|
3196
|
+
let sawStatus = false;
|
|
3197
|
+
for (let attempt = 0; attempt < _VioCheckout.VIPPS_STATUS_MAX_ATTEMPTS; attempt++) {
|
|
3198
|
+
if (attempt > 0) {
|
|
3199
|
+
await new Promise((r) => setTimeout(r, _VioCheckout.VIPPS_STATUS_DELAY_MS));
|
|
3200
|
+
}
|
|
3201
|
+
try {
|
|
3202
|
+
const vippsStatus = await Vio.checkout.getVippsStatus(checkoutId, sponsorId || void 0);
|
|
3203
|
+
const state6 = String(vippsStatus?.state ?? "").toUpperCase();
|
|
3204
|
+
sawStatus = true;
|
|
3205
|
+
if (state6 === "AUTHORIZED") return { outcome: "paid", sawStatus };
|
|
3206
|
+
if (state6 === "CREATED") continue;
|
|
3207
|
+
return { outcome: "failed", sawStatus };
|
|
3208
|
+
} catch (err) {
|
|
3209
|
+
if (typeof console !== "undefined") {
|
|
3210
|
+
console.warn("[VioCheckout] GetVippsStatus attempt failed:", err);
|
|
3211
|
+
}
|
|
3212
|
+
}
|
|
3213
|
+
}
|
|
3214
|
+
return { outcome: "pending", sawStatus };
|
|
3215
|
+
}
|
|
3216
|
+
/** Shared outcome handler for both the generic (GetCheckout) and the
|
|
3217
|
+
* Vipps-specific (GetVippsStatus) return-verification paths. */
|
|
3218
|
+
applyReturnOutcome(outcome, sawStatus, vioMethod, sponsorId, checkoutId) {
|
|
3219
|
+
if (outcome === "paid") {
|
|
3220
|
+
if (sponsorId > 0) Vio.cart.clearSponsorCart(sponsorId);
|
|
3221
|
+
this.confirmedOrder = {
|
|
3222
|
+
items: [],
|
|
3223
|
+
currency: getGlobalCurrency(),
|
|
3224
|
+
total: 0,
|
|
3225
|
+
orderId: checkoutId || void 0
|
|
3226
|
+
};
|
|
3227
|
+
this.confirmedMethod = vioMethod;
|
|
3228
|
+
this.orderConfirmed = true;
|
|
3229
|
+
this.open = true;
|
|
3230
|
+
this.dispatchEvent(
|
|
3231
|
+
new CustomEvent("vio:payment-success", {
|
|
3232
|
+
bubbles: true,
|
|
3233
|
+
composed: true,
|
|
3234
|
+
detail: { method: vioMethod, sponsorId, orderId: checkoutId }
|
|
3235
|
+
})
|
|
3236
|
+
);
|
|
3237
|
+
} else if (outcome === "failed") {
|
|
3238
|
+
this.paymentError = "Betalingen ble avbrutt eller feilet. Vennligst pr\xF8v igjen.";
|
|
3239
|
+
this.open = true;
|
|
3240
|
+
} else if (sawStatus) {
|
|
3241
|
+
this.paymentNotice = "Betalingen behandles fortsatt. Du f\xE5r ordrebekreftelse p\xE5 e-post n\xE5r den er gjennomf\xF8rt. Handlekurven er uendret.";
|
|
3242
|
+
this.open = true;
|
|
3243
|
+
} else {
|
|
3244
|
+
this.paymentError = "Vi kunne ikke bekrefte betalingen. Handlekurven er uendret \u2014 pr\xF8v igjen, eller kontakt support hvis du ble belastet.";
|
|
3245
|
+
this.open = true;
|
|
3246
|
+
}
|
|
3247
|
+
}
|
|
3248
|
+
async checkReturnPaymentStatus() {
|
|
3249
|
+
if (typeof window === "undefined" || !window.location || !window.location.search) return;
|
|
3250
|
+
try {
|
|
3251
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
3252
|
+
const vioPayment = urlParams.get("vio_payment");
|
|
3253
|
+
const vioMethod = urlParams.get("vio_method") || "stripe";
|
|
3254
|
+
const sponsorId = Number(urlParams.get("vio_sponsor") || 0);
|
|
3255
|
+
const checkoutId = urlParams.get("checkout_id") || "";
|
|
3256
|
+
if (!vioPayment && !checkoutId) return;
|
|
3257
|
+
if (vioMethod === "klarna" && typeof sessionStorage !== "undefined" && sessionStorage.getItem("vio.klarna.pending.v1")) {
|
|
3258
|
+
this.cleanReturnQueryParams(["vio_payment", "vio_method", "vio_sponsor", "checkout_id"]);
|
|
3259
|
+
return;
|
|
3260
|
+
}
|
|
3261
|
+
if (vioMethod === "vipps" && checkoutId) {
|
|
3262
|
+
if (!this.checkoutState) {
|
|
3263
|
+
const spIdToUse = sponsorId || [...Vio.cart.getAllCarts().keys()][0] || 0;
|
|
3264
|
+
if (spIdToUse) {
|
|
3265
|
+
try {
|
|
3266
|
+
Vio.checkout.open(spIdToUse);
|
|
3267
|
+
Vio.checkout.selectPaymentMethod("vipps");
|
|
3268
|
+
} catch {
|
|
3269
|
+
}
|
|
3270
|
+
}
|
|
3271
|
+
}
|
|
3272
|
+
this.open = true;
|
|
3273
|
+
this.paymentNotice = "Bekrefter betalingen med Vipps\u2026";
|
|
3274
|
+
const { outcome, sawStatus } = await this.pollVippsStatus(checkoutId, sponsorId);
|
|
3275
|
+
this.paymentNotice = null;
|
|
3276
|
+
this.applyReturnOutcome(outcome, sawStatus, vioMethod, sponsorId, checkoutId);
|
|
3277
|
+
this.cleanReturnQueryParams(["vio_payment", "vio_method", "vio_sponsor", "checkout_id"]);
|
|
3278
|
+
} else if (vioPayment === "success") {
|
|
3279
|
+
let outcome = "pending";
|
|
3280
|
+
let sawStatus = false;
|
|
3281
|
+
if (checkoutId) {
|
|
3282
|
+
this.open = true;
|
|
3283
|
+
this.paymentNotice = "Bekrefter betalingen\u2026";
|
|
3284
|
+
for (const delayMs of _VioCheckout.RETURN_VERIFY_DELAYS_MS) {
|
|
3285
|
+
if (delayMs > 0) await new Promise((r) => setTimeout(r, delayMs));
|
|
3286
|
+
try {
|
|
3287
|
+
const checkout = await Vio.checkout.getCheckout(checkoutId, sponsorId || void 0);
|
|
3288
|
+
const status = String(checkout?.status ?? "").toLowerCase();
|
|
3289
|
+
sawStatus = true;
|
|
3290
|
+
if (_VioCheckout.FAILED_STATUSES.has(status)) {
|
|
3291
|
+
outcome = "failed";
|
|
3292
|
+
break;
|
|
3293
|
+
}
|
|
3294
|
+
if (_VioCheckout.PAID_STATUSES.has(status) || Boolean(checkout?.origin_payment_id)) {
|
|
3295
|
+
outcome = "paid";
|
|
3296
|
+
break;
|
|
3297
|
+
}
|
|
3298
|
+
} catch (err) {
|
|
3299
|
+
if (typeof console !== "undefined") {
|
|
3300
|
+
console.warn("[VioCheckout] return verification attempt failed:", err);
|
|
3301
|
+
}
|
|
3302
|
+
}
|
|
3303
|
+
}
|
|
3304
|
+
this.paymentNotice = null;
|
|
3305
|
+
}
|
|
3306
|
+
this.applyReturnOutcome(outcome, sawStatus, vioMethod, sponsorId, checkoutId);
|
|
3307
|
+
this.cleanReturnQueryParams(["vio_payment", "vio_method", "vio_sponsor", "checkout_id"]);
|
|
3308
|
+
} else if (vioPayment === "cancel" || vioPayment === "error") {
|
|
3309
|
+
this.paymentError = "Betalingen ble avbrutt eller feilet. Vennligst pr\xF8v igjen.";
|
|
3310
|
+
this.open = true;
|
|
3311
|
+
this.cleanReturnQueryParams(["vio_payment", "vio_method", "vio_sponsor", "checkout_id"]);
|
|
3312
|
+
}
|
|
3313
|
+
} catch (err) {
|
|
3314
|
+
if (typeof console !== "undefined") {
|
|
3315
|
+
console.warn("[VioCheckout] checkReturnPaymentStatus failed:", err);
|
|
3316
|
+
}
|
|
3317
|
+
}
|
|
3318
|
+
}
|
|
3319
|
+
/** Remove our payment-return params from the URL without a reload. */
|
|
3320
|
+
cleanReturnQueryParams(keys) {
|
|
3321
|
+
try {
|
|
3322
|
+
if (typeof window === "undefined" || !window.history || !window.location) return;
|
|
3323
|
+
const url = new URL(window.location.href);
|
|
3324
|
+
let changed = false;
|
|
3325
|
+
keys.forEach((k) => {
|
|
3326
|
+
if (url.searchParams.has(k)) {
|
|
3327
|
+
url.searchParams.delete(k);
|
|
3328
|
+
changed = true;
|
|
3329
|
+
}
|
|
3330
|
+
});
|
|
3331
|
+
if (changed) {
|
|
3332
|
+
const newSearch = url.searchParams.toString();
|
|
3333
|
+
const newUrl = url.pathname + (newSearch ? "?" + newSearch : "") + url.hash;
|
|
3334
|
+
window.history.replaceState({}, document.title, newUrl);
|
|
3335
|
+
}
|
|
3336
|
+
} catch {
|
|
2562
3337
|
}
|
|
2563
3338
|
}
|
|
2564
3339
|
disconnectedCallback() {
|
|
@@ -2568,7 +3343,10 @@ var VioCheckout = class extends LitElement {
|
|
|
2568
3343
|
this.unmountKlarna();
|
|
2569
3344
|
super.disconnectedCallback();
|
|
2570
3345
|
}
|
|
2571
|
-
updated() {
|
|
3346
|
+
updated(changed) {
|
|
3347
|
+
if (changed.has("open") && this.open && this.availableShippingsList.length === 0) {
|
|
3348
|
+
void this.loadAvailableShippings();
|
|
3349
|
+
}
|
|
2572
3350
|
void this.mountKlarnaIfNeeded();
|
|
2573
3351
|
}
|
|
2574
3352
|
close() {
|
|
@@ -2578,6 +3356,83 @@ var VioCheckout = class extends LitElement {
|
|
|
2578
3356
|
}
|
|
2579
3357
|
show() {
|
|
2580
3358
|
this.open = true;
|
|
3359
|
+
if (this.availableShippingsList.length === 0) {
|
|
3360
|
+
void this.loadAvailableShippings();
|
|
3361
|
+
}
|
|
3362
|
+
}
|
|
3363
|
+
/** Fetch the real per-supplier shippings and preselect + persist the first. */
|
|
3364
|
+
async loadAvailableShippings() {
|
|
3365
|
+
const spId = this.checkoutState?.sponsorId;
|
|
3366
|
+
if (!spId) return;
|
|
3367
|
+
if (this.loadingShippings) return;
|
|
3368
|
+
this.loadingShippings = true;
|
|
3369
|
+
this.shippingsAttempted = true;
|
|
3370
|
+
try {
|
|
3371
|
+
const shippings = await Vio.checkout.fetchAvailableShippings(spId);
|
|
3372
|
+
if (Array.isArray(shippings) && shippings.length > 0) {
|
|
3373
|
+
this.availableShippingsList = shippings;
|
|
3374
|
+
if (!this.selectedShipping || !shippings.some((s) => s.id === this.selectedShipping)) {
|
|
3375
|
+
const firstOpt = shippings[0];
|
|
3376
|
+
this.selectedShipping = firstOpt.id;
|
|
3377
|
+
if (firstOpt.supplierId) {
|
|
3378
|
+
void Vio.checkout.updateShippingsBySupplier(
|
|
3379
|
+
[{ shipping_id: firstOpt.id, supplier_id: Number(firstOpt.supplierId) }],
|
|
3380
|
+
spId
|
|
3381
|
+
);
|
|
3382
|
+
}
|
|
3383
|
+
}
|
|
3384
|
+
}
|
|
3385
|
+
} catch (err) {
|
|
3386
|
+
if (typeof console !== "undefined") {
|
|
3387
|
+
console.warn("[VioCheckout] loadAvailableShippings failed:", err);
|
|
3388
|
+
}
|
|
3389
|
+
} finally {
|
|
3390
|
+
this.loadingShippings = false;
|
|
3391
|
+
}
|
|
3392
|
+
}
|
|
3393
|
+
/** Whether all mandatory Leveringsadresse fields are filled. */
|
|
3394
|
+
get isAddressValid() {
|
|
3395
|
+
const f = this.form;
|
|
3396
|
+
if (!f) return false;
|
|
3397
|
+
return Boolean(
|
|
3398
|
+
f.firstName && String(f.firstName).trim().length > 0 && f.lastName && String(f.lastName).trim().length > 0 && f.email && String(f.email).trim().length > 0 && String(f.email).includes("@") && f.address && String(f.address).trim().length > 0 && f.postalCode && String(f.postalCode).trim().length > 0 && f.city && String(f.city).trim().length > 0
|
|
3399
|
+
);
|
|
3400
|
+
}
|
|
3401
|
+
/** Whether a valid shipping option is selected (when shippings exist). */
|
|
3402
|
+
get hasSelectedShipping() {
|
|
3403
|
+
if (this.availableShippingsList.length === 0) return true;
|
|
3404
|
+
return Boolean(
|
|
3405
|
+
this.selectedShipping && this.shippingList.some((s) => s.id === this.selectedShipping)
|
|
3406
|
+
);
|
|
3407
|
+
}
|
|
3408
|
+
/** Which payment buttons to render — backend-configured per sponsor. */
|
|
3409
|
+
async loadAvailablePaymentMethods() {
|
|
3410
|
+
if (!this.checkoutState) return;
|
|
3411
|
+
if (this.loadingPaymentMethods) return;
|
|
3412
|
+
this.loadingPaymentMethods = true;
|
|
3413
|
+
try {
|
|
3414
|
+
const methods = await Vio.checkout.getAvailablePaymentMethods(
|
|
3415
|
+
this.checkoutState.sponsorId
|
|
3416
|
+
);
|
|
3417
|
+
if (Array.isArray(methods)) {
|
|
3418
|
+
this.availableMethods = methods.map((m) => m.name);
|
|
3419
|
+
} else {
|
|
3420
|
+
this.availableMethods = null;
|
|
3421
|
+
}
|
|
3422
|
+
} catch (err) {
|
|
3423
|
+
if (typeof console !== "undefined") {
|
|
3424
|
+
console.warn("[VioCheckout] Failed to load payment methods:", err);
|
|
3425
|
+
}
|
|
3426
|
+
this.availableMethods = null;
|
|
3427
|
+
} finally {
|
|
3428
|
+
this.loadingPaymentMethods = false;
|
|
3429
|
+
}
|
|
3430
|
+
}
|
|
3431
|
+
/** Backend names arrive as e.g. "Apple Pay" — compare letters only. */
|
|
3432
|
+
methodEnabled(name) {
|
|
3433
|
+
if (this.availableMethods === null) return true;
|
|
3434
|
+
const norm = (s) => s.toLowerCase().replace(/[^a-z]/g, "");
|
|
3435
|
+
return this.availableMethods.some((m) => norm(m) === norm(name));
|
|
2581
3436
|
}
|
|
2582
3437
|
async refreshApplePay() {
|
|
2583
3438
|
Vio.checkout.clearApplePayCache();
|
|
@@ -2616,6 +3471,12 @@ var VioCheckout = class extends LitElement {
|
|
|
2616
3471
|
this.klarnaMountedShipping = this.selectedShipping;
|
|
2617
3472
|
this.klarnaCategories = handle.categories;
|
|
2618
3473
|
this.klarnaSelectedCat = handle.selected;
|
|
3474
|
+
if (Array.isArray(handle.shippings) && handle.shippings.length > 0) {
|
|
3475
|
+
this.availableShippingsList = handle.shippings;
|
|
3476
|
+
if (!this.selectedShipping || !handle.shippings.some((s) => s.id === this.selectedShipping)) {
|
|
3477
|
+
this.selectedShipping = handle.shippings[0].id;
|
|
3478
|
+
}
|
|
3479
|
+
}
|
|
2619
3480
|
} catch (err) {
|
|
2620
3481
|
if (typeof console !== "undefined") {
|
|
2621
3482
|
console.warn("[VioCheckout] Klarna Payments mount failed:", err);
|
|
@@ -2636,14 +3497,33 @@ var VioCheckout = class extends LitElement {
|
|
|
2636
3497
|
this.klarnaSelectedCat = "";
|
|
2637
3498
|
this.klarnaAuthorizing = false;
|
|
2638
3499
|
}
|
|
3500
|
+
/** Shippings in effect: backend per-supplier when loaded, static otherwise. */
|
|
3501
|
+
get shippingList() {
|
|
3502
|
+
return this.availableShippingsList.length > 0 ? this.availableShippingsList : KLARNA_SHIPPING_OPTIONS;
|
|
3503
|
+
}
|
|
2639
3504
|
/** Selected shipping option (express flow). */
|
|
2640
3505
|
get shippingOption() {
|
|
2641
|
-
|
|
3506
|
+
const list = this.shippingList;
|
|
3507
|
+
return list.find((o) => o.id === this.selectedShipping) ?? list[0] ?? null;
|
|
2642
3508
|
}
|
|
2643
|
-
/** Pick a shipping option → re-mounts the Klarna widget with the new total.
|
|
2644
|
-
|
|
3509
|
+
/** Pick a shipping option → re-mounts the Klarna widget with the new total.
|
|
3510
|
+
* Also persists the choice on the backend cart (per supplier). */
|
|
3511
|
+
async onSelectShipping(id) {
|
|
2645
3512
|
if (this.selectedShipping === id || this.klarnaMounting) return;
|
|
2646
3513
|
this.selectedShipping = id;
|
|
3514
|
+
const selectedOpt = this.shippingList.find((s) => s.id === id);
|
|
3515
|
+
if (selectedOpt?.supplierId) {
|
|
3516
|
+
try {
|
|
3517
|
+
await Vio.checkout.updateShippingsBySupplier(
|
|
3518
|
+
[{ shipping_id: selectedOpt.id, supplier_id: Number(selectedOpt.supplierId) }],
|
|
3519
|
+
this.checkoutState?.sponsorId
|
|
3520
|
+
);
|
|
3521
|
+
} catch (err) {
|
|
3522
|
+
if (typeof console !== "undefined") {
|
|
3523
|
+
console.warn("[VioCheckout] updateShippingsBySupplier failed:", err);
|
|
3524
|
+
}
|
|
3525
|
+
}
|
|
3526
|
+
}
|
|
2647
3527
|
}
|
|
2648
3528
|
/** Switch the Klarna widget to a different payment_method_category. */
|
|
2649
3529
|
async onKlarnaCategory(identifier) {
|
|
@@ -2661,6 +3541,7 @@ var VioCheckout = class extends LitElement {
|
|
|
2661
3541
|
async onKlarnaAuthorize() {
|
|
2662
3542
|
if (!this.klarnaHandle || this.klarnaAuthorizing) return;
|
|
2663
3543
|
this.paymentError = null;
|
|
3544
|
+
this.paymentNotice = null;
|
|
2664
3545
|
this.klarnaAuthorizing = true;
|
|
2665
3546
|
try {
|
|
2666
3547
|
await this.klarnaHandle.authorize();
|
|
@@ -2685,24 +3566,80 @@ var VioCheckout = class extends LitElement {
|
|
|
2685
3566
|
this.applePayInProgress = false;
|
|
2686
3567
|
}
|
|
2687
3568
|
}
|
|
2688
|
-
onPay(method) {
|
|
3569
|
+
async onPay(method) {
|
|
2689
3570
|
this.paymentError = null;
|
|
3571
|
+
this.paymentNotice = null;
|
|
2690
3572
|
if (method === "apple-pay") {
|
|
2691
3573
|
void this.onApplePay();
|
|
2692
3574
|
return;
|
|
2693
3575
|
}
|
|
3576
|
+
if (method === "vipps") {
|
|
3577
|
+
const email = String(this.form.email ?? "").trim();
|
|
3578
|
+
if (!email || !email.includes("@")) {
|
|
3579
|
+
this.paymentError = "Vennligst fyll inn e-postadressen din.";
|
|
3580
|
+
return;
|
|
3581
|
+
}
|
|
3582
|
+
} else {
|
|
3583
|
+
if (!this.isAddressValid) {
|
|
3584
|
+
this.paymentError = "Vennligst fyll ut alle feltene i leveringsadresse.";
|
|
3585
|
+
return;
|
|
3586
|
+
}
|
|
3587
|
+
if (!this.hasSelectedShipping) {
|
|
3588
|
+
this.paymentError = "Vennligst velg en fraktmetode.";
|
|
3589
|
+
return;
|
|
3590
|
+
}
|
|
3591
|
+
}
|
|
2694
3592
|
Vio.checkout.selectPaymentMethod(method);
|
|
3593
|
+
if (method === "stripe") {
|
|
3594
|
+
const selectedOpt = this.shippingList.find((s) => s.id === this.selectedShipping);
|
|
3595
|
+
if (selectedOpt?.supplierId) {
|
|
3596
|
+
try {
|
|
3597
|
+
await Vio.checkout.updateShippingsBySupplier(
|
|
3598
|
+
[{ shipping_id: selectedOpt.id, supplier_id: Number(selectedOpt.supplierId) }],
|
|
3599
|
+
this.checkoutState?.sponsorId
|
|
3600
|
+
);
|
|
3601
|
+
} catch (err) {
|
|
3602
|
+
if (typeof console !== "undefined") {
|
|
3603
|
+
console.warn("[VioCheckout] updateShippingsBySupplier on stripe pay failed:", err);
|
|
3604
|
+
}
|
|
3605
|
+
}
|
|
3606
|
+
}
|
|
3607
|
+
this.stripeLoading = true;
|
|
3608
|
+
void Vio.checkout.startStripePayment(this.checkoutState?.sponsorId, this.form).catch((err) => {
|
|
3609
|
+
this.paymentError = `Kunne ikke starte Stripe-betaling: ${err instanceof Error ? err.message : String(err)}`;
|
|
3610
|
+
}).finally(() => {
|
|
3611
|
+
this.stripeLoading = false;
|
|
3612
|
+
});
|
|
3613
|
+
}
|
|
3614
|
+
if (method === "vipps") {
|
|
3615
|
+
this.vippsLoading = true;
|
|
3616
|
+
void Vio.checkout.startVippsPayment(this.checkoutState?.sponsorId, this.form).catch((err) => {
|
|
3617
|
+
this.paymentError = `Kunne ikke starte Vipps-betaling: ${err instanceof Error ? err.message : String(err)}`;
|
|
3618
|
+
}).finally(() => {
|
|
3619
|
+
this.vippsLoading = false;
|
|
3620
|
+
});
|
|
3621
|
+
}
|
|
2695
3622
|
}
|
|
2696
3623
|
/**
|
|
2697
|
-
* Complete the order for the selected method
|
|
2698
|
-
*
|
|
2699
|
-
*
|
|
2700
|
-
* the capture happens backend-side in the full flow — so this confirms the
|
|
2701
|
-
* order state-only and shows the confirmation screen.
|
|
3624
|
+
* Complete the order for the selected method. Apple Pay and Klarna Express
|
|
3625
|
+
* finish inside their own sheet/popup; Stripe and Vipps redirect to their
|
|
3626
|
+
* hosted pages (via onPay). Anything else confirms state-only.
|
|
2702
3627
|
*/
|
|
2703
3628
|
onCompleteOrder() {
|
|
2704
3629
|
const s = this.checkoutState;
|
|
2705
3630
|
if (!s || !s.paymentMethod) return;
|
|
3631
|
+
if (s.paymentMethod !== "vipps" && !this.isAddressValid) {
|
|
3632
|
+
this.paymentError = "Vennligst fyll ut alle feltene i leveringsadresse.";
|
|
3633
|
+
return;
|
|
3634
|
+
}
|
|
3635
|
+
if (s.paymentMethod === "stripe" || s.paymentMethod === "vipps") {
|
|
3636
|
+
void this.onPay(s.paymentMethod);
|
|
3637
|
+
return;
|
|
3638
|
+
}
|
|
3639
|
+
if (s.paymentMethod !== "klarna" && s.paymentMethod !== "apple-pay") {
|
|
3640
|
+
this.paymentError = "Betalingsmetoden er ikke tilgjengelig enn\xE5.";
|
|
3641
|
+
return;
|
|
3642
|
+
}
|
|
2706
3643
|
this.confirmOrder(s.paymentMethod, s.sponsorId);
|
|
2707
3644
|
}
|
|
2708
3645
|
/**
|
|
@@ -2739,8 +3676,10 @@ var VioCheckout = class extends LitElement {
|
|
|
2739
3676
|
return "Klarna";
|
|
2740
3677
|
case "vipps":
|
|
2741
3678
|
return "Vipps";
|
|
3679
|
+
case "stripe":
|
|
3680
|
+
return "Stripe";
|
|
2742
3681
|
case "card":
|
|
2743
|
-
return "
|
|
3682
|
+
return "Kort";
|
|
2744
3683
|
default:
|
|
2745
3684
|
return "";
|
|
2746
3685
|
}
|
|
@@ -2754,9 +3693,14 @@ var VioCheckout = class extends LitElement {
|
|
|
2754
3693
|
if (!s) return "";
|
|
2755
3694
|
return formatPrice(s.subtotal, s.currency);
|
|
2756
3695
|
}
|
|
2757
|
-
/** Shipping cost (major units)
|
|
3696
|
+
/** Shipping cost (major units) whenever a shipping option is in effect —
|
|
3697
|
+
* express always has one; the normal flow has one once the backend
|
|
3698
|
+
* shippings loaded (the choice IS persisted on the cart and charged). */
|
|
2758
3699
|
shippingMajor() {
|
|
2759
|
-
|
|
3700
|
+
if (!this.express && this.availableShippingsList.length === 0) return 0;
|
|
3701
|
+
const o = this.shippingOption;
|
|
3702
|
+
if (!o) return 0;
|
|
3703
|
+
return o.priceMajor ?? o.price / 100;
|
|
2760
3704
|
}
|
|
2761
3705
|
/** Total to charge — items + shipping (express). */
|
|
2762
3706
|
payTotalLabel() {
|
|
@@ -2815,24 +3759,26 @@ var VioCheckout = class extends LitElement {
|
|
|
2815
3759
|
}
|
|
2816
3760
|
/** Klarna Payments widget panel: shipping + category chips + widget + pay button. */
|
|
2817
3761
|
renderKlarnaPanel() {
|
|
2818
|
-
const currency = this.checkoutState?.currency
|
|
3762
|
+
const currency = this.checkoutState?.currency || getGlobalCurrency();
|
|
2819
3763
|
return html`
|
|
2820
3764
|
<div class="klarna-panel">
|
|
2821
3765
|
${this.express ? html`
|
|
2822
3766
|
<div class="ship-select" role="radiogroup" aria-label="Frakt">
|
|
2823
|
-
${
|
|
3767
|
+
${this.shippingList.map(
|
|
2824
3768
|
(o) => html`
|
|
2825
3769
|
<button
|
|
2826
3770
|
class="ship-opt ${this.selectedShipping === o.id ? "active" : ""}"
|
|
2827
3771
|
role="radio"
|
|
2828
3772
|
aria-checked=${this.selectedShipping === o.id}
|
|
2829
3773
|
?disabled=${this.klarnaMounting}
|
|
2830
|
-
@click=${() => this.onSelectShipping(o.id)}
|
|
3774
|
+
@click=${() => void this.onSelectShipping(o.id)}
|
|
2831
3775
|
>
|
|
2832
3776
|
<span class="ship-meta">
|
|
2833
|
-
<b>${o.method}</b><span>${o.description}</span>
|
|
3777
|
+
<b>${o.method || o.name}</b><span>${o.description}</span>
|
|
3778
|
+
</span>
|
|
3779
|
+
<span class="ship-price">
|
|
3780
|
+
${formatPrice(o.priceMajor ?? o.price / 100, o.currency || currency)}
|
|
2834
3781
|
</span>
|
|
2835
|
-
<span class="ship-price">${formatPrice(o.price / 100, currency)}</span>
|
|
2836
3782
|
</button>
|
|
2837
3783
|
`
|
|
2838
3784
|
)}
|
|
@@ -2852,19 +3798,43 @@ var VioCheckout = class extends LitElement {
|
|
|
2852
3798
|
)}
|
|
2853
3799
|
</div>
|
|
2854
3800
|
` : ""}
|
|
3801
|
+
${this.klarnaMounting ? html`
|
|
3802
|
+
<div style="padding: 20px; text-align: center; color: var(--vio-color-text-secondary, #666); font-size: 14px;">
|
|
3803
|
+
Klargjør Klarna-betaling…
|
|
3804
|
+
</div>
|
|
3805
|
+
` : ""}
|
|
2855
3806
|
<div class="klarna-widget" id="vio-klarna-payments-container"></div>
|
|
2856
3807
|
<button
|
|
2857
3808
|
class="payment-btn primary complete-cta"
|
|
2858
3809
|
@click=${() => void this.onKlarnaAuthorize()}
|
|
2859
3810
|
?disabled=${this.klarnaAuthorizing || !this.klarnaHandle || this.klarnaMounting}
|
|
2860
3811
|
>
|
|
2861
|
-
${this.klarnaAuthorizing ? "Behandler\u2026" : `Betal ${this.payTotalLabel()} med Klarna`}
|
|
3812
|
+
${this.klarnaAuthorizing ? "Behandler\u2026" : this.klarnaMounting ? "Klargj\xF8r Klarna\u2026" : `Betal ${this.payTotalLabel()} med Klarna`}
|
|
2862
3813
|
</button>
|
|
2863
3814
|
</div>
|
|
2864
3815
|
`;
|
|
2865
3816
|
}
|
|
2866
3817
|
/** Express layout — only the Klarna widget + a compact order summary. */
|
|
2867
3818
|
renderKlarnaExpress() {
|
|
3819
|
+
const currency = this.checkoutState?.currency || getGlobalCurrency();
|
|
3820
|
+
if (!this.klarnaAvailable) {
|
|
3821
|
+
return html`
|
|
3822
|
+
<section class="section">
|
|
3823
|
+
<h3 class="section-heading">Kjøp med Klarna</h3>
|
|
3824
|
+
<div class="payment-error">
|
|
3825
|
+
Klarna er ikke tilgjengelig akkurat nå (kan være blokkert av nettleseren).
|
|
3826
|
+
</div>
|
|
3827
|
+
<button
|
|
3828
|
+
class="payment-btn primary complete-cta"
|
|
3829
|
+
@click=${() => {
|
|
3830
|
+
this.express = false;
|
|
3831
|
+
}}
|
|
3832
|
+
>
|
|
3833
|
+
Gå til vanlig kasse
|
|
3834
|
+
</button>
|
|
3835
|
+
</section>
|
|
3836
|
+
`;
|
|
3837
|
+
}
|
|
2868
3838
|
return html`
|
|
2869
3839
|
<section class="section">
|
|
2870
3840
|
<h3 class="section-heading">Kjøp med Klarna</h3>
|
|
@@ -2874,13 +3844,13 @@ var VioCheckout = class extends LitElement {
|
|
|
2874
3844
|
(item) => html`
|
|
2875
3845
|
<div class="order-row">
|
|
2876
3846
|
<span>${item.brand ? `${item.brand} ` : ""}${item.name} ×${item.quantity}</span>
|
|
2877
|
-
<span>${formatPrice(item.unitPrice * item.quantity, item.currency)}</span>
|
|
3847
|
+
<span>${formatPrice(item.unitPrice * item.quantity, item.currency || currency)}</span>
|
|
2878
3848
|
</div>
|
|
2879
3849
|
`
|
|
2880
3850
|
)}
|
|
2881
3851
|
<div class="order-row">
|
|
2882
3852
|
<span>Frakt${this.shippingOption ? ` \u2013 ${this.shippingOption.method}` : ""}</span>
|
|
2883
|
-
<span>${formatPrice(this.shippingMajor(),
|
|
3853
|
+
<span>${formatPrice(this.shippingMajor(), currency)}</span>
|
|
2884
3854
|
</div>
|
|
2885
3855
|
<div class="order-row total">
|
|
2886
3856
|
<span>Totalt</span><span>${this.payTotalLabel()}</span>
|
|
@@ -2888,6 +3858,7 @@ var VioCheckout = class extends LitElement {
|
|
|
2888
3858
|
</div>
|
|
2889
3859
|
` : ""}
|
|
2890
3860
|
${this.renderKlarnaPanel()}
|
|
3861
|
+
${this.paymentNotice ? html`<div class="payment-notice">${this.paymentNotice}</div>` : ""}
|
|
2891
3862
|
${this.paymentError ? html`<div class="payment-error">${this.paymentError}</div>` : ""}
|
|
2892
3863
|
</section>
|
|
2893
3864
|
`;
|
|
@@ -2895,24 +3866,28 @@ var VioCheckout = class extends LitElement {
|
|
|
2895
3866
|
renderCheckoutBody() {
|
|
2896
3867
|
const method = this.checkoutState?.paymentMethod;
|
|
2897
3868
|
const showCompleteCta = !!method && !this.isExpressMethod(method);
|
|
3869
|
+
const isVipps = method === "vipps";
|
|
2898
3870
|
return html`
|
|
3871
|
+
${!isVipps ? html`
|
|
2899
3872
|
<section class="section">
|
|
2900
3873
|
<div class="section-label">Steg 1</div>
|
|
2901
3874
|
<h3 class="section-heading">Leveringsadresse</h3>
|
|
2902
3875
|
<div class="form-row">
|
|
2903
3876
|
<div class="field">
|
|
2904
|
-
<label>Fornavn
|
|
3877
|
+
<label>Fornavn *</label>
|
|
2905
3878
|
<input
|
|
2906
3879
|
type="text"
|
|
3880
|
+
required
|
|
2907
3881
|
autocomplete="given-name"
|
|
2908
3882
|
.value=${this.form.firstName}
|
|
2909
3883
|
@input=${(e) => this.onFieldChange("firstName", e.target.value)}
|
|
2910
3884
|
/>
|
|
2911
3885
|
</div>
|
|
2912
3886
|
<div class="field">
|
|
2913
|
-
<label>Etternavn
|
|
3887
|
+
<label>Etternavn *</label>
|
|
2914
3888
|
<input
|
|
2915
3889
|
type="text"
|
|
3890
|
+
required
|
|
2916
3891
|
autocomplete="family-name"
|
|
2917
3892
|
.value=${this.form.lastName}
|
|
2918
3893
|
@input=${(e) => this.onFieldChange("lastName", e.target.value)}
|
|
@@ -2920,18 +3895,20 @@ var VioCheckout = class extends LitElement {
|
|
|
2920
3895
|
</div>
|
|
2921
3896
|
</div>
|
|
2922
3897
|
<div class="field">
|
|
2923
|
-
<label>E-post
|
|
3898
|
+
<label>E-post *</label>
|
|
2924
3899
|
<input
|
|
2925
3900
|
type="email"
|
|
3901
|
+
required
|
|
2926
3902
|
autocomplete="email"
|
|
2927
3903
|
.value=${this.form.email}
|
|
2928
3904
|
@input=${(e) => this.onFieldChange("email", e.target.value)}
|
|
2929
3905
|
/>
|
|
2930
3906
|
</div>
|
|
2931
3907
|
<div class="field">
|
|
2932
|
-
<label>Adresse
|
|
3908
|
+
<label>Adresse *</label>
|
|
2933
3909
|
<input
|
|
2934
3910
|
type="text"
|
|
3911
|
+
required
|
|
2935
3912
|
autocomplete="street-address"
|
|
2936
3913
|
.value=${this.form.address}
|
|
2937
3914
|
@input=${(e) => this.onFieldChange("address", e.target.value)}
|
|
@@ -2939,79 +3916,182 @@ var VioCheckout = class extends LitElement {
|
|
|
2939
3916
|
</div>
|
|
2940
3917
|
<div class="form-row">
|
|
2941
3918
|
<div class="field">
|
|
2942
|
-
<label>Postnummer
|
|
3919
|
+
<label>Postnummer *</label>
|
|
2943
3920
|
<input
|
|
2944
3921
|
type="text"
|
|
3922
|
+
required
|
|
2945
3923
|
autocomplete="postal-code"
|
|
2946
3924
|
.value=${this.form.postalCode}
|
|
2947
3925
|
@input=${(e) => this.onFieldChange("postalCode", e.target.value)}
|
|
2948
3926
|
/>
|
|
2949
3927
|
</div>
|
|
2950
3928
|
<div class="field">
|
|
2951
|
-
<label>Sted
|
|
3929
|
+
<label>Sted *</label>
|
|
2952
3930
|
<input
|
|
2953
3931
|
type="text"
|
|
3932
|
+
required
|
|
2954
3933
|
autocomplete="address-level2"
|
|
2955
3934
|
.value=${this.form.city}
|
|
2956
3935
|
@input=${(e) => this.onFieldChange("city", e.target.value)}
|
|
2957
3936
|
/>
|
|
2958
3937
|
</div>
|
|
2959
3938
|
</div>
|
|
3939
|
+
${this.availableShippingsList.length > 0 ? html`
|
|
3940
|
+
<div style="margin-top: 16px;">
|
|
3941
|
+
<label style="display: block; font-weight: 600; font-size: 13px; margin-bottom: 8px;">
|
|
3942
|
+
Fraktmetode *
|
|
3943
|
+
</label>
|
|
3944
|
+
<div class="ship-select">
|
|
3945
|
+
${this.availableShippingsList.map(
|
|
3946
|
+
(o) => html`
|
|
3947
|
+
<button
|
|
3948
|
+
type="button"
|
|
3949
|
+
class="ship-opt ${this.selectedShipping === o.id ? "active" : ""}"
|
|
3950
|
+
@click=${() => void this.onSelectShipping(o.id)}
|
|
3951
|
+
>
|
|
3952
|
+
<span class="ship-meta">
|
|
3953
|
+
<b>${o.method || o.name}</b>
|
|
3954
|
+
${o.description ? html`<span>${o.description}</span>` : ""}
|
|
3955
|
+
</span>
|
|
3956
|
+
<span class="ship-price">
|
|
3957
|
+
${formatPrice(
|
|
3958
|
+
o.priceMajor ?? o.price / 100,
|
|
3959
|
+
o.currency || this.checkoutState?.currency || "NOK"
|
|
3960
|
+
)}
|
|
3961
|
+
</span>
|
|
3962
|
+
</button>
|
|
3963
|
+
`
|
|
3964
|
+
)}
|
|
3965
|
+
</div>
|
|
3966
|
+
</div>
|
|
3967
|
+
` : ""}
|
|
2960
3968
|
</section>
|
|
3969
|
+
` : ""}
|
|
3970
|
+
|
|
3971
|
+
${isVipps ? html`
|
|
3972
|
+
<section class="section">
|
|
3973
|
+
<div class="section-label">Kontakt</div>
|
|
3974
|
+
<div class="field">
|
|
3975
|
+
<label>E-post * <span style="font-weight:400;opacity:0.6">(for kvittering — adresse fylles ut i Vipps)</span></label>
|
|
3976
|
+
<input
|
|
3977
|
+
type="email"
|
|
3978
|
+
required
|
|
3979
|
+
autocomplete="email"
|
|
3980
|
+
.value=${this.form.email}
|
|
3981
|
+
@input=${(e) => this.onFieldChange("email", e.target.value)}
|
|
3982
|
+
/>
|
|
3983
|
+
</div>
|
|
3984
|
+
</section>
|
|
3985
|
+
` : ""}
|
|
2961
3986
|
|
|
2962
3987
|
<section class="section">
|
|
2963
|
-
<div class="section-label"
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
@click=${() => this.onPay("apple-pay")}
|
|
2970
|
-
?disabled=${this.applePayInProgress}
|
|
2971
|
-
aria-pressed=${this.checkoutState?.paymentMethod === "apple-pay"}
|
|
2972
|
-
>
|
|
2973
|
-
${this.applePayInProgress ? "\xC5pner\u2026" : " Pay"}
|
|
2974
|
-
</button>
|
|
2975
|
-
` : ""}
|
|
2976
|
-
${this.klarnaAvailable ? html`
|
|
2977
|
-
<button
|
|
2978
|
-
class="payment-btn"
|
|
2979
|
-
@click=${() => this.onPay("klarna")}
|
|
2980
|
-
aria-pressed=${this.checkoutState?.paymentMethod === "klarna"}
|
|
2981
|
-
>
|
|
2982
|
-
Klarna
|
|
2983
|
-
</button>
|
|
2984
|
-
` : ""}
|
|
2985
|
-
<button
|
|
2986
|
-
class="payment-btn"
|
|
2987
|
-
@click=${() => this.onPay("vipps")}
|
|
2988
|
-
aria-pressed=${this.checkoutState?.paymentMethod === "vipps"}
|
|
2989
|
-
>
|
|
2990
|
-
Vipps
|
|
2991
|
-
</button>
|
|
2992
|
-
<button
|
|
2993
|
-
class="payment-btn"
|
|
2994
|
-
@click=${() => this.onPay("card")}
|
|
2995
|
-
aria-pressed=${this.checkoutState?.paymentMethod === "card"}
|
|
2996
|
-
>
|
|
2997
|
-
Kort
|
|
2998
|
-
</button>
|
|
2999
|
-
${!this.applePayAvailable ? html`
|
|
3000
|
-
<div class="apple-pay-note">
|
|
3001
|
-
Pay krever Safari på iOS eller macOS med Apple Pay konfigurert.
|
|
3002
|
-
</div>
|
|
3003
|
-
` : ""}
|
|
3004
|
-
${showCompleteCta ? html`
|
|
3988
|
+
<div class="section-label">${isVipps ? "Betaling" : "Steg 2"}</div>
|
|
3989
|
+
${method ? html`
|
|
3990
|
+
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px;">
|
|
3991
|
+
<h3 class="section-heading" style="margin:0;">
|
|
3992
|
+
Betalingsmåte: <b>${this.methodLabel(method)}</b>
|
|
3993
|
+
</h3>
|
|
3005
3994
|
<button
|
|
3006
|
-
|
|
3007
|
-
|
|
3995
|
+
type="button"
|
|
3996
|
+
style="background:none; border:none; color:var(--vio-color-accent, #c14a3b); text-decoration:underline; font-size:13px; cursor:pointer;"
|
|
3997
|
+
@click=${() => {
|
|
3998
|
+
this.unmountKlarna();
|
|
3999
|
+
Vio.checkout.selectPaymentMethod("");
|
|
4000
|
+
}}
|
|
3008
4001
|
>
|
|
3009
|
-
|
|
4002
|
+
Endre
|
|
3010
4003
|
</button>
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
4004
|
+
</div>
|
|
4005
|
+
|
|
4006
|
+
${method === "klarna" ? this.renderKlarnaPanel() : ""}
|
|
4007
|
+
${method === "stripe" ? html`
|
|
4008
|
+
<button
|
|
4009
|
+
class="payment-btn primary complete-cta"
|
|
4010
|
+
@click=${() => void this.onPay("stripe")}
|
|
4011
|
+
?disabled=${this.stripeLoading || !this.isAddressValid || !this.hasSelectedShipping}
|
|
4012
|
+
title=${!this.isAddressValid ? "Vennligst fyll ut leveringsadresse" : !this.hasSelectedShipping ? "Vennligst velg en fraktmetode" : ""}
|
|
4013
|
+
>
|
|
4014
|
+
${this.stripeLoading ? "G\xE5r til Stripe\u2026" : `Betal ${this.payTotalLabel()} med Stripe`}
|
|
4015
|
+
</button>
|
|
4016
|
+
` : ""}
|
|
4017
|
+
${method === "vipps" ? html`
|
|
4018
|
+
<button
|
|
4019
|
+
class="payment-btn primary complete-cta"
|
|
4020
|
+
@click=${() => void this.onPay("vipps")}
|
|
4021
|
+
?disabled=${this.vippsLoading}
|
|
4022
|
+
>
|
|
4023
|
+
${this.vippsLoading ? "G\xE5r til Vipps\u2026" : `Betal ${this.payTotalLabel()} med Vipps`}
|
|
4024
|
+
</button>
|
|
4025
|
+
` : ""}
|
|
4026
|
+
${method === "apple-pay" ? html`
|
|
4027
|
+
<button
|
|
4028
|
+
class="payment-btn primary complete-cta"
|
|
4029
|
+
@click=${() => void this.onPay("apple-pay")}
|
|
4030
|
+
?disabled=${this.applePayInProgress || !this.isAddressValid || !this.hasSelectedShipping}
|
|
4031
|
+
title=${!this.isAddressValid ? "Vennligst fyll ut leveringsadresse" : !this.hasSelectedShipping ? "Vennligst velg en fraktmetode" : ""}
|
|
4032
|
+
>
|
|
4033
|
+
${this.applePayInProgress ? "\xC5pner\u2026" : `Betal ${this.payTotalLabel()} med Apple Pay`}
|
|
4034
|
+
</button>
|
|
4035
|
+
` : ""}
|
|
4036
|
+
${showCompleteCta && method !== "stripe" && method !== "vipps" ? html`
|
|
4037
|
+
<button
|
|
4038
|
+
class="payment-btn primary complete-cta"
|
|
4039
|
+
@click=${this.onCompleteOrder}
|
|
4040
|
+
?disabled=${!this.isAddressValid || !this.hasSelectedShipping}
|
|
4041
|
+
title=${!this.isAddressValid ? "Vennligst fyll ut leveringsadresse" : !this.hasSelectedShipping ? "Vennligst velg en fraktmetode" : ""}
|
|
4042
|
+
>
|
|
4043
|
+
Betal ${this.payTotalLabel()} med ${this.methodLabel(method ?? null)}
|
|
4044
|
+
</button>
|
|
4045
|
+
` : ""}
|
|
4046
|
+
${!isVipps && !this.isAddressValid ? html`
|
|
4047
|
+
<div style="font-size: 12px; color: var(--vio-color-accent, #c14a3b); text-align: center; margin-top: 8px;">
|
|
4048
|
+
Vennligst fyll ut leveringsadresse for å fullføre betalingen
|
|
4049
|
+
</div>
|
|
4050
|
+
` : !isVipps && !this.hasSelectedShipping ? html`
|
|
4051
|
+
<div style="font-size: 12px; color: var(--vio-color-accent, #c14a3b); text-align: center; margin-top: 8px;">
|
|
4052
|
+
Vennligst velg en fraktmetode for å fullføre betalingen
|
|
4053
|
+
</div>
|
|
4054
|
+
` : ""}
|
|
4055
|
+
` : html`
|
|
4056
|
+
<h3 class="section-heading">Velg betalingsmåte</h3>
|
|
4057
|
+
<div class="payment-grid">
|
|
4058
|
+
${this.methodEnabled("apple-pay") && this.applePayAvailable ? html`
|
|
4059
|
+
<button
|
|
4060
|
+
class="payment-btn primary"
|
|
4061
|
+
@click=${() => this.onPay("apple-pay")}
|
|
4062
|
+
?disabled=${this.applePayInProgress}
|
|
4063
|
+
>
|
|
4064
|
+
${this.applePayInProgress ? "\xC5pner\u2026" : html`<span style="display:inline-flex;align-items:center;gap:6px"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" style="width:18px;height:18px"><path d="M17.05 20.28c-.98.95-2.05.8-3.08.35-1.09-.46-2.09-.48-3.24 0-1.44.62-2.2.44-3.06-.35C2.79 15.25 3.51 7.59 9.05 7.31c1.35.07 2.29.74 3.08.8 1.18-.24 2.31-.93 3.57-.84 1.51.12 2.65.72 3.4 1.8-3.12 1.87-2.38 5.98.48 7.13-.57 1.5-1.31 2.99-2.54 4.09l.01-.01zM12.03 7.25c-.15-2.23 1.66-4.07 3.74-4.25.29 2.58-2.34 4.5-3.74 4.25z"/></svg>Apple Pay</span>`}
|
|
4065
|
+
</button>
|
|
4066
|
+
` : ""}
|
|
4067
|
+
${this.methodEnabled("stripe") ? html`
|
|
4068
|
+
<button
|
|
4069
|
+
class="payment-btn"
|
|
4070
|
+
@click=${() => this.onPay("stripe")}
|
|
4071
|
+
?disabled=${this.stripeLoading}
|
|
4072
|
+
>
|
|
4073
|
+
<img src=${STRIPE_LOGO_SRC3} style="height: 20px; display: block;" alt="Stripe" />
|
|
4074
|
+
</button>
|
|
4075
|
+
` : ""}
|
|
4076
|
+
${this.methodEnabled("klarna") && this.klarnaAvailable ? html`
|
|
4077
|
+
<button class="payment-btn" @click=${() => this.onPay("klarna")}>
|
|
4078
|
+
Klarna
|
|
4079
|
+
</button>
|
|
4080
|
+
` : ""}
|
|
4081
|
+
${this.methodEnabled("vipps") ? html`
|
|
4082
|
+
<button class="payment-btn" @click=${() => this.onPay("vipps")}>
|
|
4083
|
+
<span style="color:#ff5b24;font-weight:800;font-size:17px;letter-spacing:-0.02em">vipps</span>
|
|
4084
|
+
</button>
|
|
4085
|
+
` : ""}
|
|
4086
|
+
${this.methodEnabled("apple-pay") && !this.applePayAvailable ? html`
|
|
4087
|
+
<div class="apple-pay-note">
|
|
4088
|
+
Pay krever Safari på iOS eller macOS med Apple Pay konfigurert.
|
|
4089
|
+
</div>
|
|
4090
|
+
` : ""}
|
|
4091
|
+
</div>
|
|
4092
|
+
`}
|
|
4093
|
+
${this.paymentNotice ? html`<div class="payment-notice">${this.paymentNotice}</div>` : ""}
|
|
4094
|
+
${this.paymentError ? html`<div class="payment-error">${this.paymentError}</div>` : ""}
|
|
3015
4095
|
</section>
|
|
3016
4096
|
|
|
3017
4097
|
${this.items.length > 0 ? html`
|
|
@@ -3037,10 +4117,11 @@ var VioCheckout = class extends LitElement {
|
|
|
3037
4117
|
<span>Sum</span><span>${this.orderTotal()}</span>
|
|
3038
4118
|
</div>
|
|
3039
4119
|
<div class="order-row">
|
|
3040
|
-
<span>Frakt
|
|
4120
|
+
<span>Frakt${this.shippingMajor() > 0 && this.shippingOption ? ` \u2013 ${this.shippingOption.method || this.shippingOption.name}` : ""}</span>
|
|
4121
|
+
<span>${this.shippingMajor() > 0 ? formatPrice(this.shippingMajor(), this.checkoutState?.currency) : this.shippingLabel}</span>
|
|
3041
4122
|
</div>
|
|
3042
4123
|
<div class="order-row total">
|
|
3043
|
-
<span>Totalt</span><span>${this.
|
|
4124
|
+
<span>Totalt</span><span>${this.payTotalLabel()}</span>
|
|
3044
4125
|
</div>
|
|
3045
4126
|
</section>
|
|
3046
4127
|
` : ""}
|
|
@@ -3051,62 +4132,79 @@ var VioCheckout = class extends LitElement {
|
|
|
3051
4132
|
};
|
|
3052
4133
|
__decorateClass([
|
|
3053
4134
|
property({ type: Boolean, reflect: true })
|
|
3054
|
-
],
|
|
4135
|
+
], _VioCheckout.prototype, "open", 2);
|
|
3055
4136
|
__decorateClass([
|
|
3056
4137
|
property({ type: String })
|
|
3057
|
-
],
|
|
4138
|
+
], _VioCheckout.prototype, "heading", 2);
|
|
3058
4139
|
__decorateClass([
|
|
3059
4140
|
property({ type: String, attribute: "shipping-label" })
|
|
3060
|
-
],
|
|
4141
|
+
], _VioCheckout.prototype, "shippingLabel", 2);
|
|
3061
4142
|
__decorateClass([
|
|
3062
4143
|
property({ type: Boolean, reflect: true })
|
|
3063
|
-
],
|
|
4144
|
+
], _VioCheckout.prototype, "express", 2);
|
|
4145
|
+
__decorateClass([
|
|
4146
|
+
state()
|
|
4147
|
+
], _VioCheckout.prototype, "checkoutState", 2);
|
|
4148
|
+
__decorateClass([
|
|
4149
|
+
state()
|
|
4150
|
+
], _VioCheckout.prototype, "items", 2);
|
|
4151
|
+
__decorateClass([
|
|
4152
|
+
state()
|
|
4153
|
+
], _VioCheckout.prototype, "applePayAvailable", 2);
|
|
4154
|
+
__decorateClass([
|
|
4155
|
+
state()
|
|
4156
|
+
], _VioCheckout.prototype, "applePayInProgress", 2);
|
|
4157
|
+
__decorateClass([
|
|
4158
|
+
state()
|
|
4159
|
+
], _VioCheckout.prototype, "klarnaAvailable", 2);
|
|
3064
4160
|
__decorateClass([
|
|
3065
4161
|
state()
|
|
3066
|
-
],
|
|
4162
|
+
], _VioCheckout.prototype, "orderConfirmed", 2);
|
|
3067
4163
|
__decorateClass([
|
|
3068
4164
|
state()
|
|
3069
|
-
],
|
|
4165
|
+
], _VioCheckout.prototype, "confirmedMethod", 2);
|
|
3070
4166
|
__decorateClass([
|
|
3071
4167
|
state()
|
|
3072
|
-
],
|
|
4168
|
+
], _VioCheckout.prototype, "paymentError", 2);
|
|
3073
4169
|
__decorateClass([
|
|
3074
4170
|
state()
|
|
3075
|
-
],
|
|
4171
|
+
], _VioCheckout.prototype, "paymentNotice", 2);
|
|
3076
4172
|
__decorateClass([
|
|
3077
4173
|
state()
|
|
3078
|
-
],
|
|
4174
|
+
], _VioCheckout.prototype, "confirmedOrder", 2);
|
|
3079
4175
|
__decorateClass([
|
|
3080
4176
|
state()
|
|
3081
|
-
],
|
|
4177
|
+
], _VioCheckout.prototype, "klarnaCategories", 2);
|
|
3082
4178
|
__decorateClass([
|
|
3083
4179
|
state()
|
|
3084
|
-
],
|
|
4180
|
+
], _VioCheckout.prototype, "klarnaSelectedCat", 2);
|
|
3085
4181
|
__decorateClass([
|
|
3086
4182
|
state()
|
|
3087
|
-
],
|
|
4183
|
+
], _VioCheckout.prototype, "klarnaAuthorizing", 2);
|
|
3088
4184
|
__decorateClass([
|
|
3089
4185
|
state()
|
|
3090
|
-
],
|
|
4186
|
+
], _VioCheckout.prototype, "selectedShipping", 2);
|
|
3091
4187
|
__decorateClass([
|
|
3092
4188
|
state()
|
|
3093
|
-
],
|
|
4189
|
+
], _VioCheckout.prototype, "availableShippingsList", 2);
|
|
3094
4190
|
__decorateClass([
|
|
3095
4191
|
state()
|
|
3096
|
-
],
|
|
4192
|
+
], _VioCheckout.prototype, "availableMethods", 2);
|
|
3097
4193
|
__decorateClass([
|
|
3098
4194
|
state()
|
|
3099
|
-
],
|
|
4195
|
+
], _VioCheckout.prototype, "stripeLoading", 2);
|
|
3100
4196
|
__decorateClass([
|
|
3101
4197
|
state()
|
|
3102
|
-
],
|
|
4198
|
+
], _VioCheckout.prototype, "vippsLoading", 2);
|
|
3103
4199
|
__decorateClass([
|
|
3104
4200
|
state()
|
|
3105
|
-
],
|
|
4201
|
+
], _VioCheckout.prototype, "form", 2);
|
|
4202
|
+
var VioCheckout = _VioCheckout;
|
|
3106
4203
|
|
|
3107
4204
|
// src/ui/elements.ts
|
|
3108
4205
|
function registerVioElements() {
|
|
3109
|
-
if (typeof customElements === "undefined")
|
|
4206
|
+
if (typeof customElements === "undefined" || typeof customElements.get !== "function" || typeof customElements.define !== "function")
|
|
4207
|
+
return;
|
|
3110
4208
|
const defs = [
|
|
3111
4209
|
["vio-product", VioProduct],
|
|
3112
4210
|
["vio-product-carousel", VioProductCarousel],
|
|
@@ -3120,5 +4218,5 @@ function registerVioElements() {
|
|
|
3120
4218
|
}
|
|
3121
4219
|
|
|
3122
4220
|
export { VioCart, VioCheckout, VioProduct, VioProductCarousel, VioProductDetail, registerVioElements };
|
|
3123
|
-
//# sourceMappingURL=chunk-
|
|
3124
|
-
//# sourceMappingURL=chunk-
|
|
4221
|
+
//# sourceMappingURL=chunk-NWORYWIL.js.map
|
|
4222
|
+
//# sourceMappingURL=chunk-NWORYWIL.js.map
|