@vio-live/web-sdk 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,159 @@
1
- import { Vio, __decorateClass, primaryImageUrl, displayPrice, formatPrice, KLARNA_SHIPPING_OPTIONS } from './chunk-SOO2G6CF.js';
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
 
5
+ // src/ui/tokens.ts
6
+ var VioTokens = {
7
+ color: {
8
+ text: {
9
+ primary: "#0a0a0a",
10
+ secondary: "#666666",
11
+ tertiary: "#999999",
12
+ onPrimary: "#ffffff"
13
+ },
14
+ accent: {
15
+ editorial: "#c14a3b"
16
+ // "REDAKSJONENS FAVORITTER" red
17
+ },
18
+ surface: {
19
+ base: "#ffffff",
20
+ muted: "#f2f2f2",
21
+ // product image background
22
+ hover: "#fafafa"
23
+ },
24
+ border: {
25
+ subtle: "#e5e5e5",
26
+ default: "#cccccc"
27
+ }
28
+ },
29
+ font: {
30
+ serif: '"Tinos", "Playfair Display", Georgia, serif',
31
+ sans: '"Inter", -apple-system, "Helvetica Neue", Arial, sans-serif'
32
+ },
33
+ size: {
34
+ xs: "12px",
35
+ sm: "14px",
36
+ md: "16px",
37
+ lg: "20px",
38
+ xl: "24px",
39
+ "2xl": "32px",
40
+ "3xl": "40px"
41
+ },
42
+ weight: {
43
+ regular: 400,
44
+ medium: 500,
45
+ semibold: 600,
46
+ bold: 700
47
+ },
48
+ tracking: {
49
+ label: "0.1em",
50
+ // uppercase labels (BRAND, RETAILER, REDAKSJONENS FAVORITTER)
51
+ title: "0"
52
+ },
53
+ lineHeight: {
54
+ tight: 1.1,
55
+ snug: 1.3,
56
+ normal: 1.5
57
+ },
58
+ space: {
59
+ xs: "4px",
60
+ sm: "8px",
61
+ md: "16px",
62
+ lg: "24px",
63
+ xl: "32px",
64
+ "2xl": "48px",
65
+ "3xl": "64px"
66
+ },
67
+ radius: {
68
+ none: "0",
69
+ sm: "2px",
70
+ md: "4px",
71
+ lg: "8px",
72
+ full: "9999px"
73
+ }
74
+ };
75
+ var VIO_TOKENS_CSS = `
76
+ :root {
77
+ /* color \u2014 text */
78
+ --vio-color-text: ${VioTokens.color.text.primary};
79
+ --vio-color-text-secondary: ${VioTokens.color.text.secondary};
80
+ --vio-color-text-tertiary: ${VioTokens.color.text.tertiary};
81
+ --vio-color-text-on-primary:${VioTokens.color.text.onPrimary};
82
+
83
+ /* color \u2014 accent */
84
+ --vio-color-accent: ${VioTokens.color.accent.editorial};
85
+
86
+ /* color \u2014 surface */
87
+ --vio-color-surface: ${VioTokens.color.surface.base};
88
+ --vio-color-surface-muted: ${VioTokens.color.surface.muted};
89
+ --vio-color-surface-hover: ${VioTokens.color.surface.hover};
90
+
91
+ /* color \u2014 border */
92
+ --vio-color-border: ${VioTokens.color.border.subtle};
93
+ --vio-color-border-default: ${VioTokens.color.border.default};
94
+
95
+ /* font */
96
+ --vio-font-serif: ${VioTokens.font.serif};
97
+ --vio-font-sans: ${VioTokens.font.sans};
98
+
99
+ /* size */
100
+ --vio-size-xs: ${VioTokens.size.xs};
101
+ --vio-size-sm: ${VioTokens.size.sm};
102
+ --vio-size-md: ${VioTokens.size.md};
103
+ --vio-size-lg: ${VioTokens.size.lg};
104
+ --vio-size-xl: ${VioTokens.size.xl};
105
+
106
+ /* tracking (uppercase letterspacing) */
107
+ --vio-tracking-label: ${VioTokens.tracking.label};
108
+
109
+ /* space */
110
+ --vio-space-xs: ${VioTokens.space.xs};
111
+ --vio-space-sm: ${VioTokens.space.sm};
112
+ --vio-space-md: ${VioTokens.space.md};
113
+ --vio-space-lg: ${VioTokens.space.lg};
114
+ --vio-space-xl: ${VioTokens.space.xl};
115
+
116
+ /* radius */
117
+ --vio-radius-sm: ${VioTokens.radius.sm};
118
+ --vio-radius-md: ${VioTokens.radius.md};
119
+ --vio-radius-lg: ${VioTokens.radius.lg};
120
+ }
121
+ `;
122
+ function injectTokens() {
123
+ if (typeof document === "undefined") return;
124
+ if (document.querySelector('style[data-vio="tokens"]') !== null) return;
125
+ const style = document.createElement("style");
126
+ style.setAttribute("data-vio", "tokens");
127
+ style.textContent = VIO_TOKENS_CSS;
128
+ document.head.appendChild(style);
129
+ }
130
+ injectTokens();
131
+ var THEME_KEY_TO_CSS_VAR = {
132
+ colorText: "--vio-color-text",
133
+ colorTextSecondary: "--vio-color-text-secondary",
134
+ colorTextTertiary: "--vio-color-text-tertiary",
135
+ colorTextOnPrimary: "--vio-color-text-on-primary",
136
+ colorAccent: "--vio-color-accent",
137
+ colorSurface: "--vio-color-surface",
138
+ colorSurfaceMuted: "--vio-color-surface-muted",
139
+ colorSurfaceHover: "--vio-color-surface-hover",
140
+ colorBorder: "--vio-color-border",
141
+ colorBorderDefault: "--vio-color-border-default",
142
+ fontSerif: "--vio-font-serif",
143
+ fontSans: "--vio-font-sans"
144
+ };
145
+ function applyVioTheme(overrides, target = typeof document !== "undefined" ? document.documentElement : null) {
146
+ if (!target) return;
147
+ for (const key of Object.keys(THEME_KEY_TO_CSS_VAR)) {
148
+ const cssVar = THEME_KEY_TO_CSS_VAR[key];
149
+ const value = overrides[key];
150
+ if (value) {
151
+ target.style.setProperty(cssVar, value);
152
+ } else if (key in overrides) {
153
+ target.style.removeProperty(cssVar);
154
+ }
155
+ }
156
+ }
5
157
  var VioProduct = class extends LitElement {
6
158
  constructor() {
7
159
  super(...arguments);
@@ -13,6 +165,9 @@ var VioProduct = class extends LitElement {
13
165
  this.productId = "";
14
166
  this.sponsorId = "";
15
167
  this.hasVariants = false;
168
+ this.variants = void 0;
169
+ this.options = void 0;
170
+ this.hideMeta = false;
16
171
  this.inCart = false;
17
172
  this.cartQty = 0;
18
173
  this.justAdded = false;
@@ -185,7 +340,7 @@ var VioProduct = class extends LitElement {
185
340
  * `vio:product-add` so the host adds it (bumping quantity on repeat taps). */
186
341
  onAdd(e) {
187
342
  e.stopPropagation();
188
- if (this.hasVariants) {
343
+ if (this.hasVariants || Array.isArray(this.variants) && this.variants.length > 1 || Array.isArray(this.options) && this.options.length > 0) {
189
344
  this.onClick();
190
345
  return;
191
346
  }
@@ -236,12 +391,13 @@ var VioProduct = class extends LitElement {
236
391
  </svg>`}
237
392
  </button>
238
393
  </div>
394
+ ${this.hideMeta ? "" : html`
239
395
  <div class="meta">
240
396
  ${this.brand ? html`<div class="brand">${this.brand}</div>` : ""}
241
397
  ${this.name ? html`<div class="name">${this.name}</div>` : ""}
242
398
  ${this.price ? html`<div class="price">${this.price}</div>` : ""}
243
399
  ${this.retailer ? html`<div class="retailer">${this.retailer}</div>` : ""}
244
- </div>
400
+ </div>`}
245
401
  </div>
246
402
  `;
247
403
  }
@@ -270,6 +426,15 @@ __decorateClass([
270
426
  __decorateClass([
271
427
  property({ type: Boolean, attribute: "has-variants" })
272
428
  ], VioProduct.prototype, "hasVariants", 2);
429
+ __decorateClass([
430
+ property({ attribute: false })
431
+ ], VioProduct.prototype, "variants", 2);
432
+ __decorateClass([
433
+ property({ attribute: false })
434
+ ], VioProduct.prototype, "options", 2);
435
+ __decorateClass([
436
+ property({ type: Boolean, attribute: "hide-meta" })
437
+ ], VioProduct.prototype, "hideMeta", 2);
273
438
  __decorateClass([
274
439
  state()
275
440
  ], VioProduct.prototype, "inCart", 2);
@@ -625,6 +790,9 @@ function sponsorNameFromBootstrap(sponsorId) {
625
790
  if (boot.primarySponsor?.id === sponsorId) return boot.primarySponsor.name;
626
791
  return boot.secondarySponsors?.find((s) => s.id === sponsorId)?.name;
627
792
  }
793
+ var STRIPE_LOGO_SRC = "data:image/svg+xml;utf8," + encodeURIComponent(
794
+ '<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>'
795
+ );
628
796
  var VioProductDetail = class extends LitElement {
629
797
  constructor() {
630
798
  super(...arguments);
@@ -641,6 +809,12 @@ var VioProductDetail = class extends LitElement {
641
809
  this.activeImageIndex = 0;
642
810
  this.adding = false;
643
811
  this.applePayOk = false;
812
+ this.availableMethods = null;
813
+ /** Race guard: only the latest fetch may write state (two quick taps on
814
+ * different cards would otherwise let the slow response win). */
815
+ this.fetchSeq = 0;
816
+ /** Memoised option maps per variant object (recomputed per product). */
817
+ this.optMapCache = /* @__PURE__ */ new WeakMap();
644
818
  /** Pre-prepared Apple Pay handle — show() fires synchronously on tap. */
645
819
  this.applePayHandle = null;
646
820
  this.boundProductClick = (e) => {
@@ -652,6 +826,7 @@ var VioProductDetail = class extends LitElement {
652
826
  if (Number.isNaN(sponsorIdNum) || sponsorIdNum <= 0) return;
653
827
  this.productId = ev.detail.productId;
654
828
  this.sponsorId = sponsorIdNum;
829
+ this.currency = ev.detail.currency || getGlobalCurrency();
655
830
  this.show();
656
831
  void this.fetchProduct();
657
832
  };
@@ -890,6 +1065,13 @@ var VioProductDetail = class extends LitElement {
890
1065
  background: var(--vio-color-text, #0a0a0a);
891
1066
  color: var(--vio-color-text-on-primary, #fff);
892
1067
  }
1068
+ .option-pill.unavailable {
1069
+ opacity: 0.4;
1070
+ cursor: not-allowed;
1071
+ text-decoration: line-through;
1072
+ background: repeating-linear-gradient(-45deg, #fafafa, #fafafa 5px, #f0f0f0 5px, #f0f0f0 10px);
1073
+ }
1074
+ .option-pill.unavailable:hover { border-color: var(--vio-color-border, #e5e5e5); }
893
1075
 
894
1076
  /* Qty + CTA row */
895
1077
  .qty-cta-row {
@@ -960,6 +1142,35 @@ var VioProductDetail = class extends LitElement {
960
1142
  .buy-klarna .klarna-badge { height: 22px; width: auto; display: block; }
961
1143
  .buy-klarna:hover:not(:disabled) { background: #ffa0b9; }
962
1144
  .buy-klarna:disabled { opacity: 0.5; cursor: not-allowed; }
1145
+ .buy-applepay .ap-logo { width: 20px; height: 20px; }
1146
+ /* Vipps — brand orange buy button. */
1147
+ .buy-vipps {
1148
+ width: 100%;
1149
+ margin-top: 10px;
1150
+ display: flex;
1151
+ align-items: center;
1152
+ justify-content: center;
1153
+ gap: 8px;
1154
+ background: #ff5b24;
1155
+ color: #fff;
1156
+ border: none;
1157
+ padding: 14px 24px;
1158
+ font-size: 12px;
1159
+ text-transform: uppercase;
1160
+ letter-spacing: var(--vio-tracking-label, 0.1em);
1161
+ font-weight: 700;
1162
+ cursor: pointer;
1163
+ font-family: inherit;
1164
+ transition: background 0.15s;
1165
+ }
1166
+ .buy-vipps .vipps-badge {
1167
+ font-size: 20px;
1168
+ font-weight: 800;
1169
+ letter-spacing: -0.02em;
1170
+ text-transform: none;
1171
+ }
1172
+ .buy-vipps:hover:not(:disabled) { background: #ec4f1c; }
1173
+ .buy-vipps:disabled { opacity: 0.5; cursor: not-allowed; }
963
1174
  /* Apple Pay express button — black, per Apple's button guidelines. Only
964
1175
  shown in Safari (canApplePay). The glyph is the Apple logo (Safari). */
965
1176
  .buy-applepay {
@@ -1071,49 +1282,262 @@ var VioProductDetail = class extends LitElement {
1071
1282
  this.product = null;
1072
1283
  return;
1073
1284
  }
1285
+ const seq = ++this.fetchSeq;
1074
1286
  this.isLoading = true;
1075
1287
  this.fetchError = "";
1076
- try {
1077
- const commerce = Vio.commerceFor(this.sponsorId);
1078
- const products = await commerce.channel.product.getByIds({
1079
- product_ids: [productIdNum],
1080
- currency: this.currency,
1081
- image_size: "large"
1082
- });
1083
- this.product = products[0] ?? null;
1084
- if (this.product) {
1085
- const initial = {};
1086
- for (const opt of this.product.options) {
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;
1288
+ let products = null;
1289
+ let lastError = null;
1290
+ for (let attempt = 0; attempt < 4; attempt++) {
1291
+ try {
1292
+ const activeCurrency = getGlobalCurrency() || this.currency;
1293
+ this.currency = activeCurrency;
1294
+ const commerce = Vio.commerceFor(this.sponsorId);
1295
+ products = await commerce.channel.product.getByIds({
1296
+ product_ids: [productIdNum],
1297
+ currency: activeCurrency,
1298
+ image_size: "large"
1097
1299
  });
1300
+ lastError = null;
1301
+ break;
1302
+ } catch (err) {
1303
+ lastError = err;
1304
+ await new Promise((resolve) => setTimeout(resolve, 300));
1098
1305
  }
1099
- } catch (err) {
1100
- this.fetchError = err instanceof Error ? err.message : String(err);
1101
- } finally {
1306
+ }
1307
+ if (seq !== this.fetchSeq) return;
1308
+ if (lastError) {
1309
+ this.fetchError = lastError instanceof Error ? lastError.message : String(lastError);
1310
+ console.warn("[VioDetail] fetch failed after retries \u2192", this.fetchError);
1102
1311
  this.isLoading = false;
1312
+ return;
1313
+ }
1314
+ this.optMapCache = /* @__PURE__ */ new WeakMap();
1315
+ this.product = products?.[0] ?? null;
1316
+ if (!this.product) {
1317
+ this.fetchError = "Fant ikke produktet.";
1103
1318
  }
1319
+ if (this.product) {
1320
+ const initial = {};
1321
+ for (const opt of this.product.options ?? []) {
1322
+ const values = normalizeValues(opt.values);
1323
+ if (values.length > 0) initial[opt.name] = values[0];
1324
+ }
1325
+ this.selectedOptions = initial;
1326
+ void Vio.checkout.prepareApplePayFor(this.unitPrice * this.quantity, this.currency).then((handle) => {
1327
+ this.applePayHandle = handle;
1328
+ this.applePayOk = handle.available;
1329
+ }).catch(() => {
1330
+ this.applePayHandle = null;
1331
+ this.applePayOk = false;
1332
+ });
1333
+ void this.loadAvailablePaymentMethods();
1334
+ }
1335
+ this.isLoading = false;
1336
+ }
1337
+ /** Which buy buttons to render — backend-configured per sponsor. */
1338
+ async loadAvailablePaymentMethods() {
1339
+ const spId = this.sponsorId || globalThis.__VIO_SPONSOR_ID__;
1340
+ if (!spId) return;
1341
+ try {
1342
+ const methods = await Vio.checkout.getAvailablePaymentMethods(spId);
1343
+ if (Array.isArray(methods)) {
1344
+ this.availableMethods = methods.map((m) => m.name);
1345
+ } else {
1346
+ this.availableMethods = null;
1347
+ }
1348
+ } catch (err) {
1349
+ if (typeof console !== "undefined") {
1350
+ console.warn("[VioProductDetail] Failed to load payment methods:", err);
1351
+ }
1352
+ this.availableMethods = null;
1353
+ }
1354
+ }
1355
+ /** Backend names arrive as e.g. "Apple Pay" — compare letters only. */
1356
+ methodEnabled(...names) {
1357
+ if (this.availableMethods === null) return true;
1358
+ const norm = (s) => s.toLowerCase().replace(/[^a-z]/g, "");
1359
+ return this.availableMethods.some((m) => names.some((n) => norm(m) === norm(n)));
1360
+ }
1361
+ /* eslint-disable @typescript-eslint/no-explicit-any */
1362
+ /**
1363
+ * Build an option-name → value map for a variant, from every shape the feeds
1364
+ * ship: option1/2/3 columns, options/selected_options/variant arrays, plain
1365
+ * object maps, and finally the title (value scan first, position fallback).
1366
+ */
1367
+ getVariantOptionMap(v, productOptions) {
1368
+ if (v && typeof v === "object") {
1369
+ const cached = this.optMapCache.get(v);
1370
+ if (cached) return cached;
1371
+ }
1372
+ const map = {};
1373
+ if (!v) return { map, titleParts: [] };
1374
+ if (v.option1 != null && productOptions?.[0]) {
1375
+ map[productOptions[0].name.toLowerCase()] = String(v.option1).trim().toLowerCase();
1376
+ }
1377
+ if (v.option2 != null && productOptions?.[1]) {
1378
+ map[productOptions[1].name.toLowerCase()] = String(v.option2).trim().toLowerCase();
1379
+ }
1380
+ if (v.option3 != null && productOptions?.[2]) {
1381
+ map[productOptions[2].name.toLowerCase()] = String(v.option3).trim().toLowerCase();
1382
+ }
1383
+ const optArray = Array.isArray(v.options) ? v.options : Array.isArray(v.selected_options) ? v.selected_options : Array.isArray(v.variant) ? v.variant : null;
1384
+ if (optArray) {
1385
+ for (const item of optArray) {
1386
+ const key = (item.name || item.option || "").trim().toLowerCase();
1387
+ const val = (item.value || "").trim().toLowerCase();
1388
+ if (key && val) map[key] = val;
1389
+ }
1390
+ } else if (v.options && typeof v.options === "object") {
1391
+ for (const [k, val] of Object.entries(v.options)) {
1392
+ map[k.trim().toLowerCase()] = String(val).trim().toLowerCase();
1393
+ }
1394
+ }
1395
+ const title = String(v.title ?? "").trim();
1396
+ const titleLower = title.toLowerCase();
1397
+ if (Array.isArray(productOptions)) {
1398
+ for (const opt of productOptions) {
1399
+ const key = opt.name.toLowerCase();
1400
+ if (!map[key]) {
1401
+ const rawValues = normalizeValues(opt.values).filter(
1402
+ (val) => !productOptions.some(
1403
+ (other) => other !== opt && normalizeValues(other.values).some(
1404
+ (ov) => ov.trim().toLowerCase() === val.trim().toLowerCase()
1405
+ )
1406
+ )
1407
+ );
1408
+ const sortedVals = [...rawValues].sort((a, b) => b.length - a.length);
1409
+ for (const valStr of sortedVals) {
1410
+ const valLower = valStr.trim().toLowerCase();
1411
+ const escaped = valLower.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1412
+ const regex = new RegExp(`(?:^|[/|\\-,;\\s])${escaped}(?:$|[/|\\-,;\\s])`, "i");
1413
+ if (regex.test(titleLower) || titleLower === valLower) {
1414
+ map[key] = valLower;
1415
+ break;
1416
+ }
1417
+ }
1418
+ }
1419
+ }
1420
+ }
1421
+ const titleParts = title.split(/\s*[/|,]\s*|\s+-\s+/).map((s) => s.trim()).filter(Boolean);
1422
+ if (Array.isArray(productOptions)) {
1423
+ productOptions.forEach((opt, idx) => {
1424
+ const key = opt.name.toLowerCase();
1425
+ if (!map[key] && titleParts[idx] != null) {
1426
+ map[key] = titleParts[idx].toLowerCase();
1427
+ }
1428
+ });
1429
+ }
1430
+ const result = { map, titleParts };
1431
+ if (v && typeof v === "object") this.optMapCache.set(v, result);
1432
+ return result;
1104
1433
  }
1105
- /** Resolve the active variant from the selected option values. */
1434
+ /** Option values a variant represents (map values ∪ title parts). */
1435
+ variantOptionValues(v) {
1436
+ const { map, titleParts } = this.getVariantOptionMap(v, this.product?.options);
1437
+ const set = new Set(titleParts.map((s) => s.toLowerCase()));
1438
+ for (const val of Object.values(map)) set.add(val);
1439
+ return Array.from(set);
1440
+ }
1441
+ /** Does a variant part represent this option value? Exact match, plus a
1442
+ * whole-token match ONLY for numeric values ("30" matches "30 ml") — word
1443
+ * subsets of text values are not matches ("Large" must NOT match
1444
+ * "Extra Large"). */
1445
+ partMatchesValue(part, value) {
1446
+ if (!part || !value) return false;
1447
+ const p = String(part).trim().toLowerCase();
1448
+ const v = String(value).trim().toLowerCase();
1449
+ if (p === v) return true;
1450
+ if (!/^[\d.,]+$/.test(v)) return false;
1451
+ return p.split(/\s+/).includes(v);
1452
+ }
1453
+ /** Does a variant satisfy every currently-selected option value? */
1454
+ variantMatchesSelection(v, selection) {
1455
+ if (!v || !selection) return false;
1456
+ const entries = Object.entries(selection);
1457
+ if (entries.length === 0) return true;
1458
+ const { map, titleParts } = this.getVariantOptionMap(v, this.product?.options);
1459
+ const titlePartsLower = titleParts.map((t) => t.toLowerCase());
1460
+ return entries.every(([optName, val]) => {
1461
+ const key = optName.trim().toLowerCase();
1462
+ const targetVal = String(val).trim().toLowerCase();
1463
+ if (map[key]) {
1464
+ return this.partMatchesValue(map[key], targetVal);
1465
+ }
1466
+ return titlePartsLower.some((part) => this.partMatchesValue(part, targetVal));
1467
+ });
1468
+ }
1469
+ /** Sellable quantity for a variant, across the stock field zoo. Boolean
1470
+ * availability flags win; a numeric field wins next; `null` = the feed does
1471
+ * not report stock for this variant (callers fall back to product stock —
1472
+ * never assume "in stock" from absent data). */
1473
+ getVariantQty(v) {
1474
+ if (!v) return 0;
1475
+ if (v.available === false || v.in_stock === false || v.is_available === false) return 0;
1476
+ if (v.available === true || v.in_stock === true || v.is_available === true) return 999;
1477
+ const rawQty = v.quantity ?? v.inventory_quantity ?? v.stock ?? v.available_quantity ?? v.stock_quantity;
1478
+ if (rawQty !== null && rawQty !== void 0 && rawQty !== "") {
1479
+ const parsed = Number(rawQty);
1480
+ if (!Number.isNaN(parsed)) return parsed;
1481
+ }
1482
+ return null;
1483
+ }
1484
+ /** Variant stock with the product-level fallback applied (0 = not sellable). */
1485
+ variantSellableQty(v) {
1486
+ const q = this.getVariantQty(v);
1487
+ if (q !== null) return q;
1488
+ return this.product?.quantity ?? 0;
1489
+ }
1490
+ /** Resolve the active variant: full in-stock match → full match → best score
1491
+ * (2 per mapped option match, 1 per title match, +0.5 if in stock). */
1106
1492
  selectedVariant() {
1107
- if (!this.product || this.product.variants.length === 0) return null;
1108
- const vals = Object.values(this.selectedOptions);
1109
- if (vals.length === 0) return this.product.variants[0] ?? null;
1110
- const found = this.product.variants.find((v) => {
1111
- if (!v.title) return false;
1112
- return vals.every(
1113
- (val) => v.title.toLowerCase().includes(val.toLowerCase())
1114
- );
1493
+ if (!this.product || !Array.isArray(this.product.variants) || this.product.variants.length === 0) {
1494
+ return null;
1495
+ }
1496
+ const sel = this.selectedOptions;
1497
+ const entries = Object.entries(sel);
1498
+ if (entries.length === 0) return this.product.variants[0] ?? null;
1499
+ const fullInStock = this.product.variants.find(
1500
+ (v) => this.variantMatchesSelection(v, sel) && this.variantSellableQty(v) > 0
1501
+ );
1502
+ if (fullInStock) return fullInStock;
1503
+ const full = this.product.variants.find((v) => this.variantMatchesSelection(v, sel));
1504
+ if (full) return full;
1505
+ let bestVariant = null;
1506
+ let maxScore = -1;
1507
+ for (const v of this.product.variants) {
1508
+ const { map, titleParts } = this.getVariantOptionMap(v, this.product?.options);
1509
+ const titlePartsLower = titleParts.map((t) => t.toLowerCase());
1510
+ let score = 0;
1511
+ for (const [optName, val] of entries) {
1512
+ const key = optName.trim().toLowerCase();
1513
+ const targetVal = String(val).trim().toLowerCase();
1514
+ if (map[key] && this.partMatchesValue(map[key], targetVal)) {
1515
+ score += 2;
1516
+ } else if (titlePartsLower.some((part) => this.partMatchesValue(part, targetVal))) {
1517
+ score += 1;
1518
+ }
1519
+ }
1520
+ if (this.variantSellableQty(v) > 0) score += 0.5;
1521
+ if (score > maxScore) {
1522
+ maxScore = score;
1523
+ bestVariant = v;
1524
+ }
1525
+ }
1526
+ return bestVariant ?? this.product.variants[0] ?? null;
1527
+ }
1528
+ /** True if some in-stock variant carries this option value. */
1529
+ isValueAvailable(optionName, value) {
1530
+ if (!this.product || !Array.isArray(this.product.variants) || this.product.variants.length === 0) {
1531
+ return true;
1532
+ }
1533
+ const valLower = String(value).trim().toLowerCase();
1534
+ const key = optionName.trim().toLowerCase();
1535
+ return this.product.variants.some((v) => {
1536
+ const { map, titleParts } = this.getVariantOptionMap(v, this.product?.options);
1537
+ if (this.variantSellableQty(v) <= 0) return false;
1538
+ if (map[key]) return this.partMatchesValue(map[key], valLower);
1539
+ return titleParts.some((part) => this.partMatchesValue(part.toLowerCase(), valLower));
1115
1540
  });
1116
- return found ?? this.product.variants[0] ?? null;
1117
1541
  }
1118
1542
  get unitPrice() {
1119
1543
  const v = this.selectedVariant();
@@ -1127,7 +1551,7 @@ var VioProductDetail = class extends LitElement {
1127
1551
  }
1128
1552
  get availableQuantity() {
1129
1553
  const v = this.selectedVariant();
1130
- if (v) return v.quantity ?? 0;
1554
+ if (v) return this.variantSellableQty(v);
1131
1555
  return this.product?.quantity ?? 0;
1132
1556
  }
1133
1557
  stockLabel() {
@@ -1136,9 +1560,50 @@ var VioProductDetail = class extends LitElement {
1136
1560
  if (qty <= 3) return { text: `F\xE5 igjen \u2014 ${qty} stk`, cls: "low-stock" };
1137
1561
  return { text: "P\xE5 lager", cls: "in-stock" };
1138
1562
  }
1563
+ /** Pick an option value. If the exact combination doesn't exist, jump to an
1564
+ * in-stock variant that carries the value and adopt its full option map
1565
+ * (keeps the selection always resolvable to a real variant). */
1139
1566
  selectOption(optionName, value) {
1140
- this.selectedOptions = { ...this.selectedOptions, [optionName]: value };
1567
+ const newSelection = { ...this.selectedOptions, [optionName]: value };
1568
+ const exactInStock = this.product?.variants?.find(
1569
+ (v) => this.variantMatchesSelection(v, newSelection) && this.variantSellableQty(v) > 0
1570
+ );
1571
+ if (exactInStock) {
1572
+ this.selectedOptions = newSelection;
1573
+ return;
1574
+ }
1575
+ const exactAnyStock = this.product?.variants?.find(
1576
+ (v) => this.variantMatchesSelection(v, newSelection)
1577
+ );
1578
+ if (exactAnyStock) {
1579
+ this.selectedOptions = newSelection;
1580
+ return;
1581
+ }
1582
+ const valLower = String(value).trim().toLowerCase();
1583
+ const key = optionName.trim().toLowerCase();
1584
+ const anyInStockWithVal = this.product?.variants?.find((v) => {
1585
+ const { map, titleParts } = this.getVariantOptionMap(v, this.product?.options);
1586
+ if (this.variantSellableQty(v) <= 0) return false;
1587
+ if (map[key]) return this.partMatchesValue(map[key], valLower);
1588
+ return titleParts.some((p) => this.partMatchesValue(p.toLowerCase(), valLower));
1589
+ });
1590
+ if (anyInStockWithVal) {
1591
+ const { map } = this.getVariantOptionMap(anyInStockWithVal, this.product?.options);
1592
+ const updated = { ...newSelection };
1593
+ for (const opt of this.product?.options ?? []) {
1594
+ const k = opt.name.toLowerCase();
1595
+ if (map[k]) {
1596
+ const optValues = normalizeValues(opt.values);
1597
+ const origCase = optValues.find((val) => val.toLowerCase() === map[k]) || map[k];
1598
+ updated[opt.name] = origCase;
1599
+ }
1600
+ }
1601
+ this.selectedOptions = updated;
1602
+ } else {
1603
+ this.selectedOptions = newSelection;
1604
+ }
1141
1605
  }
1606
+ /* eslint-enable @typescript-eslint/no-explicit-any */
1142
1607
  allImages() {
1143
1608
  if (!this.product) return [];
1144
1609
  const variantImages = this.selectedVariant()?.images ?? [];
@@ -1209,6 +1674,32 @@ var VioProductDetail = class extends LitElement {
1209
1674
  this.adding = false;
1210
1675
  }
1211
1676
  }
1677
+ /** Buy with Vipps — opens the checkout with Vipps preselected. */
1678
+ buyWithVipps() {
1679
+ if (!this.product || this.availableQuantity <= 0 || this.adding) return;
1680
+ this.addCurrentToCart();
1681
+ this.close();
1682
+ this.dispatchEvent(
1683
+ new CustomEvent("vio:checkout-open", {
1684
+ bubbles: true,
1685
+ composed: true,
1686
+ detail: { sponsorId: this.sponsorId, paymentMethod: "vipps", express: false }
1687
+ })
1688
+ );
1689
+ }
1690
+ /** Buy with Stripe — opens the checkout with Stripe preselected. */
1691
+ buyWithStripe() {
1692
+ if (!this.product || this.availableQuantity <= 0 || this.adding) return;
1693
+ this.addCurrentToCart();
1694
+ this.close();
1695
+ this.dispatchEvent(
1696
+ new CustomEvent("vio:checkout-open", {
1697
+ bubbles: true,
1698
+ composed: true,
1699
+ detail: { sponsorId: this.sponsorId, paymentMethod: "stripe", express: false }
1700
+ })
1701
+ );
1702
+ }
1212
1703
  /**
1213
1704
  * Express buy with Apple Pay. Runs synchronously inside the click so the
1214
1705
  * Apple Pay sheet keeps the user gesture: add to cart → open checkout (sets
@@ -1333,17 +1824,35 @@ var VioProductDetail = class extends LitElement {
1333
1824
  </button>
1334
1825
  </div>
1335
1826
 
1336
- ${this.availableQuantity > 0 && this.applePayOk ? html`
1827
+ ${this.availableQuantity > 0 && this.applePayOk && this.methodEnabled("apple-pay", "applepay") ? html`
1337
1828
  <button
1338
1829
  class="buy-applepay"
1339
1830
  @click=${this.buyWithApplePay}
1340
1831
  ?disabled=${this.adding}
1341
1832
  aria-label="Kjøp nå med Apple Pay"
1342
1833
  >
1343
- Kjøp nå med Pay
1834
+ <span>Kjøp nå med</span>
1835
+ <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>
1836
+ <span>Apple Pay</span>
1837
+ </button>
1838
+ ` : ""}
1839
+ ${this.availableQuantity > 0 && this.methodEnabled("stripe") ? html`
1840
+ <button
1841
+ class="buy-stripe"
1842
+ @click=${this.buyWithStripe}
1843
+ ?disabled=${this.adding}
1844
+ aria-label="Kjøp nå med Stripe"
1845
+ 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;"
1846
+ >
1847
+ <span>Kjøp nå med</span>
1848
+ <img
1849
+ src=${STRIPE_LOGO_SRC}
1850
+ style="height: 20px; display: block; filter: brightness(0) invert(1);"
1851
+ alt="Stripe"
1852
+ />
1344
1853
  </button>
1345
1854
  ` : ""}
1346
- ${this.availableQuantity > 0 ? html`
1855
+ ${this.availableQuantity > 0 && this.methodEnabled("klarna") ? html`
1347
1856
  <button
1348
1857
  class="buy-klarna"
1349
1858
  @click=${this.buyWithKlarna}
@@ -1358,6 +1867,18 @@ var VioProductDetail = class extends LitElement {
1358
1867
  </button>
1359
1868
  ` : ""}
1360
1869
 
1870
+ ${this.availableQuantity > 0 && this.methodEnabled("vipps") ? html`
1871
+ <button
1872
+ class="buy-vipps"
1873
+ @click=${this.buyWithVipps}
1874
+ ?disabled=${this.adding}
1875
+ aria-label="Kjøp nå med Vipps"
1876
+ >
1877
+ <span>Kjøp nå med</span>
1878
+ <span class="vipps-badge">vipps</span>
1879
+ </button>
1880
+ ` : ""}
1881
+
1361
1882
  ${p.description ? html`
1362
1883
  <div>
1363
1884
  <div class="description-label">Beskrivelse</div>
@@ -1377,17 +1898,22 @@ var VioProductDetail = class extends LitElement {
1377
1898
  <div class="option-group">
1378
1899
  <div class="option-label">${opt.name}</div>
1379
1900
  <div class="option-values">
1380
- ${values.map(
1381
- (val) => html`
1901
+ ${values.map((val) => {
1902
+ const available = this.isValueAvailable(opt.name, val);
1903
+ const currentVal = this.selectedOptions[opt.name] ?? "";
1904
+ const isSelected = String(currentVal).trim().toLowerCase() === String(val).trim().toLowerCase();
1905
+ return html`
1382
1906
  <button
1383
- class="option-pill"
1907
+ class="option-pill ${available ? "" : "unavailable"}"
1384
1908
  @click=${() => this.selectOption(opt.name, val)}
1385
- aria-pressed=${this.selectedOptions[opt.name] === val}
1909
+ aria-pressed=${isSelected}
1910
+ ?disabled=${!available}
1911
+ title=${available ? "" : "Utsolgt"}
1386
1912
  >
1387
1913
  ${val}
1388
1914
  </button>
1389
- `
1390
- )}
1915
+ `;
1916
+ })}
1391
1917
  </div>
1392
1918
  </div>
1393
1919
  `;
@@ -1437,6 +1963,9 @@ __decorateClass([
1437
1963
  __decorateClass([
1438
1964
  state()
1439
1965
  ], VioProductDetail.prototype, "applePayOk", 2);
1966
+ __decorateClass([
1967
+ state()
1968
+ ], VioProductDetail.prototype, "availableMethods", 2);
1440
1969
  function normalizeValues(raw) {
1441
1970
  if (Array.isArray(raw)) {
1442
1971
  return raw.map((v) => String(v).trim()).filter((v) => v.length > 0);
@@ -1446,6 +1975,9 @@ function normalizeValues(raw) {
1446
1975
  }
1447
1976
  return [];
1448
1977
  }
1978
+ var STRIPE_LOGO_SRC2 = "data:image/svg+xml;utf8," + encodeURIComponent(
1979
+ '<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>'
1980
+ );
1449
1981
  var VioCart = class extends LitElement {
1450
1982
  constructor() {
1451
1983
  super(...arguments);
@@ -1456,15 +1988,21 @@ var VioCart = class extends LitElement {
1456
1988
  this.carts = /* @__PURE__ */ new Map();
1457
1989
  this.itemCount = 0;
1458
1990
  this.applePayAvailable = false;
1991
+ this.availableMethods = null;
1459
1992
  this.confirmedOrder = null;
1460
1993
  /** Pre-resolved Apple Pay handle; `show()` fires the sheet synchronously. */
1461
1994
  this.applePayHandle = null;
1462
1995
  /** Cart total Apple Pay was last prepared for (avoids redundant prep). */
1463
1996
  this.preparedTotal = -1;
1997
+ /** In-flight guard for loadAvailablePaymentMethods. */
1998
+ this.loadingPaymentMethods = false;
1464
1999
  this.boundOnCartChange = (e) => {
1465
2000
  const detail = e.detail;
1466
2001
  this.carts = detail.cartsBySponsor;
1467
2002
  this.itemCount = detail.itemCount;
2003
+ if (this.availableMethods === null) {
2004
+ void this.loadAvailablePaymentMethods();
2005
+ }
1468
2006
  };
1469
2007
  }
1470
2008
  static {
@@ -1672,7 +2210,7 @@ var VioCart = class extends LitElement {
1672
2210
  transition: opacity 0.15s;
1673
2211
  }
1674
2212
  .applepay-btn:hover { opacity: 0.86; }
1675
- .applepay-btn .ap-logo { font-size: 19px; line-height: 1; }
2213
+ .applepay-btn .ap-logo { width: 20px; height: 20px; }
1676
2214
  /* Klarna express — pink brand button, same express row as Apple Pay. */
1677
2215
  .klarna-btn {
1678
2216
  width: 100%;
@@ -1694,6 +2232,50 @@ var VioCart = class extends LitElement {
1694
2232
  }
1695
2233
  .klarna-btn:hover { background: #ffa1ba; }
1696
2234
  .klarna-btn .klarna-logo { height: 15px; width: auto; display: inline-block; }
2235
+ /* Vipps — brand orange button, same express row. */
2236
+ .vipps-btn {
2237
+ width: 100%;
2238
+ padding: 15px;
2239
+ margin-bottom: 10px;
2240
+ background: #ff5b24;
2241
+ color: #fff;
2242
+ border: none;
2243
+ border-radius: 8px;
2244
+ cursor: pointer;
2245
+ display: flex;
2246
+ align-items: center;
2247
+ justify-content: center;
2248
+ font-family: var(--vio-font-sans, -apple-system, sans-serif);
2249
+ transition: background 0.15s;
2250
+ }
2251
+ .vipps-btn:hover { background: #ec4f1c; }
2252
+ .vipps-btn .vipps-logo {
2253
+ font-size: 20px;
2254
+ font-weight: 800;
2255
+ letter-spacing: -0.02em;
2256
+ line-height: 1;
2257
+ }
2258
+ /* Stripe — brand blurple button, same express row. */
2259
+ .stripe-btn {
2260
+ width: 100%;
2261
+ padding: 15px;
2262
+ margin-bottom: 10px;
2263
+ background: #635bff;
2264
+ color: #fff;
2265
+ border: none;
2266
+ border-radius: 8px;
2267
+ font-size: 16px;
2268
+ font-weight: 600;
2269
+ cursor: pointer;
2270
+ display: flex;
2271
+ align-items: center;
2272
+ justify-content: center;
2273
+ gap: 6px;
2274
+ font-family: var(--vio-font-sans, -apple-system, sans-serif);
2275
+ transition: background 0.15s;
2276
+ }
2277
+ .stripe-btn:hover { background: #5248ff; }
2278
+ .stripe-btn .stripe-logo { height: 20px; width: auto; display: inline-block; filter: brightness(0) invert(1); }
1697
2279
 
1698
2280
  /* In-drawer confirmation after Apple Pay (nothing else is shown). */
1699
2281
  .confirm {
@@ -1768,6 +2350,7 @@ var VioCart = class extends LitElement {
1768
2350
  this.carts = Vio.cart.getAllCarts();
1769
2351
  this.itemCount = Vio.cart.itemCount;
1770
2352
  Vio.cart.addEventListener("change", this.boundOnCartChange);
2353
+ void this.loadAvailablePaymentMethods();
1771
2354
  }
1772
2355
  disconnectedCallback() {
1773
2356
  Vio.cart.removeEventListener("change", this.boundOnCartChange);
@@ -1778,6 +2361,39 @@ var VioCart = class extends LitElement {
1778
2361
  }
1779
2362
  show() {
1780
2363
  this.open = true;
2364
+ void this.loadAvailablePaymentMethods();
2365
+ }
2366
+ /** Which payment buttons to render — backend-configured per sponsor.
2367
+ * Falls back to showing everything if the query fails or none is set. */
2368
+ async loadAvailablePaymentMethods() {
2369
+ if (this.loadingPaymentMethods) return;
2370
+ this.loadingPaymentMethods = true;
2371
+ const firstSponsorId = [...this.carts.keys()][0] ?? globalThis.__VIO_SPONSOR_ID__;
2372
+ if (firstSponsorId === void 0) {
2373
+ this.loadingPaymentMethods = false;
2374
+ return;
2375
+ }
2376
+ try {
2377
+ const methods = await Vio.checkout.getAvailablePaymentMethods(firstSponsorId);
2378
+ if (Array.isArray(methods)) {
2379
+ this.availableMethods = methods.map((m) => m.name);
2380
+ } else {
2381
+ this.availableMethods = null;
2382
+ }
2383
+ } catch (err) {
2384
+ if (typeof console !== "undefined") {
2385
+ console.warn("[VioCart] Failed to load payment methods:", err);
2386
+ }
2387
+ this.availableMethods = null;
2388
+ } finally {
2389
+ this.loadingPaymentMethods = false;
2390
+ }
2391
+ }
2392
+ /** Backend names arrive as e.g. "Apple Pay" — compare letters only. */
2393
+ methodEnabled(name) {
2394
+ if (this.availableMethods === null) return true;
2395
+ const norm = (s) => s.toLowerCase().replace(/[^a-z]/g, "");
2396
+ return this.availableMethods.some((m) => norm(m) === norm(name));
1781
2397
  }
1782
2398
  updated(changed) {
1783
2399
  if (changed.has("open") && this.open) {
@@ -1864,6 +2480,32 @@ var VioCart = class extends LitElement {
1864
2480
  })
1865
2481
  );
1866
2482
  }
2483
+ /** Vipps — opens the checkout with Vipps preselected. */
2484
+ onVipps() {
2485
+ if (this.itemCount === 0) return;
2486
+ const firstSponsorId = [...this.carts.keys()][0];
2487
+ if (firstSponsorId === void 0) return;
2488
+ this.dispatchEvent(
2489
+ new CustomEvent("vio:checkout-open", {
2490
+ bubbles: true,
2491
+ composed: true,
2492
+ detail: { sponsorId: firstSponsorId, paymentMethod: "vipps", express: false }
2493
+ })
2494
+ );
2495
+ }
2496
+ /** Stripe — opens the checkout with Stripe preselected. */
2497
+ onStripe() {
2498
+ if (this.itemCount === 0) return;
2499
+ const firstSponsorId = [...this.carts.keys()][0];
2500
+ if (firstSponsorId === void 0) return;
2501
+ this.dispatchEvent(
2502
+ new CustomEvent("vio:checkout-open", {
2503
+ bubbles: true,
2504
+ composed: true,
2505
+ detail: { sponsorId: firstSponsorId, paymentMethod: "stripe", express: false }
2506
+ })
2507
+ );
2508
+ }
1867
2509
  get totalAcrossSponsors() {
1868
2510
  let sum = 0;
1869
2511
  for (const cart of this.carts.values()) {
@@ -1959,27 +2601,48 @@ var VioCart = class extends LitElement {
1959
2601
  ${formatPrice(this.totalAcrossSponsors, this.primaryCurrency)}
1960
2602
  </span>
1961
2603
  </div>
1962
- ${this.applePayAvailable ? html`
2604
+ ${this.methodEnabled("apple-pay") && this.applePayAvailable ? html`
1963
2605
  <button
1964
2606
  class="applepay-btn"
1965
2607
  @click=${this.onApplePay}
1966
2608
  aria-label="Betal med Apple Pay"
1967
2609
  >
1968
- <span class="ap-logo">${"\uF8FF"}</span> Pay
2610
+ <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>
2611
+ Apple Pay
2612
+ </button>
2613
+ ` : ""}
2614
+ ${this.methodEnabled("stripe") ? html`
2615
+ <button
2616
+ class="stripe-btn"
2617
+ @click=${this.onStripe}
2618
+ aria-label="Betal med Stripe"
2619
+ >
2620
+ <img class="stripe-logo" src=${STRIPE_LOGO_SRC2} alt="Stripe" />
2621
+ </button>
2622
+ ` : ""}
2623
+ ${this.methodEnabled("klarna") ? html`
2624
+ <button
2625
+ class="klarna-btn"
2626
+ @click=${this.onKlarnaExpress}
2627
+ aria-label="Betal med Klarna"
2628
+ >
2629
+ Kjøp med
2630
+ <img
2631
+ class="klarna-logo"
2632
+ src="https://x.klarnacdn.net/payment-method/assets/badges/generic/klarna.svg"
2633
+ alt="Klarna"
2634
+ />
2635
+ </button>
2636
+ ` : ""}
2637
+ ${this.methodEnabled("vipps") ? html`
2638
+ <button
2639
+ class="vipps-btn"
2640
+ @click=${this.onVipps}
2641
+ aria-label="Betal med Vipps"
2642
+ >
2643
+ <span class="vipps-logo">vipps</span>
1969
2644
  </button>
1970
2645
  ` : ""}
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
2646
  </div>
1984
2647
  `}
1985
2648
  </aside>
@@ -2033,10 +2696,16 @@ __decorateClass([
2033
2696
  __decorateClass([
2034
2697
  state()
2035
2698
  ], VioCart.prototype, "applePayAvailable", 2);
2699
+ __decorateClass([
2700
+ state()
2701
+ ], VioCart.prototype, "availableMethods", 2);
2036
2702
  __decorateClass([
2037
2703
  state()
2038
2704
  ], VioCart.prototype, "confirmedOrder", 2);
2039
- var VioCheckout = class extends LitElement {
2705
+ var STRIPE_LOGO_SRC3 = "data:image/svg+xml;utf8," + encodeURIComponent(
2706
+ '<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>'
2707
+ );
2708
+ var _VioCheckout = class _VioCheckout extends LitElement {
2040
2709
  constructor() {
2041
2710
  super(...arguments);
2042
2711
  this.open = false;
@@ -2051,6 +2720,7 @@ var VioCheckout = class extends LitElement {
2051
2720
  this.orderConfirmed = false;
2052
2721
  this.confirmedMethod = null;
2053
2722
  this.paymentError = null;
2723
+ this.paymentNotice = null;
2054
2724
  this.confirmedOrder = null;
2055
2725
  /** Active Klarna Payments widget handle + the subtotal it was mounted for. */
2056
2726
  this.klarnaHandle = null;
@@ -2061,6 +2731,10 @@ var VioCheckout = class extends LitElement {
2061
2731
  this.klarnaSelectedCat = "";
2062
2732
  this.klarnaAuthorizing = false;
2063
2733
  this.selectedShipping = KLARNA_SHIPPING_OPTIONS.find((o) => o.preselected)?.id ?? KLARNA_SHIPPING_OPTIONS[0]?.id ?? "";
2734
+ this.availableShippingsList = [];
2735
+ this.availableMethods = null;
2736
+ this.stripeLoading = false;
2737
+ this.vippsLoading = false;
2064
2738
  this.form = {
2065
2739
  firstName: "",
2066
2740
  lastName: "",
@@ -2069,13 +2743,27 @@ var VioCheckout = class extends LitElement {
2069
2743
  postalCode: "",
2070
2744
  city: ""
2071
2745
  };
2746
+ /** In-flight guard for loadAvailablePaymentMethods. */
2747
+ this.loadingPaymentMethods = false;
2748
+ /** In-flight + attempted guards for loadAvailableShippings (change events
2749
+ * fire per keystroke — without these, typing an address hammers the API). */
2750
+ this.loadingShippings = false;
2751
+ this.shippingsAttempted = false;
2072
2752
  this.boundOnCheckoutChange = (e) => {
2073
2753
  const detail = e.detail;
2754
+ const prevSponsorId = this.checkoutState?.sponsorId;
2755
+ const sponsorChanged = !prevSponsorId || prevSponsorId !== detail.state?.sponsorId;
2074
2756
  this.checkoutState = detail.state;
2075
2757
  this.items = Vio.checkout.items;
2076
2758
  if (this.checkoutState) {
2077
- this.refreshApplePay();
2078
- this.refreshKlarna();
2759
+ if (sponsorChanged || this.availableMethods === null) {
2760
+ void this.refreshApplePay();
2761
+ void this.refreshKlarna();
2762
+ void this.loadAvailablePaymentMethods();
2763
+ void this.loadAvailableShippings();
2764
+ } else if (!this.shippingsAttempted) {
2765
+ void this.loadAvailableShippings();
2766
+ }
2079
2767
  } else {
2080
2768
  this.applePayAvailable = false;
2081
2769
  this.klarnaAvailable = false;
@@ -2083,7 +2771,9 @@ var VioCheckout = class extends LitElement {
2083
2771
  this.confirmedMethod = null;
2084
2772
  this.confirmedOrder = null;
2085
2773
  this.paymentError = null;
2774
+ this.paymentNotice = null;
2086
2775
  this.express = false;
2776
+ this.availableMethods = null;
2087
2777
  this.unmountKlarna();
2088
2778
  }
2089
2779
  };
@@ -2164,6 +2854,21 @@ var VioCheckout = class extends LitElement {
2164
2854
  box-shadow: -8px 0 24px rgba(0, 0, 0, 0.1);
2165
2855
  }
2166
2856
  :host([open]) .modal.as-side { transform: translateX(0); }
2857
+
2858
+ /* Desktop: the checkout ALWAYS lives as a right-side panel (like the cart,
2859
+ product detail and express drawer) — never a full-width overlay. Only the
2860
+ compact confirmation card stays centered. */
2861
+ @media (min-width: 601px) {
2862
+ .modal:not(.as-confirm) {
2863
+ inset: 0 0 0 auto;
2864
+ width: min(440px, 100%);
2865
+ max-width: 440px;
2866
+ transform: translateX(100%);
2867
+ border-radius: 0;
2868
+ box-shadow: -8px 0 24px rgba(0, 0, 0, 0.1);
2869
+ }
2870
+ :host([open]) .modal:not(.as-confirm) { transform: translateX(0); }
2871
+ }
2167
2872
  @media (max-width: 600px) {
2168
2873
  /* On phones, keep it a bottom sheet for reachability. */
2169
2874
  .modal.as-side {
@@ -2344,8 +3049,9 @@ var VioCheckout = class extends LitElement {
2344
3049
  background: var(--vio-color-surface-hover, #fafafa);
2345
3050
  }
2346
3051
  .payment-btn:disabled {
2347
- cursor: progress;
2348
- opacity: 0.6;
3052
+ cursor: not-allowed;
3053
+ opacity: 0.45;
3054
+ pointer-events: none;
2349
3055
  }
2350
3056
  .payment-btn.primary {
2351
3057
  background: var(--vio-color-text, #0a0a0a);
@@ -2429,9 +3135,27 @@ var VioCheckout = class extends LitElement {
2429
3135
  color: var(--vio-color-accent, #c14a3b);
2430
3136
  text-align: center;
2431
3137
  }
3138
+ .payment-notice {
3139
+ grid-column: 1 / -1;
3140
+ padding: 12px 16px;
3141
+ background: #f4f4f2;
3142
+ border: 1px solid var(--vio-color-border, #ddd);
3143
+ border-radius: 8px;
3144
+ font-size: 13px;
3145
+ color: var(--vio-color-text, #1a1a1a);
3146
+ text-align: center;
3147
+ }
2432
3148
  /* "Betal" CTA — sits full-width below the method grid once a method is
2433
3149
  picked. Reuses .payment-btn.primary (its grid-column spans the row). */
2434
- .complete-cta { margin-top: 8px; }
3150
+ .complete-cta {
3151
+ width: 100% !important;
3152
+ box-sizing: border-box !important;
3153
+ margin-top: 8px;
3154
+ display: flex !important;
3155
+ align-items: center !important;
3156
+ justify-content: center !important;
3157
+ text-align: center !important;
3158
+ }
2435
3159
 
2436
3160
  /* Order confirmation — compact, shown inside the drawer. */
2437
3161
  .confirmation {
@@ -2559,6 +3283,209 @@ var VioCheckout = class extends LitElement {
2559
3283
  if (this.checkoutState) {
2560
3284
  void this.refreshApplePay();
2561
3285
  void this.refreshKlarna();
3286
+ void this.loadAvailablePaymentMethods();
3287
+ }
3288
+ void Vio.checkout.resumeKlarnaReturn();
3289
+ void this.checkReturnPaymentStatus();
3290
+ }
3291
+ static {
3292
+ /**
3293
+ * Detect the redirect return from a hosted payment page (Stripe / Vipps /
3294
+ * Klarna): the return URL carries vio_payment/vio_method/vio_sponsor. On
3295
+ * success: clear that sponsor's cart, show the confirmation drawer, emit
3296
+ * vio:payment-success (analytics/host hooks) and scrub the URL params.
3297
+ */
3298
+ /** Backend checkout statuses that mean the payment went through. */
3299
+ this.PAID_STATUSES = /* @__PURE__ */ new Set([
3300
+ "paid",
3301
+ "completed",
3302
+ "complete",
3303
+ "success",
3304
+ "succeeded",
3305
+ "confirmed",
3306
+ "captured"
3307
+ ]);
3308
+ }
3309
+ static {
3310
+ /** Statuses that mean the payment definitively did NOT go through. */
3311
+ this.FAILED_STATUSES = /* @__PURE__ */ new Set([
3312
+ "failed",
3313
+ "cancelled",
3314
+ "canceled",
3315
+ "rejected",
3316
+ "declined",
3317
+ "expired",
3318
+ "aborted",
3319
+ "terminated",
3320
+ "error",
3321
+ "voided",
3322
+ "unpaid"
3323
+ ]);
3324
+ }
3325
+ static {
3326
+ /** Waits between GetCheckout attempts — PSP webhooks (Vipps especially) can
3327
+ * land seconds after the shopper is redirected back, so a single immediate
3328
+ * check misreads a successful charge as unpaid. ~20s total. */
3329
+ this.RETURN_VERIFY_DELAYS_MS = [0, 2e3, 4e3, 6e3, 8e3];
3330
+ }
3331
+ static {
3332
+ /** Vipps polling cadence — up to 2 minutes. Its webhook can lag well behind
3333
+ * the redirect; this window was verified against a live checkout. */
3334
+ this.VIPPS_STATUS_MAX_ATTEMPTS = 24;
3335
+ }
3336
+ static {
3337
+ this.VIPPS_STATUS_DELAY_MS = 5e3;
3338
+ }
3339
+ /**
3340
+ * Vipps redirects to our return URL the same way on cancel as on success —
3341
+ * the URL alone can't tell them apart, and the generic checkout status can
3342
+ * lag behind Vipps' own webhook. Poll Vipps' own state directly instead:
3343
+ * AUTHORIZED confirms, CREATED means still processing (keep polling),
3344
+ * anything else (including an unrecognized state) is treated as failed —
3345
+ * fail-closed by default rather than guessing.
3346
+ */
3347
+ async pollVippsStatus(checkoutId, sponsorId) {
3348
+ let sawStatus = false;
3349
+ for (let attempt = 0; attempt < _VioCheckout.VIPPS_STATUS_MAX_ATTEMPTS; attempt++) {
3350
+ if (attempt > 0) {
3351
+ await new Promise((r) => setTimeout(r, _VioCheckout.VIPPS_STATUS_DELAY_MS));
3352
+ }
3353
+ try {
3354
+ const vippsStatus = await Vio.checkout.getVippsStatus(checkoutId, sponsorId || void 0);
3355
+ const state6 = String(vippsStatus?.state ?? "").toUpperCase();
3356
+ sawStatus = true;
3357
+ if (state6 === "AUTHORIZED") return { outcome: "paid", sawStatus };
3358
+ if (state6 === "CREATED") continue;
3359
+ return { outcome: "failed", sawStatus };
3360
+ } catch (err) {
3361
+ if (typeof console !== "undefined") {
3362
+ console.warn("[VioCheckout] GetVippsStatus attempt failed:", err);
3363
+ }
3364
+ }
3365
+ }
3366
+ return { outcome: "pending", sawStatus };
3367
+ }
3368
+ /** Shared outcome handler for both the generic (GetCheckout) and the
3369
+ * Vipps-specific (GetVippsStatus) return-verification paths. */
3370
+ applyReturnOutcome(outcome, sawStatus, vioMethod, sponsorId, checkoutId) {
3371
+ if (outcome === "paid") {
3372
+ if (sponsorId > 0) Vio.cart.clearSponsorCart(sponsorId);
3373
+ this.confirmedOrder = {
3374
+ items: [],
3375
+ currency: getGlobalCurrency(),
3376
+ total: 0,
3377
+ orderId: checkoutId || void 0
3378
+ };
3379
+ this.confirmedMethod = vioMethod;
3380
+ this.orderConfirmed = true;
3381
+ this.open = true;
3382
+ this.dispatchEvent(
3383
+ new CustomEvent("vio:payment-success", {
3384
+ bubbles: true,
3385
+ composed: true,
3386
+ detail: { method: vioMethod, sponsorId, orderId: checkoutId }
3387
+ })
3388
+ );
3389
+ } else if (outcome === "failed") {
3390
+ this.paymentError = "Betalingen ble avbrutt eller feilet. Vennligst pr\xF8v igjen.";
3391
+ this.open = true;
3392
+ } else if (sawStatus) {
3393
+ this.paymentNotice = "Betalingen behandles fortsatt. Du f\xE5r ordrebekreftelse p\xE5 e-post n\xE5r den er gjennomf\xF8rt. Handlekurven er uendret.";
3394
+ this.open = true;
3395
+ } else {
3396
+ this.paymentError = "Vi kunne ikke bekrefte betalingen. Handlekurven er uendret \u2014 pr\xF8v igjen, eller kontakt support hvis du ble belastet.";
3397
+ this.open = true;
3398
+ }
3399
+ }
3400
+ async checkReturnPaymentStatus() {
3401
+ if (typeof window === "undefined" || !window.location || !window.location.search) return;
3402
+ try {
3403
+ const urlParams = new URLSearchParams(window.location.search);
3404
+ const vioPayment = urlParams.get("vio_payment");
3405
+ const vioMethod = urlParams.get("vio_method") || "stripe";
3406
+ const sponsorId = Number(urlParams.get("vio_sponsor") || 0);
3407
+ const checkoutId = urlParams.get("checkout_id") || "";
3408
+ if (!vioPayment && !checkoutId) return;
3409
+ if (vioMethod === "klarna" && typeof sessionStorage !== "undefined" && sessionStorage.getItem("vio.klarna.pending.v1")) {
3410
+ this.cleanReturnQueryParams(["vio_payment", "vio_method", "vio_sponsor", "checkout_id"]);
3411
+ return;
3412
+ }
3413
+ if (vioMethod === "vipps" && checkoutId) {
3414
+ if (!this.checkoutState) {
3415
+ const spIdToUse = sponsorId || [...Vio.cart.getAllCarts().keys()][0] || 0;
3416
+ if (spIdToUse) {
3417
+ try {
3418
+ Vio.checkout.open(spIdToUse);
3419
+ Vio.checkout.selectPaymentMethod("vipps");
3420
+ } catch {
3421
+ }
3422
+ }
3423
+ }
3424
+ this.open = true;
3425
+ this.paymentNotice = "Bekrefter betalingen med Vipps\u2026";
3426
+ const { outcome, sawStatus } = await this.pollVippsStatus(checkoutId, sponsorId);
3427
+ this.paymentNotice = null;
3428
+ this.applyReturnOutcome(outcome, sawStatus, vioMethod, sponsorId, checkoutId);
3429
+ this.cleanReturnQueryParams(["vio_payment", "vio_method", "vio_sponsor", "checkout_id"]);
3430
+ } else if (vioPayment === "success") {
3431
+ let outcome = "pending";
3432
+ let sawStatus = false;
3433
+ if (checkoutId) {
3434
+ this.open = true;
3435
+ this.paymentNotice = "Bekrefter betalingen\u2026";
3436
+ for (const delayMs of _VioCheckout.RETURN_VERIFY_DELAYS_MS) {
3437
+ if (delayMs > 0) await new Promise((r) => setTimeout(r, delayMs));
3438
+ try {
3439
+ const checkout = await Vio.checkout.getCheckout(checkoutId, sponsorId || void 0);
3440
+ const status = String(checkout?.status ?? "").toLowerCase();
3441
+ sawStatus = true;
3442
+ if (_VioCheckout.FAILED_STATUSES.has(status)) {
3443
+ outcome = "failed";
3444
+ break;
3445
+ }
3446
+ if (_VioCheckout.PAID_STATUSES.has(status) || Boolean(checkout?.origin_payment_id)) {
3447
+ outcome = "paid";
3448
+ break;
3449
+ }
3450
+ } catch (err) {
3451
+ if (typeof console !== "undefined") {
3452
+ console.warn("[VioCheckout] return verification attempt failed:", err);
3453
+ }
3454
+ }
3455
+ }
3456
+ this.paymentNotice = null;
3457
+ }
3458
+ this.applyReturnOutcome(outcome, sawStatus, vioMethod, sponsorId, checkoutId);
3459
+ this.cleanReturnQueryParams(["vio_payment", "vio_method", "vio_sponsor", "checkout_id"]);
3460
+ } else if (vioPayment === "cancel" || vioPayment === "error") {
3461
+ this.paymentError = "Betalingen ble avbrutt eller feilet. Vennligst pr\xF8v igjen.";
3462
+ this.open = true;
3463
+ this.cleanReturnQueryParams(["vio_payment", "vio_method", "vio_sponsor", "checkout_id"]);
3464
+ }
3465
+ } catch (err) {
3466
+ if (typeof console !== "undefined") {
3467
+ console.warn("[VioCheckout] checkReturnPaymentStatus failed:", err);
3468
+ }
3469
+ }
3470
+ }
3471
+ /** Remove our payment-return params from the URL without a reload. */
3472
+ cleanReturnQueryParams(keys) {
3473
+ try {
3474
+ if (typeof window === "undefined" || !window.history || !window.location) return;
3475
+ const url = new URL(window.location.href);
3476
+ let changed = false;
3477
+ keys.forEach((k) => {
3478
+ if (url.searchParams.has(k)) {
3479
+ url.searchParams.delete(k);
3480
+ changed = true;
3481
+ }
3482
+ });
3483
+ if (changed) {
3484
+ const newSearch = url.searchParams.toString();
3485
+ const newUrl = url.pathname + (newSearch ? "?" + newSearch : "") + url.hash;
3486
+ window.history.replaceState({}, document.title, newUrl);
3487
+ }
3488
+ } catch {
2562
3489
  }
2563
3490
  }
2564
3491
  disconnectedCallback() {
@@ -2568,7 +3495,10 @@ var VioCheckout = class extends LitElement {
2568
3495
  this.unmountKlarna();
2569
3496
  super.disconnectedCallback();
2570
3497
  }
2571
- updated() {
3498
+ updated(changed) {
3499
+ if (changed.has("open") && this.open && this.availableShippingsList.length === 0) {
3500
+ void this.loadAvailableShippings();
3501
+ }
2572
3502
  void this.mountKlarnaIfNeeded();
2573
3503
  }
2574
3504
  close() {
@@ -2578,6 +3508,83 @@ var VioCheckout = class extends LitElement {
2578
3508
  }
2579
3509
  show() {
2580
3510
  this.open = true;
3511
+ if (this.availableShippingsList.length === 0) {
3512
+ void this.loadAvailableShippings();
3513
+ }
3514
+ }
3515
+ /** Fetch the real per-supplier shippings and preselect + persist the first. */
3516
+ async loadAvailableShippings() {
3517
+ const spId = this.checkoutState?.sponsorId;
3518
+ if (!spId) return;
3519
+ if (this.loadingShippings) return;
3520
+ this.loadingShippings = true;
3521
+ this.shippingsAttempted = true;
3522
+ try {
3523
+ const shippings = await Vio.checkout.fetchAvailableShippings(spId);
3524
+ if (Array.isArray(shippings) && shippings.length > 0) {
3525
+ this.availableShippingsList = shippings;
3526
+ if (!this.selectedShipping || !shippings.some((s) => s.id === this.selectedShipping)) {
3527
+ const firstOpt = shippings[0];
3528
+ this.selectedShipping = firstOpt.id;
3529
+ if (firstOpt.supplierId) {
3530
+ void Vio.checkout.updateShippingsBySupplier(
3531
+ [{ shipping_id: firstOpt.id, supplier_id: Number(firstOpt.supplierId) }],
3532
+ spId
3533
+ );
3534
+ }
3535
+ }
3536
+ }
3537
+ } catch (err) {
3538
+ if (typeof console !== "undefined") {
3539
+ console.warn("[VioCheckout] loadAvailableShippings failed:", err);
3540
+ }
3541
+ } finally {
3542
+ this.loadingShippings = false;
3543
+ }
3544
+ }
3545
+ /** Whether all mandatory Leveringsadresse fields are filled. */
3546
+ get isAddressValid() {
3547
+ const f = this.form;
3548
+ if (!f) return false;
3549
+ return Boolean(
3550
+ 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
3551
+ );
3552
+ }
3553
+ /** Whether a valid shipping option is selected (when shippings exist). */
3554
+ get hasSelectedShipping() {
3555
+ if (this.availableShippingsList.length === 0) return true;
3556
+ return Boolean(
3557
+ this.selectedShipping && this.shippingList.some((s) => s.id === this.selectedShipping)
3558
+ );
3559
+ }
3560
+ /** Which payment buttons to render — backend-configured per sponsor. */
3561
+ async loadAvailablePaymentMethods() {
3562
+ if (!this.checkoutState) return;
3563
+ if (this.loadingPaymentMethods) return;
3564
+ this.loadingPaymentMethods = true;
3565
+ try {
3566
+ const methods = await Vio.checkout.getAvailablePaymentMethods(
3567
+ this.checkoutState.sponsorId
3568
+ );
3569
+ if (Array.isArray(methods)) {
3570
+ this.availableMethods = methods.map((m) => m.name);
3571
+ } else {
3572
+ this.availableMethods = null;
3573
+ }
3574
+ } catch (err) {
3575
+ if (typeof console !== "undefined") {
3576
+ console.warn("[VioCheckout] Failed to load payment methods:", err);
3577
+ }
3578
+ this.availableMethods = null;
3579
+ } finally {
3580
+ this.loadingPaymentMethods = false;
3581
+ }
3582
+ }
3583
+ /** Backend names arrive as e.g. "Apple Pay" — compare letters only. */
3584
+ methodEnabled(name) {
3585
+ if (this.availableMethods === null) return true;
3586
+ const norm = (s) => s.toLowerCase().replace(/[^a-z]/g, "");
3587
+ return this.availableMethods.some((m) => norm(m) === norm(name));
2581
3588
  }
2582
3589
  async refreshApplePay() {
2583
3590
  Vio.checkout.clearApplePayCache();
@@ -2616,6 +3623,12 @@ var VioCheckout = class extends LitElement {
2616
3623
  this.klarnaMountedShipping = this.selectedShipping;
2617
3624
  this.klarnaCategories = handle.categories;
2618
3625
  this.klarnaSelectedCat = handle.selected;
3626
+ if (Array.isArray(handle.shippings) && handle.shippings.length > 0) {
3627
+ this.availableShippingsList = handle.shippings;
3628
+ if (!this.selectedShipping || !handle.shippings.some((s) => s.id === this.selectedShipping)) {
3629
+ this.selectedShipping = handle.shippings[0].id;
3630
+ }
3631
+ }
2619
3632
  } catch (err) {
2620
3633
  if (typeof console !== "undefined") {
2621
3634
  console.warn("[VioCheckout] Klarna Payments mount failed:", err);
@@ -2636,14 +3649,33 @@ var VioCheckout = class extends LitElement {
2636
3649
  this.klarnaSelectedCat = "";
2637
3650
  this.klarnaAuthorizing = false;
2638
3651
  }
3652
+ /** Shippings in effect: backend per-supplier when loaded, static otherwise. */
3653
+ get shippingList() {
3654
+ return this.availableShippingsList.length > 0 ? this.availableShippingsList : KLARNA_SHIPPING_OPTIONS;
3655
+ }
2639
3656
  /** Selected shipping option (express flow). */
2640
3657
  get shippingOption() {
2641
- return KLARNA_SHIPPING_OPTIONS.find((o) => o.id === this.selectedShipping) ?? null;
3658
+ const list = this.shippingList;
3659
+ return list.find((o) => o.id === this.selectedShipping) ?? list[0] ?? null;
2642
3660
  }
2643
- /** Pick a shipping option → re-mounts the Klarna widget with the new total. */
2644
- onSelectShipping(id) {
3661
+ /** Pick a shipping option → re-mounts the Klarna widget with the new total.
3662
+ * Also persists the choice on the backend cart (per supplier). */
3663
+ async onSelectShipping(id) {
2645
3664
  if (this.selectedShipping === id || this.klarnaMounting) return;
2646
3665
  this.selectedShipping = id;
3666
+ const selectedOpt = this.shippingList.find((s) => s.id === id);
3667
+ if (selectedOpt?.supplierId) {
3668
+ try {
3669
+ await Vio.checkout.updateShippingsBySupplier(
3670
+ [{ shipping_id: selectedOpt.id, supplier_id: Number(selectedOpt.supplierId) }],
3671
+ this.checkoutState?.sponsorId
3672
+ );
3673
+ } catch (err) {
3674
+ if (typeof console !== "undefined") {
3675
+ console.warn("[VioCheckout] updateShippingsBySupplier failed:", err);
3676
+ }
3677
+ }
3678
+ }
2647
3679
  }
2648
3680
  /** Switch the Klarna widget to a different payment_method_category. */
2649
3681
  async onKlarnaCategory(identifier) {
@@ -2661,6 +3693,7 @@ var VioCheckout = class extends LitElement {
2661
3693
  async onKlarnaAuthorize() {
2662
3694
  if (!this.klarnaHandle || this.klarnaAuthorizing) return;
2663
3695
  this.paymentError = null;
3696
+ this.paymentNotice = null;
2664
3697
  this.klarnaAuthorizing = true;
2665
3698
  try {
2666
3699
  await this.klarnaHandle.authorize();
@@ -2685,24 +3718,80 @@ var VioCheckout = class extends LitElement {
2685
3718
  this.applePayInProgress = false;
2686
3719
  }
2687
3720
  }
2688
- onPay(method) {
3721
+ async onPay(method) {
2689
3722
  this.paymentError = null;
3723
+ this.paymentNotice = null;
2690
3724
  if (method === "apple-pay") {
2691
3725
  void this.onApplePay();
2692
3726
  return;
2693
3727
  }
3728
+ if (method === "vipps") {
3729
+ const email = String(this.form.email ?? "").trim();
3730
+ if (!email || !email.includes("@")) {
3731
+ this.paymentError = "Vennligst fyll inn e-postadressen din.";
3732
+ return;
3733
+ }
3734
+ } else {
3735
+ if (!this.isAddressValid) {
3736
+ this.paymentError = "Vennligst fyll ut alle feltene i leveringsadresse.";
3737
+ return;
3738
+ }
3739
+ if (!this.hasSelectedShipping) {
3740
+ this.paymentError = "Vennligst velg en fraktmetode.";
3741
+ return;
3742
+ }
3743
+ }
2694
3744
  Vio.checkout.selectPaymentMethod(method);
3745
+ if (method === "stripe") {
3746
+ const selectedOpt = this.shippingList.find((s) => s.id === this.selectedShipping);
3747
+ if (selectedOpt?.supplierId) {
3748
+ try {
3749
+ await Vio.checkout.updateShippingsBySupplier(
3750
+ [{ shipping_id: selectedOpt.id, supplier_id: Number(selectedOpt.supplierId) }],
3751
+ this.checkoutState?.sponsorId
3752
+ );
3753
+ } catch (err) {
3754
+ if (typeof console !== "undefined") {
3755
+ console.warn("[VioCheckout] updateShippingsBySupplier on stripe pay failed:", err);
3756
+ }
3757
+ }
3758
+ }
3759
+ this.stripeLoading = true;
3760
+ void Vio.checkout.startStripePayment(this.checkoutState?.sponsorId, this.form).catch((err) => {
3761
+ this.paymentError = `Kunne ikke starte Stripe-betaling: ${err instanceof Error ? err.message : String(err)}`;
3762
+ }).finally(() => {
3763
+ this.stripeLoading = false;
3764
+ });
3765
+ }
3766
+ if (method === "vipps") {
3767
+ this.vippsLoading = true;
3768
+ void Vio.checkout.startVippsPayment(this.checkoutState?.sponsorId, this.form).catch((err) => {
3769
+ this.paymentError = `Kunne ikke starte Vipps-betaling: ${err instanceof Error ? err.message : String(err)}`;
3770
+ }).finally(() => {
3771
+ this.vippsLoading = false;
3772
+ });
3773
+ }
2695
3774
  }
2696
3775
  /**
2697
- * Complete the order for the selected method (Vipps / Card / plain Klarna).
2698
- * Apple Pay and Klarna Express finish inside their own sheet/popup, so they
2699
- * never reach here. No real PSP is wired for Vipps/Card in this iteration —
2700
- * the capture happens backend-side in the full flow — so this confirms the
2701
- * order state-only and shows the confirmation screen.
3776
+ * Complete the order for the selected method. Apple Pay and Klarna Express
3777
+ * finish inside their own sheet/popup; Stripe and Vipps redirect to their
3778
+ * hosted pages (via onPay). Anything else confirms state-only.
2702
3779
  */
2703
3780
  onCompleteOrder() {
2704
3781
  const s = this.checkoutState;
2705
3782
  if (!s || !s.paymentMethod) return;
3783
+ if (s.paymentMethod !== "vipps" && !this.isAddressValid) {
3784
+ this.paymentError = "Vennligst fyll ut alle feltene i leveringsadresse.";
3785
+ return;
3786
+ }
3787
+ if (s.paymentMethod === "stripe" || s.paymentMethod === "vipps") {
3788
+ void this.onPay(s.paymentMethod);
3789
+ return;
3790
+ }
3791
+ if (s.paymentMethod !== "klarna" && s.paymentMethod !== "apple-pay") {
3792
+ this.paymentError = "Betalingsmetoden er ikke tilgjengelig enn\xE5.";
3793
+ return;
3794
+ }
2706
3795
  this.confirmOrder(s.paymentMethod, s.sponsorId);
2707
3796
  }
2708
3797
  /**
@@ -2739,8 +3828,10 @@ var VioCheckout = class extends LitElement {
2739
3828
  return "Klarna";
2740
3829
  case "vipps":
2741
3830
  return "Vipps";
3831
+ case "stripe":
3832
+ return "Stripe";
2742
3833
  case "card":
2743
- return "kort";
3834
+ return "Kort";
2744
3835
  default:
2745
3836
  return "";
2746
3837
  }
@@ -2754,9 +3845,14 @@ var VioCheckout = class extends LitElement {
2754
3845
  if (!s) return "";
2755
3846
  return formatPrice(s.subtotal, s.currency);
2756
3847
  }
2757
- /** Shipping cost (major units) applied in express mode, else 0. */
3848
+ /** Shipping cost (major units) whenever a shipping option is in effect —
3849
+ * express always has one; the normal flow has one once the backend
3850
+ * shippings loaded (the choice IS persisted on the cart and charged). */
2758
3851
  shippingMajor() {
2759
- return this.express ? (this.shippingOption?.price ?? 0) / 100 : 0;
3852
+ if (!this.express && this.availableShippingsList.length === 0) return 0;
3853
+ const o = this.shippingOption;
3854
+ if (!o) return 0;
3855
+ return o.priceMajor ?? o.price / 100;
2760
3856
  }
2761
3857
  /** Total to charge — items + shipping (express). */
2762
3858
  payTotalLabel() {
@@ -2815,24 +3911,26 @@ var VioCheckout = class extends LitElement {
2815
3911
  }
2816
3912
  /** Klarna Payments widget panel: shipping + category chips + widget + pay button. */
2817
3913
  renderKlarnaPanel() {
2818
- const currency = this.checkoutState?.currency ?? "NOK";
3914
+ const currency = this.checkoutState?.currency || getGlobalCurrency();
2819
3915
  return html`
2820
3916
  <div class="klarna-panel">
2821
3917
  ${this.express ? html`
2822
3918
  <div class="ship-select" role="radiogroup" aria-label="Frakt">
2823
- ${KLARNA_SHIPPING_OPTIONS.map(
3919
+ ${this.shippingList.map(
2824
3920
  (o) => html`
2825
3921
  <button
2826
3922
  class="ship-opt ${this.selectedShipping === o.id ? "active" : ""}"
2827
3923
  role="radio"
2828
3924
  aria-checked=${this.selectedShipping === o.id}
2829
3925
  ?disabled=${this.klarnaMounting}
2830
- @click=${() => this.onSelectShipping(o.id)}
3926
+ @click=${() => void this.onSelectShipping(o.id)}
2831
3927
  >
2832
3928
  <span class="ship-meta">
2833
- <b>${o.method}</b><span>${o.description}</span>
3929
+ <b>${o.method || o.name}</b><span>${o.description}</span>
3930
+ </span>
3931
+ <span class="ship-price">
3932
+ ${formatPrice(o.priceMajor ?? o.price / 100, o.currency || currency)}
2834
3933
  </span>
2835
- <span class="ship-price">${formatPrice(o.price / 100, currency)}</span>
2836
3934
  </button>
2837
3935
  `
2838
3936
  )}
@@ -2852,19 +3950,43 @@ var VioCheckout = class extends LitElement {
2852
3950
  )}
2853
3951
  </div>
2854
3952
  ` : ""}
3953
+ ${this.klarnaMounting ? html`
3954
+ <div style="padding: 20px; text-align: center; color: var(--vio-color-text-secondary, #666); font-size: 14px;">
3955
+ Klargjør Klarna-betaling…
3956
+ </div>
3957
+ ` : ""}
2855
3958
  <div class="klarna-widget" id="vio-klarna-payments-container"></div>
2856
3959
  <button
2857
3960
  class="payment-btn primary complete-cta"
2858
3961
  @click=${() => void this.onKlarnaAuthorize()}
2859
3962
  ?disabled=${this.klarnaAuthorizing || !this.klarnaHandle || this.klarnaMounting}
2860
3963
  >
2861
- ${this.klarnaAuthorizing ? "Behandler\u2026" : `Betal ${this.payTotalLabel()} med Klarna`}
3964
+ ${this.klarnaAuthorizing ? "Behandler\u2026" : this.klarnaMounting ? "Klargj\xF8r Klarna\u2026" : `Betal ${this.payTotalLabel()} med Klarna`}
2862
3965
  </button>
2863
3966
  </div>
2864
3967
  `;
2865
3968
  }
2866
3969
  /** Express layout — only the Klarna widget + a compact order summary. */
2867
3970
  renderKlarnaExpress() {
3971
+ const currency = this.checkoutState?.currency || getGlobalCurrency();
3972
+ if (!this.klarnaAvailable) {
3973
+ return html`
3974
+ <section class="section">
3975
+ <h3 class="section-heading">Kjøp med Klarna</h3>
3976
+ <div class="payment-error">
3977
+ Klarna er ikke tilgjengelig akkurat nå (kan være blokkert av nettleseren).
3978
+ </div>
3979
+ <button
3980
+ class="payment-btn primary complete-cta"
3981
+ @click=${() => {
3982
+ this.express = false;
3983
+ }}
3984
+ >
3985
+ Gå til vanlig kasse
3986
+ </button>
3987
+ </section>
3988
+ `;
3989
+ }
2868
3990
  return html`
2869
3991
  <section class="section">
2870
3992
  <h3 class="section-heading">Kjøp med Klarna</h3>
@@ -2874,13 +3996,13 @@ var VioCheckout = class extends LitElement {
2874
3996
  (item) => html`
2875
3997
  <div class="order-row">
2876
3998
  <span>${item.brand ? `${item.brand} ` : ""}${item.name} ×${item.quantity}</span>
2877
- <span>${formatPrice(item.unitPrice * item.quantity, item.currency)}</span>
3999
+ <span>${formatPrice(item.unitPrice * item.quantity, item.currency || currency)}</span>
2878
4000
  </div>
2879
4001
  `
2880
4002
  )}
2881
4003
  <div class="order-row">
2882
4004
  <span>Frakt${this.shippingOption ? ` \u2013 ${this.shippingOption.method}` : ""}</span>
2883
- <span>${formatPrice(this.shippingMajor(), this.checkoutState?.currency ?? "NOK")}</span>
4005
+ <span>${formatPrice(this.shippingMajor(), currency)}</span>
2884
4006
  </div>
2885
4007
  <div class="order-row total">
2886
4008
  <span>Totalt</span><span>${this.payTotalLabel()}</span>
@@ -2888,6 +4010,7 @@ var VioCheckout = class extends LitElement {
2888
4010
  </div>
2889
4011
  ` : ""}
2890
4012
  ${this.renderKlarnaPanel()}
4013
+ ${this.paymentNotice ? html`<div class="payment-notice">${this.paymentNotice}</div>` : ""}
2891
4014
  ${this.paymentError ? html`<div class="payment-error">${this.paymentError}</div>` : ""}
2892
4015
  </section>
2893
4016
  `;
@@ -2895,24 +4018,28 @@ var VioCheckout = class extends LitElement {
2895
4018
  renderCheckoutBody() {
2896
4019
  const method = this.checkoutState?.paymentMethod;
2897
4020
  const showCompleteCta = !!method && !this.isExpressMethod(method);
4021
+ const isVipps = method === "vipps";
2898
4022
  return html`
4023
+ ${!isVipps ? html`
2899
4024
  <section class="section">
2900
4025
  <div class="section-label">Steg 1</div>
2901
4026
  <h3 class="section-heading">Leveringsadresse</h3>
2902
4027
  <div class="form-row">
2903
4028
  <div class="field">
2904
- <label>Fornavn</label>
4029
+ <label>Fornavn *</label>
2905
4030
  <input
2906
4031
  type="text"
4032
+ required
2907
4033
  autocomplete="given-name"
2908
4034
  .value=${this.form.firstName}
2909
4035
  @input=${(e) => this.onFieldChange("firstName", e.target.value)}
2910
4036
  />
2911
4037
  </div>
2912
4038
  <div class="field">
2913
- <label>Etternavn</label>
4039
+ <label>Etternavn *</label>
2914
4040
  <input
2915
4041
  type="text"
4042
+ required
2916
4043
  autocomplete="family-name"
2917
4044
  .value=${this.form.lastName}
2918
4045
  @input=${(e) => this.onFieldChange("lastName", e.target.value)}
@@ -2920,18 +4047,20 @@ var VioCheckout = class extends LitElement {
2920
4047
  </div>
2921
4048
  </div>
2922
4049
  <div class="field">
2923
- <label>E-post</label>
4050
+ <label>E-post *</label>
2924
4051
  <input
2925
4052
  type="email"
4053
+ required
2926
4054
  autocomplete="email"
2927
4055
  .value=${this.form.email}
2928
4056
  @input=${(e) => this.onFieldChange("email", e.target.value)}
2929
4057
  />
2930
4058
  </div>
2931
4059
  <div class="field">
2932
- <label>Adresse</label>
4060
+ <label>Adresse *</label>
2933
4061
  <input
2934
4062
  type="text"
4063
+ required
2935
4064
  autocomplete="street-address"
2936
4065
  .value=${this.form.address}
2937
4066
  @input=${(e) => this.onFieldChange("address", e.target.value)}
@@ -2939,79 +4068,182 @@ var VioCheckout = class extends LitElement {
2939
4068
  </div>
2940
4069
  <div class="form-row">
2941
4070
  <div class="field">
2942
- <label>Postnummer</label>
4071
+ <label>Postnummer *</label>
2943
4072
  <input
2944
4073
  type="text"
4074
+ required
2945
4075
  autocomplete="postal-code"
2946
4076
  .value=${this.form.postalCode}
2947
4077
  @input=${(e) => this.onFieldChange("postalCode", e.target.value)}
2948
4078
  />
2949
4079
  </div>
2950
4080
  <div class="field">
2951
- <label>Sted</label>
4081
+ <label>Sted *</label>
2952
4082
  <input
2953
4083
  type="text"
4084
+ required
2954
4085
  autocomplete="address-level2"
2955
4086
  .value=${this.form.city}
2956
4087
  @input=${(e) => this.onFieldChange("city", e.target.value)}
2957
4088
  />
2958
4089
  </div>
2959
4090
  </div>
4091
+ ${this.availableShippingsList.length > 0 ? html`
4092
+ <div style="margin-top: 16px;">
4093
+ <label style="display: block; font-weight: 600; font-size: 13px; margin-bottom: 8px;">
4094
+ Fraktmetode *
4095
+ </label>
4096
+ <div class="ship-select">
4097
+ ${this.availableShippingsList.map(
4098
+ (o) => html`
4099
+ <button
4100
+ type="button"
4101
+ class="ship-opt ${this.selectedShipping === o.id ? "active" : ""}"
4102
+ @click=${() => void this.onSelectShipping(o.id)}
4103
+ >
4104
+ <span class="ship-meta">
4105
+ <b>${o.method || o.name}</b>
4106
+ ${o.description ? html`<span>${o.description}</span>` : ""}
4107
+ </span>
4108
+ <span class="ship-price">
4109
+ ${formatPrice(
4110
+ o.priceMajor ?? o.price / 100,
4111
+ o.currency || this.checkoutState?.currency || "NOK"
4112
+ )}
4113
+ </span>
4114
+ </button>
4115
+ `
4116
+ )}
4117
+ </div>
4118
+ </div>
4119
+ ` : ""}
2960
4120
  </section>
4121
+ ` : ""}
4122
+
4123
+ ${isVipps ? html`
4124
+ <section class="section">
4125
+ <div class="section-label">Kontakt</div>
4126
+ <div class="field">
4127
+ <label>E-post * <span style="font-weight:400;opacity:0.6">(for kvittering — adresse fylles ut i Vipps)</span></label>
4128
+ <input
4129
+ type="email"
4130
+ required
4131
+ autocomplete="email"
4132
+ .value=${this.form.email}
4133
+ @input=${(e) => this.onFieldChange("email", e.target.value)}
4134
+ />
4135
+ </div>
4136
+ </section>
4137
+ ` : ""}
2961
4138
 
2962
4139
  <section class="section">
2963
- <div class="section-label">Steg 2</div>
2964
- <h3 class="section-heading">Velg betalingsmåte</h3>
2965
- <div class="payment-grid">
2966
- ${this.applePayAvailable ? html`
2967
- <button
2968
- class="payment-btn primary"
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`
4140
+ <div class="section-label">${isVipps ? "Betaling" : "Steg 2"}</div>
4141
+ ${method ? html`
4142
+ <div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px;">
4143
+ <h3 class="section-heading" style="margin:0;">
4144
+ Betalingsmåte: <b>${this.methodLabel(method)}</b>
4145
+ </h3>
3005
4146
  <button
3006
- class="payment-btn primary complete-cta"
3007
- @click=${this.onCompleteOrder}
4147
+ type="button"
4148
+ style="background:none; border:none; color:var(--vio-color-accent, #c14a3b); text-decoration:underline; font-size:13px; cursor:pointer;"
4149
+ @click=${() => {
4150
+ this.unmountKlarna();
4151
+ Vio.checkout.selectPaymentMethod("");
4152
+ }}
3008
4153
  >
3009
- Betal ${this.orderTotal()} med ${this.methodLabel(method ?? null)}
4154
+ Endre
3010
4155
  </button>
3011
- ` : ""}
3012
- ${method === "klarna" ? this.renderKlarnaPanel() : ""}
3013
- ${this.paymentError ? html`<div class="payment-error">${this.paymentError}</div>` : ""}
3014
- </div>
4156
+ </div>
4157
+
4158
+ ${method === "klarna" ? this.renderKlarnaPanel() : ""}
4159
+ ${method === "stripe" ? html`
4160
+ <button
4161
+ class="payment-btn primary complete-cta"
4162
+ @click=${() => void this.onPay("stripe")}
4163
+ ?disabled=${this.stripeLoading || !this.isAddressValid || !this.hasSelectedShipping}
4164
+ title=${!this.isAddressValid ? "Vennligst fyll ut leveringsadresse" : !this.hasSelectedShipping ? "Vennligst velg en fraktmetode" : ""}
4165
+ >
4166
+ ${this.stripeLoading ? "G\xE5r til Stripe\u2026" : `Betal ${this.payTotalLabel()} med Stripe`}
4167
+ </button>
4168
+ ` : ""}
4169
+ ${method === "vipps" ? html`
4170
+ <button
4171
+ class="payment-btn primary complete-cta"
4172
+ @click=${() => void this.onPay("vipps")}
4173
+ ?disabled=${this.vippsLoading}
4174
+ >
4175
+ ${this.vippsLoading ? "G\xE5r til Vipps\u2026" : `Betal ${this.payTotalLabel()} med Vipps`}
4176
+ </button>
4177
+ ` : ""}
4178
+ ${method === "apple-pay" ? html`
4179
+ <button
4180
+ class="payment-btn primary complete-cta"
4181
+ @click=${() => void this.onPay("apple-pay")}
4182
+ ?disabled=${this.applePayInProgress || !this.isAddressValid || !this.hasSelectedShipping}
4183
+ title=${!this.isAddressValid ? "Vennligst fyll ut leveringsadresse" : !this.hasSelectedShipping ? "Vennligst velg en fraktmetode" : ""}
4184
+ >
4185
+ ${this.applePayInProgress ? "\xC5pner\u2026" : `Betal ${this.payTotalLabel()} med Apple Pay`}
4186
+ </button>
4187
+ ` : ""}
4188
+ ${showCompleteCta && method !== "stripe" && method !== "vipps" ? html`
4189
+ <button
4190
+ class="payment-btn primary complete-cta"
4191
+ @click=${this.onCompleteOrder}
4192
+ ?disabled=${!this.isAddressValid || !this.hasSelectedShipping}
4193
+ title=${!this.isAddressValid ? "Vennligst fyll ut leveringsadresse" : !this.hasSelectedShipping ? "Vennligst velg en fraktmetode" : ""}
4194
+ >
4195
+ Betal ${this.payTotalLabel()} med ${this.methodLabel(method ?? null)}
4196
+ </button>
4197
+ ` : ""}
4198
+ ${!isVipps && !this.isAddressValid ? html`
4199
+ <div style="font-size: 12px; color: var(--vio-color-accent, #c14a3b); text-align: center; margin-top: 8px;">
4200
+ Vennligst fyll ut leveringsadresse for å fullføre betalingen
4201
+ </div>
4202
+ ` : !isVipps && !this.hasSelectedShipping ? html`
4203
+ <div style="font-size: 12px; color: var(--vio-color-accent, #c14a3b); text-align: center; margin-top: 8px;">
4204
+ Vennligst velg en fraktmetode for å fullføre betalingen
4205
+ </div>
4206
+ ` : ""}
4207
+ ` : html`
4208
+ <h3 class="section-heading">Velg betalingsmåte</h3>
4209
+ <div class="payment-grid">
4210
+ ${this.methodEnabled("apple-pay") && this.applePayAvailable ? html`
4211
+ <button
4212
+ class="payment-btn primary"
4213
+ @click=${() => this.onPay("apple-pay")}
4214
+ ?disabled=${this.applePayInProgress}
4215
+ >
4216
+ ${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>`}
4217
+ </button>
4218
+ ` : ""}
4219
+ ${this.methodEnabled("stripe") ? html`
4220
+ <button
4221
+ class="payment-btn"
4222
+ @click=${() => this.onPay("stripe")}
4223
+ ?disabled=${this.stripeLoading}
4224
+ >
4225
+ <img src=${STRIPE_LOGO_SRC3} style="height: 20px; display: block;" alt="Stripe" />
4226
+ </button>
4227
+ ` : ""}
4228
+ ${this.methodEnabled("klarna") && this.klarnaAvailable ? html`
4229
+ <button class="payment-btn" @click=${() => this.onPay("klarna")}>
4230
+ Klarna
4231
+ </button>
4232
+ ` : ""}
4233
+ ${this.methodEnabled("vipps") ? html`
4234
+ <button class="payment-btn" @click=${() => this.onPay("vipps")}>
4235
+ <span style="color:#ff5b24;font-weight:800;font-size:17px;letter-spacing:-0.02em">vipps</span>
4236
+ </button>
4237
+ ` : ""}
4238
+ ${this.methodEnabled("apple-pay") && !this.applePayAvailable ? html`
4239
+ <div class="apple-pay-note">
4240
+ Pay krever Safari på iOS eller macOS med Apple Pay konfigurert.
4241
+ </div>
4242
+ ` : ""}
4243
+ </div>
4244
+ `}
4245
+ ${this.paymentNotice ? html`<div class="payment-notice">${this.paymentNotice}</div>` : ""}
4246
+ ${this.paymentError ? html`<div class="payment-error">${this.paymentError}</div>` : ""}
3015
4247
  </section>
3016
4248
 
3017
4249
  ${this.items.length > 0 ? html`
@@ -3037,10 +4269,11 @@ var VioCheckout = class extends LitElement {
3037
4269
  <span>Sum</span><span>${this.orderTotal()}</span>
3038
4270
  </div>
3039
4271
  <div class="order-row">
3040
- <span>Frakt</span><span>${this.shippingLabel}</span>
4272
+ <span>Frakt${this.shippingMajor() > 0 && this.shippingOption ? ` \u2013 ${this.shippingOption.method || this.shippingOption.name}` : ""}</span>
4273
+ <span>${this.shippingMajor() > 0 ? formatPrice(this.shippingMajor(), this.checkoutState?.currency) : this.shippingLabel}</span>
3041
4274
  </div>
3042
4275
  <div class="order-row total">
3043
- <span>Totalt</span><span>${this.orderTotal()}</span>
4276
+ <span>Totalt</span><span>${this.payTotalLabel()}</span>
3044
4277
  </div>
3045
4278
  </section>
3046
4279
  ` : ""}
@@ -3051,62 +4284,79 @@ var VioCheckout = class extends LitElement {
3051
4284
  };
3052
4285
  __decorateClass([
3053
4286
  property({ type: Boolean, reflect: true })
3054
- ], VioCheckout.prototype, "open", 2);
4287
+ ], _VioCheckout.prototype, "open", 2);
3055
4288
  __decorateClass([
3056
4289
  property({ type: String })
3057
- ], VioCheckout.prototype, "heading", 2);
4290
+ ], _VioCheckout.prototype, "heading", 2);
3058
4291
  __decorateClass([
3059
4292
  property({ type: String, attribute: "shipping-label" })
3060
- ], VioCheckout.prototype, "shippingLabel", 2);
4293
+ ], _VioCheckout.prototype, "shippingLabel", 2);
3061
4294
  __decorateClass([
3062
4295
  property({ type: Boolean, reflect: true })
3063
- ], VioCheckout.prototype, "express", 2);
4296
+ ], _VioCheckout.prototype, "express", 2);
4297
+ __decorateClass([
4298
+ state()
4299
+ ], _VioCheckout.prototype, "checkoutState", 2);
4300
+ __decorateClass([
4301
+ state()
4302
+ ], _VioCheckout.prototype, "items", 2);
4303
+ __decorateClass([
4304
+ state()
4305
+ ], _VioCheckout.prototype, "applePayAvailable", 2);
4306
+ __decorateClass([
4307
+ state()
4308
+ ], _VioCheckout.prototype, "applePayInProgress", 2);
4309
+ __decorateClass([
4310
+ state()
4311
+ ], _VioCheckout.prototype, "klarnaAvailable", 2);
3064
4312
  __decorateClass([
3065
4313
  state()
3066
- ], VioCheckout.prototype, "checkoutState", 2);
4314
+ ], _VioCheckout.prototype, "orderConfirmed", 2);
3067
4315
  __decorateClass([
3068
4316
  state()
3069
- ], VioCheckout.prototype, "items", 2);
4317
+ ], _VioCheckout.prototype, "confirmedMethod", 2);
3070
4318
  __decorateClass([
3071
4319
  state()
3072
- ], VioCheckout.prototype, "applePayAvailable", 2);
4320
+ ], _VioCheckout.prototype, "paymentError", 2);
3073
4321
  __decorateClass([
3074
4322
  state()
3075
- ], VioCheckout.prototype, "applePayInProgress", 2);
4323
+ ], _VioCheckout.prototype, "paymentNotice", 2);
3076
4324
  __decorateClass([
3077
4325
  state()
3078
- ], VioCheckout.prototype, "klarnaAvailable", 2);
4326
+ ], _VioCheckout.prototype, "confirmedOrder", 2);
3079
4327
  __decorateClass([
3080
4328
  state()
3081
- ], VioCheckout.prototype, "orderConfirmed", 2);
4329
+ ], _VioCheckout.prototype, "klarnaCategories", 2);
3082
4330
  __decorateClass([
3083
4331
  state()
3084
- ], VioCheckout.prototype, "confirmedMethod", 2);
4332
+ ], _VioCheckout.prototype, "klarnaSelectedCat", 2);
3085
4333
  __decorateClass([
3086
4334
  state()
3087
- ], VioCheckout.prototype, "paymentError", 2);
4335
+ ], _VioCheckout.prototype, "klarnaAuthorizing", 2);
3088
4336
  __decorateClass([
3089
4337
  state()
3090
- ], VioCheckout.prototype, "confirmedOrder", 2);
4338
+ ], _VioCheckout.prototype, "selectedShipping", 2);
3091
4339
  __decorateClass([
3092
4340
  state()
3093
- ], VioCheckout.prototype, "klarnaCategories", 2);
4341
+ ], _VioCheckout.prototype, "availableShippingsList", 2);
3094
4342
  __decorateClass([
3095
4343
  state()
3096
- ], VioCheckout.prototype, "klarnaSelectedCat", 2);
4344
+ ], _VioCheckout.prototype, "availableMethods", 2);
3097
4345
  __decorateClass([
3098
4346
  state()
3099
- ], VioCheckout.prototype, "klarnaAuthorizing", 2);
4347
+ ], _VioCheckout.prototype, "stripeLoading", 2);
3100
4348
  __decorateClass([
3101
4349
  state()
3102
- ], VioCheckout.prototype, "selectedShipping", 2);
4350
+ ], _VioCheckout.prototype, "vippsLoading", 2);
3103
4351
  __decorateClass([
3104
4352
  state()
3105
- ], VioCheckout.prototype, "form", 2);
4353
+ ], _VioCheckout.prototype, "form", 2);
4354
+ var VioCheckout = _VioCheckout;
3106
4355
 
3107
4356
  // src/ui/elements.ts
3108
4357
  function registerVioElements() {
3109
- if (typeof customElements === "undefined") return;
4358
+ if (typeof customElements === "undefined" || typeof customElements.get !== "function" || typeof customElements.define !== "function")
4359
+ return;
3110
4360
  const defs = [
3111
4361
  ["vio-product", VioProduct],
3112
4362
  ["vio-product-carousel", VioProductCarousel],
@@ -3119,6 +4369,6 @@ function registerVioElements() {
3119
4369
  }
3120
4370
  }
3121
4371
 
3122
- export { VioCart, VioCheckout, VioProduct, VioProductCarousel, VioProductDetail, registerVioElements };
3123
- //# sourceMappingURL=chunk-7JSSYPRN.js.map
3124
- //# sourceMappingURL=chunk-7JSSYPRN.js.map
4372
+ export { VioCart, VioCheckout, VioProduct, VioProductCarousel, VioProductDetail, VioTokens, applyVioTheme, registerVioElements };
4373
+ //# sourceMappingURL=chunk-V5ZQG2H5.js.map
4374
+ //# sourceMappingURL=chunk-V5ZQG2H5.js.map