@vio-live/web-sdk 0.2.0 → 0.3.0

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