@vio-live/web-sdk 0.1.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,133 +1,7 @@
1
- import { __decorateClass, Vio, primaryImageUrl, displayPrice, formatPrice, KLARNA_SHIPPING_OPTIONS } from './chunk-5SPY7TH6.js';
2
- import { css, LitElement, html } from 'lit';
3
- import { property, state, customElement, query } from 'lit/decorators.js';
1
+ import { Vio, __decorateClass, primaryImageUrl, displayPrice, getGlobalCurrency, formatPrice, KLARNA_SHIPPING_OPTIONS } from './chunk-NP2KCM7X.js';
2
+ import { LitElement, css, html } from 'lit';
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
5
  var VioProduct = class extends LitElement {
132
6
  constructor() {
133
7
  super(...arguments);
@@ -139,11 +13,124 @@ var VioProduct = class extends LitElement {
139
13
  this.productId = "";
140
14
  this.sponsorId = "";
141
15
  this.hasVariants = false;
16
+ this.variants = void 0;
17
+ this.options = void 0;
18
+ this.hideMeta = false;
142
19
  this.inCart = false;
143
20
  this.cartQty = 0;
144
21
  this.justAdded = false;
145
22
  this.boundCartChange = () => this.updateInCart();
146
23
  }
24
+ static {
25
+ this.styles = css`
26
+ :host {
27
+ display: block;
28
+ box-sizing: border-box;
29
+ width: 100%;
30
+ cursor: pointer;
31
+ font-family: var(--vio-font-sans, -apple-system, sans-serif);
32
+ color: var(--vio-color-text, #0a0a0a);
33
+ -webkit-tap-highlight-color: transparent;
34
+ }
35
+ .card {
36
+ display: flex;
37
+ flex-direction: column;
38
+ height: 100%;
39
+ }
40
+ .image {
41
+ position: relative;
42
+ aspect-ratio: 1 / 1;
43
+ width: 100%;
44
+ background-color: var(--vio-color-surface-muted, #f2f2f2);
45
+ display: flex;
46
+ align-items: center;
47
+ justify-content: center;
48
+ overflow: hidden;
49
+ transition: opacity 0.2s ease;
50
+ }
51
+ :host(:hover) .image img { opacity: 0.92; }
52
+
53
+ /* Quick add-to-cart button — overlaid bottom-right of the image, like the
54
+ reference card's wishlist heart but wired to add the product to the cart. */
55
+ .add-btn {
56
+ position: absolute;
57
+ right: 10px;
58
+ bottom: 10px;
59
+ width: 40px;
60
+ height: 40px;
61
+ border-radius: 50%;
62
+ background: rgba(255, 255, 255, 0.96);
63
+ border: none;
64
+ display: flex;
65
+ align-items: center;
66
+ justify-content: center;
67
+ cursor: pointer;
68
+ color: #0a0a0a;
69
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.16);
70
+ transition: transform 0.14s ease, background 0.14s ease, color 0.14s ease;
71
+ padding: 0;
72
+ z-index: 2;
73
+ }
74
+ .add-btn:hover { background: #fff; transform: scale(1.08); }
75
+ .add-btn:active { transform: scale(0.95); }
76
+ /* In cart → black circle with a white tick (matches the floating cart). */
77
+ .add-btn.in-cart { background: #0a0a0a; color: #fff; }
78
+ .add-btn.in-cart:hover { background: #222; }
79
+ .add-btn.pop { animation: vio-add-pop 0.42s cubic-bezier(0.34, 1.56, 0.64, 1); }
80
+ .add-btn svg { width: 18px; height: 18px; }
81
+ .add-btn .add-count { font-size: 15px; font-weight: 700; line-height: 1; }
82
+ @keyframes vio-add-pop {
83
+ 0% { transform: scale(1); }
84
+ 35% { transform: scale(1.32); }
85
+ 60% { transform: scale(0.9); }
86
+ 100% { transform: scale(1); }
87
+ }
88
+ .image img {
89
+ width: 100%;
90
+ height: 100%;
91
+ object-fit: cover;
92
+ display: block;
93
+ }
94
+ .meta {
95
+ padding-top: 14px;
96
+ display: flex;
97
+ flex-direction: column;
98
+ flex: 1;
99
+ }
100
+ .brand {
101
+ font-size: var(--vio-size-xs, 12px);
102
+ letter-spacing: var(--vio-tracking-label, 0.1em);
103
+ text-transform: uppercase;
104
+ color: var(--vio-color-text-secondary, #666);
105
+ font-weight: 500;
106
+ margin-bottom: 6px;
107
+ }
108
+ .name {
109
+ font-size: var(--vio-size-md, 16px);
110
+ font-weight: 500;
111
+ line-height: 1.3;
112
+ margin: 0 0 10px;
113
+ display: -webkit-box;
114
+ -webkit-line-clamp: 2;
115
+ line-clamp: 2;
116
+ -webkit-box-orient: vertical;
117
+ overflow: hidden;
118
+ }
119
+ .price {
120
+ font-size: var(--vio-size-md, 16px);
121
+ font-weight: 700;
122
+ margin-bottom: 16px;
123
+ }
124
+ .retailer {
125
+ font-size: var(--vio-size-xs, 12px);
126
+ letter-spacing: var(--vio-tracking-label, 0.1em);
127
+ text-transform: uppercase;
128
+ color: var(--vio-color-text-tertiary, #999);
129
+ font-weight: 500;
130
+ margin-top: auto;
131
+ }
132
+ `;
133
+ }
147
134
  connectedCallback() {
148
135
  super.connectedCallback();
149
136
  this.updateInCart();
@@ -201,7 +188,7 @@ var VioProduct = class extends LitElement {
201
188
  * `vio:product-add` so the host adds it (bumping quantity on repeat taps). */
202
189
  onAdd(e) {
203
190
  e.stopPropagation();
204
- if (this.hasVariants) {
191
+ if (this.hasVariants || Array.isArray(this.variants) && this.variants.length > 1 || Array.isArray(this.options) && this.options.length > 0) {
205
192
  this.onClick();
206
193
  return;
207
194
  }
@@ -252,124 +239,17 @@ var VioProduct = class extends LitElement {
252
239
  </svg>`}
253
240
  </button>
254
241
  </div>
242
+ ${this.hideMeta ? "" : html`
255
243
  <div class="meta">
256
244
  ${this.brand ? html`<div class="brand">${this.brand}</div>` : ""}
257
245
  ${this.name ? html`<div class="name">${this.name}</div>` : ""}
258
246
  ${this.price ? html`<div class="price">${this.price}</div>` : ""}
259
247
  ${this.retailer ? html`<div class="retailer">${this.retailer}</div>` : ""}
260
- </div>
248
+ </div>`}
261
249
  </div>
262
250
  `;
263
251
  }
264
252
  };
265
- VioProduct.styles = css`
266
- :host {
267
- display: block;
268
- box-sizing: border-box;
269
- width: 100%;
270
- cursor: pointer;
271
- font-family: var(--vio-font-sans, -apple-system, sans-serif);
272
- color: var(--vio-color-text, #0a0a0a);
273
- -webkit-tap-highlight-color: transparent;
274
- }
275
- .card {
276
- display: flex;
277
- flex-direction: column;
278
- height: 100%;
279
- }
280
- .image {
281
- position: relative;
282
- aspect-ratio: 1 / 1;
283
- width: 100%;
284
- background-color: var(--vio-color-surface-muted, #f2f2f2);
285
- display: flex;
286
- align-items: center;
287
- justify-content: center;
288
- overflow: hidden;
289
- transition: opacity 0.2s ease;
290
- }
291
- :host(:hover) .image img { opacity: 0.92; }
292
-
293
- /* Quick add-to-cart button — overlaid bottom-right of the image, like the
294
- reference card's wishlist heart but wired to add the product to the cart. */
295
- .add-btn {
296
- position: absolute;
297
- right: 10px;
298
- bottom: 10px;
299
- width: 40px;
300
- height: 40px;
301
- border-radius: 50%;
302
- background: rgba(255, 255, 255, 0.96);
303
- border: none;
304
- display: flex;
305
- align-items: center;
306
- justify-content: center;
307
- cursor: pointer;
308
- color: #0a0a0a;
309
- box-shadow: 0 2px 10px rgba(0, 0, 0, 0.16);
310
- transition: transform 0.14s ease, background 0.14s ease, color 0.14s ease;
311
- padding: 0;
312
- z-index: 2;
313
- }
314
- .add-btn:hover { background: #fff; transform: scale(1.08); }
315
- .add-btn:active { transform: scale(0.95); }
316
- /* In cart → black circle with a white tick (matches the floating cart). */
317
- .add-btn.in-cart { background: #0a0a0a; color: #fff; }
318
- .add-btn.in-cart:hover { background: #222; }
319
- .add-btn.pop { animation: vio-add-pop 0.42s cubic-bezier(0.34, 1.56, 0.64, 1); }
320
- .add-btn svg { width: 18px; height: 18px; }
321
- .add-btn .add-count { font-size: 15px; font-weight: 700; line-height: 1; }
322
- @keyframes vio-add-pop {
323
- 0% { transform: scale(1); }
324
- 35% { transform: scale(1.32); }
325
- 60% { transform: scale(0.9); }
326
- 100% { transform: scale(1); }
327
- }
328
- .image img {
329
- width: 100%;
330
- height: 100%;
331
- object-fit: cover;
332
- display: block;
333
- }
334
- .meta {
335
- padding-top: 14px;
336
- display: flex;
337
- flex-direction: column;
338
- flex: 1;
339
- }
340
- .brand {
341
- font-size: var(--vio-size-xs, 12px);
342
- letter-spacing: var(--vio-tracking-label, 0.1em);
343
- text-transform: uppercase;
344
- color: var(--vio-color-text-secondary, #666);
345
- font-weight: 500;
346
- margin-bottom: 6px;
347
- }
348
- .name {
349
- font-size: var(--vio-size-md, 16px);
350
- font-weight: 500;
351
- line-height: 1.3;
352
- margin: 0 0 10px;
353
- display: -webkit-box;
354
- -webkit-line-clamp: 2;
355
- line-clamp: 2;
356
- -webkit-box-orient: vertical;
357
- overflow: hidden;
358
- }
359
- .price {
360
- font-size: var(--vio-size-md, 16px);
361
- font-weight: 700;
362
- margin-bottom: 16px;
363
- }
364
- .retailer {
365
- font-size: var(--vio-size-xs, 12px);
366
- letter-spacing: var(--vio-tracking-label, 0.1em);
367
- text-transform: uppercase;
368
- color: var(--vio-color-text-tertiary, #999);
369
- font-weight: 500;
370
- margin-top: auto;
371
- }
372
- `;
373
253
  __decorateClass([
374
254
  property({ type: String })
375
255
  ], VioProduct.prototype, "brand", 2);
@@ -394,6 +274,15 @@ __decorateClass([
394
274
  __decorateClass([
395
275
  property({ type: Boolean, attribute: "has-variants" })
396
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);
397
286
  __decorateClass([
398
287
  state()
399
288
  ], VioProduct.prototype, "inCart", 2);
@@ -403,9 +292,6 @@ __decorateClass([
403
292
  __decorateClass([
404
293
  state()
405
294
  ], VioProduct.prototype, "justAdded", 2);
406
- VioProduct = __decorateClass([
407
- customElement("vio-product")
408
- ], VioProduct);
409
295
  var VioProductCarousel = class extends LitElement {
410
296
  constructor() {
411
297
  super(...arguments);
@@ -423,158 +309,23 @@ var VioProductCarousel = class extends LitElement {
423
309
  this.isLoading = false;
424
310
  this.fetchError = "";
425
311
  }
426
- connectedCallback() {
427
- super.connectedCallback();
428
- if (this.productRefs.trim() || this.productIds.trim()) {
429
- void this.autoFetch();
312
+ static {
313
+ this.styles = css`
314
+ :host {
315
+ display: block;
316
+ box-sizing: border-box;
317
+ width: 100%;
318
+ font-family: var(--vio-font-sans, -apple-system, sans-serif);
319
+ color: var(--vio-color-text, #0a0a0a);
320
+ padding: var(--vio-space-xl, 32px) 0;
430
321
  }
431
- }
432
- updated(changed) {
433
- const propChanged = changed.has("productRefs") || changed.has("productIds") || changed.has("commerceKey") || changed.has("sponsorId");
434
- if (propChanged && (this.productRefs.trim() || this.productIds.trim())) {
435
- void this.autoFetch();
436
- }
437
- }
438
- async autoFetch() {
439
- if (!Vio.isInitialized) {
440
- this.fetchError = "Vio not initialized \u2014 call Vio.init({ apiKey }) first";
441
- return;
442
- }
443
- this.isLoading = true;
444
- this.fetchError = "";
445
- try {
446
- const refs = this.productRefs.trim() ? this.parseRefs(this.productRefs) : this.productIds.split(",").map((s) => parseInt(s.trim(), 10)).filter((n) => !Number.isNaN(n)).map((id) => ({ sponsorId: this.sponsorId, productId: id }));
447
- if (refs.length === 0) {
448
- this.fetched = [];
449
- return;
450
- }
451
- const bySponsor = /* @__PURE__ */ new Map();
452
- for (const r of refs) {
453
- const arr = bySponsor.get(r.sponsorId) ?? [];
454
- arr.push(r.productId);
455
- bySponsor.set(r.sponsorId, arr);
456
- }
457
- const fetched = await Promise.all(
458
- Array.from(bySponsor.entries()).map(async ([sponsorId, ids]) => {
459
- const commerce = this.commerceKey && this.productRefs.trim() === "" ? Vio.commerceWithKey(this.commerceKey) : Vio.commerceFor(sponsorId);
460
- const products = await commerce.channel.product.getByIds({
461
- product_ids: ids,
462
- currency: this.currency,
463
- image_size: "large"
464
- });
465
- return { sponsorId, products };
466
- })
467
- );
468
- const lookup = /* @__PURE__ */ new Map();
469
- for (const { sponsorId, products } of fetched) {
470
- for (const p of products) lookup.set(`${sponsorId}:${p.id}`, p);
471
- }
472
- this.fetched = refs.map(({ sponsorId, productId }) => {
473
- const product = lookup.get(`${sponsorId}:${productId}`);
474
- return product ? { product, sponsorId } : null;
475
- }).filter((x) => x !== null);
476
- } catch (err) {
477
- this.fetchError = err instanceof Error ? err.message : String(err);
478
- } finally {
479
- this.isLoading = false;
480
- }
481
- }
482
- parseRefs(raw) {
483
- return raw.split(",").map((pair) => pair.trim()).filter((s) => s.length > 0).map((pair) => {
484
- const [sidStr, pidStr] = pair.split(":");
485
- const sid = parseInt((sidStr ?? "").trim(), 10);
486
- const pid = parseInt((pidStr ?? "").trim(), 10);
487
- if (Number.isNaN(sid) || Number.isNaN(pid)) return null;
488
- return { sponsorId: sid, productId: pid };
489
- }).filter((x) => x !== null);
490
- }
491
- scrollByDirection(direction) {
492
- if (!this.scroller) return;
493
- const item = this.scroller.querySelector(":scope > *");
494
- const step = (item?.offsetWidth ?? 240) + 16;
495
- this.scroller.scrollBy({ left: direction * step * 2, behavior: "smooth" });
496
- }
497
- renderScrollerContent() {
498
- if (this.isLoading && !this.fetched) {
499
- return html`${Array.from({ length: 5 }).map(
500
- () => html`
501
- <div class="skeleton">
502
- <div class="skeleton-image"></div>
503
- <div class="skeleton-text"></div>
504
- <div class="skeleton-text shorter"></div>
505
- </div>
506
- `
507
- )}`;
508
- }
509
- if (this.fetched && this.fetched.length > 0) {
510
- return html`${this.fetched.map(({ product: p, sponsorId }) => {
511
- const img = primaryImageUrl(p) ?? "";
512
- const retailer = sponsorNameFromBootstrap(sponsorId) ?? p.brand ?? "";
513
- return html`
514
- <vio-product
515
- product-id=${String(p.id)}
516
- sponsor-id=${String(sponsorId)}
517
- brand=${p.brand ?? ""}
518
- name=${p.title}
519
- price=${displayPrice(p)}
520
- retailer=${retailer}
521
- image-url=${img}
522
- ?has-variants=${(p.variants?.length ?? 0) > 1}
523
- ></vio-product>
524
- `;
525
- })}`;
526
- }
527
- return html`<slot></slot>`;
528
- }
529
- render() {
530
- return html`
531
- <div class="header">
532
- <div class="header-text">
533
- ${this.label ? html`<div class="label">${this.label}</div>` : ""}
534
- <div class="heading-row">
535
- ${this.heading ? html`<h2 class="heading">${this.heading}</h2>` : ""}
536
- ${this.disclaimer ? html`<span class="disclaimer">(${this.disclaimer})</span>` : ""}
537
- </div>
538
- </div>
539
- ${this.seeAllHref ? html`<a class="see-all" href=${this.seeAllHref}>${this.seeAllLabel}</a>` : ""}
540
- </div>
541
- <div class="viewport">
542
- <button
543
- class="nav-btn prev"
544
- @click=${() => this.scrollByDirection(-1)}
545
- aria-label="Forrige"
546
- >
547
- ‹
548
- </button>
549
- <div class="scroller">${this.renderScrollerContent()}</div>
550
- <button
551
- class="nav-btn next"
552
- @click=${() => this.scrollByDirection(1)}
553
- aria-label="Neste"
554
- >
555
- ›
556
- </button>
557
- </div>
558
- ${this.fetchError && !this.fetched ? html`<div class="state error">${this.fetchError}</div>` : ""}
559
- `;
560
- }
561
- };
562
- VioProductCarousel.styles = css`
563
- :host {
564
- display: block;
565
- box-sizing: border-box;
566
- width: 100%;
567
- font-family: var(--vio-font-sans, -apple-system, sans-serif);
568
- color: var(--vio-color-text, #0a0a0a);
569
- padding: var(--vio-space-xl, 32px) 0;
570
- }
571
- .header {
572
- display: flex;
573
- align-items: flex-end;
574
- justify-content: space-between;
575
- gap: var(--vio-space-md, 16px);
576
- margin-bottom: var(--vio-space-lg, 24px);
577
- padding: 0 var(--vio-space-lg, 24px);
322
+ .header {
323
+ display: flex;
324
+ align-items: flex-end;
325
+ justify-content: space-between;
326
+ gap: var(--vio-space-md, 16px);
327
+ margin-bottom: var(--vio-space-lg, 24px);
328
+ padding: 0 var(--vio-space-lg, 24px);
578
329
  }
579
330
  .header-text { flex: 1; min-width: 0; }
580
331
  .label {
@@ -702,445 +453,234 @@ VioProductCarousel.styles = css`
702
453
  50% { opacity: 0.5; }
703
454
  }
704
455
  `;
705
- __decorateClass([
706
- property({ type: String })
707
- ], VioProductCarousel.prototype, "label", 2);
708
- __decorateClass([
709
- property({ type: String })
710
- ], VioProductCarousel.prototype, "heading", 2);
711
- __decorateClass([
712
- property({ type: String })
713
- ], VioProductCarousel.prototype, "disclaimer", 2);
714
- __decorateClass([
715
- property({ type: String, attribute: "see-all-href" })
716
- ], VioProductCarousel.prototype, "seeAllHref", 2);
717
- __decorateClass([
718
- property({ type: String, attribute: "see-all-label" })
719
- ], VioProductCarousel.prototype, "seeAllLabel", 2);
720
- __decorateClass([
721
- property({ type: String, attribute: "product-ids" })
722
- ], VioProductCarousel.prototype, "productIds", 2);
723
- __decorateClass([
724
- property({ type: String, attribute: "commerce-key" })
725
- ], VioProductCarousel.prototype, "commerceKey", 2);
726
- __decorateClass([
727
- property({ type: Number, attribute: "sponsor-id" })
728
- ], VioProductCarousel.prototype, "sponsorId", 2);
729
- __decorateClass([
730
- property({ type: String, attribute: "product-refs" })
731
- ], VioProductCarousel.prototype, "productRefs", 2);
732
- __decorateClass([
733
- property({ type: String })
734
- ], VioProductCarousel.prototype, "currency", 2);
735
- __decorateClass([
736
- query(".scroller")
737
- ], VioProductCarousel.prototype, "scroller", 2);
738
- __decorateClass([
739
- state()
740
- ], VioProductCarousel.prototype, "fetched", 2);
741
- __decorateClass([
742
- state()
743
- ], VioProductCarousel.prototype, "isLoading", 2);
744
- __decorateClass([
745
- state()
746
- ], VioProductCarousel.prototype, "fetchError", 2);
747
- VioProductCarousel = __decorateClass([
748
- customElement("vio-product-carousel")
749
- ], VioProductCarousel);
750
- function sponsorNameFromBootstrap(sponsorId) {
751
- const boot = Vio.bootstrapCache;
752
- if (!boot) return void 0;
753
- if (boot.primarySponsor?.id === sponsorId) return boot.primarySponsor.name;
754
- return boot.secondarySponsors?.find((s) => s.id === sponsorId)?.name;
755
- }
756
- var VioProductDetail = class extends LitElement {
757
- constructor() {
758
- super(...arguments);
759
- this.open = false;
760
- this.productId = "";
761
- this.sponsorId = 0;
762
- this.autoOpen = true;
763
- this.currency = "NOK";
764
- this.product = null;
765
- this.isLoading = false;
766
- this.fetchError = "";
767
- this.selectedOptions = {};
768
- this.quantity = 1;
769
- this.activeImageIndex = 0;
770
- this.adding = false;
771
- this.applePayOk = false;
772
- /** Pre-prepared Apple Pay handle — show() fires synchronously on tap. */
773
- this.applePayHandle = null;
774
- this.boundProductClick = (e) => {
775
- if (!this.autoOpen) return;
776
- const ev = e;
777
- const productIdNum = parseInt(ev.detail.productId, 10);
778
- if (Number.isNaN(productIdNum)) return;
779
- const sponsorIdNum = parseInt(ev.detail.sponsorId, 10);
780
- if (Number.isNaN(sponsorIdNum) || sponsorIdNum <= 0) return;
781
- this.productId = ev.detail.productId;
782
- this.sponsorId = sponsorIdNum;
783
- this.show();
784
- void this.fetchProduct();
785
- };
786
456
  }
787
457
  connectedCallback() {
788
458
  super.connectedCallback();
789
- document.addEventListener("vio:product-click", this.boundProductClick);
790
- }
791
- disconnectedCallback() {
792
- document.removeEventListener("vio:product-click", this.boundProductClick);
793
- super.disconnectedCallback();
794
- }
795
- show() {
796
- this.open = true;
459
+ if (this.productRefs.trim() || this.productIds.trim()) {
460
+ void this.autoFetch();
461
+ }
797
462
  }
798
- close() {
799
- this.open = false;
800
- setTimeout(() => {
801
- if (!this.open) {
802
- this.product = null;
803
- this.selectedOptions = {};
804
- this.quantity = 1;
805
- this.activeImageIndex = 0;
806
- this.fetchError = "";
807
- }
808
- }, 400);
463
+ updated(changed) {
464
+ const propChanged = changed.has("productRefs") || changed.has("productIds") || changed.has("commerceKey") || changed.has("sponsorId");
465
+ if (propChanged && (this.productRefs.trim() || this.productIds.trim())) {
466
+ void this.autoFetch();
467
+ }
809
468
  }
810
- async fetchProduct() {
811
- const productIdNum = parseInt(this.productId, 10);
812
- if (Number.isNaN(productIdNum) || !this.sponsorId) {
813
- this.product = null;
469
+ async autoFetch() {
470
+ if (!Vio.isInitialized) {
471
+ this.fetchError = "Vio not initialized \u2014 call Vio.init({ apiKey }) first";
814
472
  return;
815
473
  }
816
474
  this.isLoading = true;
817
475
  this.fetchError = "";
818
476
  try {
819
- const commerce = Vio.commerceFor(this.sponsorId);
820
- const products = await commerce.channel.product.getByIds({
821
- product_ids: [productIdNum],
822
- currency: this.currency,
823
- image_size: "large"
824
- });
825
- this.product = products[0] ?? null;
826
- if (this.product) {
827
- const initial = {};
828
- for (const opt of this.product.options) {
829
- const values = normalizeValues(opt.values);
830
- if (values.length > 0) initial[opt.name] = values[0];
831
- }
832
- this.selectedOptions = initial;
833
- void Vio.checkout.prepareApplePayFor(this.unitPrice * this.quantity, this.currency).then((handle) => {
834
- this.applePayHandle = handle;
835
- this.applePayOk = handle.available;
836
- }).catch(() => {
837
- this.applePayHandle = null;
838
- this.applePayOk = false;
839
- });
840
- }
841
- } catch (err) {
842
- this.fetchError = err instanceof Error ? err.message : String(err);
477
+ const refs = this.productRefs.trim() ? this.parseRefs(this.productRefs) : this.productIds.split(",").map((s) => parseInt(s.trim(), 10)).filter((n) => !Number.isNaN(n)).map((id) => ({ sponsorId: this.sponsorId, productId: id }));
478
+ if (refs.length === 0) {
479
+ this.fetched = [];
480
+ return;
481
+ }
482
+ const bySponsor = /* @__PURE__ */ new Map();
483
+ for (const r of refs) {
484
+ const arr = bySponsor.get(r.sponsorId) ?? [];
485
+ arr.push(r.productId);
486
+ bySponsor.set(r.sponsorId, arr);
487
+ }
488
+ const fetched = await Promise.all(
489
+ Array.from(bySponsor.entries()).map(async ([sponsorId, ids]) => {
490
+ const commerce = this.commerceKey && this.productRefs.trim() === "" ? Vio.commerceWithKey(this.commerceKey) : Vio.commerceFor(sponsorId);
491
+ const products = await commerce.channel.product.getByIds({
492
+ product_ids: ids,
493
+ currency: this.currency,
494
+ image_size: "large"
495
+ });
496
+ return { sponsorId, products };
497
+ })
498
+ );
499
+ const lookup = /* @__PURE__ */ new Map();
500
+ for (const { sponsorId, products } of fetched) {
501
+ for (const p of products) lookup.set(`${sponsorId}:${p.id}`, p);
502
+ }
503
+ this.fetched = refs.map(({ sponsorId, productId }) => {
504
+ const product = lookup.get(`${sponsorId}:${productId}`);
505
+ return product ? { product, sponsorId } : null;
506
+ }).filter((x) => x !== null);
507
+ } catch (err) {
508
+ this.fetchError = err instanceof Error ? err.message : String(err);
843
509
  } finally {
844
510
  this.isLoading = false;
845
511
  }
846
512
  }
847
- /** Resolve the active variant from the selected option values. */
848
- selectedVariant() {
849
- if (!this.product || this.product.variants.length === 0) return null;
850
- const vals = Object.values(this.selectedOptions);
851
- if (vals.length === 0) return this.product.variants[0] ?? null;
852
- const found = this.product.variants.find((v) => {
853
- if (!v.title) return false;
854
- return vals.every(
855
- (val) => v.title.toLowerCase().includes(val.toLowerCase())
856
- );
857
- });
858
- return found ?? this.product.variants[0] ?? null;
859
- }
860
- get unitPrice() {
861
- const v = this.selectedVariant();
862
- const price = v?.price ?? this.product?.price;
863
- return price?.amount_incl_taxes ?? price?.amount ?? 0;
864
- }
865
- get compareAt() {
866
- const v = this.selectedVariant();
867
- const price = v?.price ?? this.product?.price;
868
- return price?.compare_at_incl_taxes ?? price?.compare_at ?? null;
869
- }
870
- get availableQuantity() {
871
- const v = this.selectedVariant();
872
- if (v) return v.quantity ?? 0;
873
- return this.product?.quantity ?? 0;
874
- }
875
- stockLabel() {
876
- const qty = this.availableQuantity;
877
- if (qty <= 0) return { text: "Utsolgt", cls: "out-of-stock" };
878
- if (qty <= 3) return { text: `F\xE5 igjen \u2014 ${qty} stk`, cls: "low-stock" };
879
- return { text: "P\xE5 lager", cls: "in-stock" };
880
- }
881
- selectOption(optionName, value) {
882
- this.selectedOptions = { ...this.selectedOptions, [optionName]: value };
883
- }
884
- allImages() {
885
- if (!this.product) return [];
886
- const variantImages = this.selectedVariant()?.images ?? [];
887
- if (variantImages.length > 0) return variantImages;
888
- return this.product.images;
889
- }
890
- get activeImage() {
891
- return this.allImages()[this.activeImageIndex];
892
- }
893
- /** Add the current product + selected variant + qty to the cart. Returns the variant id used. */
894
- addCurrentToCart() {
895
- if (!this.product) return void 0;
896
- const variant = this.selectedVariant();
897
- const sponsor = Vio.bootstrapCache?.primarySponsor;
898
- const secondaries = Vio.bootstrapCache?.secondarySponsors ?? [];
899
- const sponsorName = sponsor?.id === this.sponsorId ? sponsor.name : secondaries.find((s) => s.id === this.sponsorId)?.name;
900
- Vio.cart.addProduct({
901
- product: this.product,
902
- sponsorId: this.sponsorId,
903
- sponsorName,
904
- quantity: this.quantity,
905
- variantId: variant?.id
906
- });
907
- return variant?.id;
513
+ parseRefs(raw) {
514
+ return raw.split(",").map((pair) => pair.trim()).filter((s) => s.length > 0).map((pair) => {
515
+ const [sidStr, pidStr] = pair.split(":");
516
+ const sid = parseInt((sidStr ?? "").trim(), 10);
517
+ const pid = parseInt((pidStr ?? "").trim(), 10);
518
+ if (Number.isNaN(sid) || Number.isNaN(pid)) return null;
519
+ return { sponsorId: sid, productId: pid };
520
+ }).filter((x) => x !== null);
908
521
  }
909
- async addToCart() {
910
- if (!this.product || this.availableQuantity <= 0 || this.adding) return;
911
- this.adding = true;
912
- try {
913
- const variantId = this.addCurrentToCart();
914
- this.dispatchEvent(
915
- new CustomEvent("vio:added-to-cart", {
916
- bubbles: true,
917
- composed: true,
918
- detail: {
919
- productId: this.product.id,
920
- sponsorId: this.sponsorId,
921
- quantity: this.quantity,
922
- variantId
923
- }
924
- })
925
- );
926
- this.close();
927
- } finally {
928
- this.adding = false;
929
- }
522
+ scrollByDirection(direction) {
523
+ if (!this.scroller) return;
524
+ const item = this.scroller.querySelector(":scope > *");
525
+ const step = (item?.offsetWidth ?? 240) + 16;
526
+ this.scroller.scrollBy({ left: direction * step * 2, behavior: "smooth" });
930
527
  }
931
- /**
932
- * Express buy: add to cart, then open the checkout overlay with Klarna
933
- * preselected. The host app handles `vio:checkout-open` (same event the cart
934
- * uses) — the `paymentMethod` hint tells it to preselect Klarna so its widget
935
- * loads immediately. Reuses the working Klarna Payments flow (no express SDK).
936
- */
937
- async buyWithKlarna() {
938
- if (!this.product || this.availableQuantity <= 0 || this.adding) return;
939
- this.adding = true;
940
- try {
941
- this.addCurrentToCart();
942
- this.close();
943
- this.dispatchEvent(
944
- new CustomEvent("vio:checkout-open", {
945
- bubbles: true,
946
- composed: true,
947
- detail: { sponsorId: this.sponsorId, paymentMethod: "klarna", express: true }
948
- })
949
- );
950
- } finally {
951
- this.adding = false;
528
+ renderScrollerContent() {
529
+ if (this.isLoading && !this.fetched) {
530
+ return html`${Array.from({ length: 5 }).map(
531
+ () => html`
532
+ <div class="skeleton">
533
+ <div class="skeleton-image"></div>
534
+ <div class="skeleton-text"></div>
535
+ <div class="skeleton-text shorter"></div>
536
+ </div>
537
+ `
538
+ )}`;
952
539
  }
953
- }
954
- /**
955
- * Express buy with Apple Pay. Runs synchronously inside the click so the
956
- * Apple Pay sheet keeps the user gesture: add to cart → open checkout (sets
957
- * state) → launch the sheet. On authorize, `payment-complete` drives the
958
- * confirmation drawer (same path as the in-checkout Apple Pay button).
959
- */
960
- buyWithApplePay() {
961
- if (!this.product || this.availableQuantity <= 0 || !this.applePayHandle) return;
962
- this.addCurrentToCart();
963
- try {
964
- Vio.checkout.open(this.sponsorId);
965
- } catch (err) {
966
- if (typeof console !== "undefined") {
967
- console.warn("[VioProductDetail] checkout open failed:", err);
968
- }
969
- return;
540
+ if (this.fetched && this.fetched.length > 0) {
541
+ return html`${this.fetched.map(({ product: p, sponsorId }) => {
542
+ const img = primaryImageUrl(p) ?? "";
543
+ const retailer = sponsorNameFromBootstrap(sponsorId) ?? p.brand ?? "";
544
+ return html`
545
+ <vio-product
546
+ product-id=${String(p.id)}
547
+ sponsor-id=${String(sponsorId)}
548
+ brand=${p.brand ?? ""}
549
+ name=${p.title}
550
+ price=${displayPrice(p)}
551
+ retailer=${retailer}
552
+ image-url=${img}
553
+ ?has-variants=${(p.variants?.length ?? 0) > 1}
554
+ ></vio-product>
555
+ `;
556
+ })}`;
970
557
  }
971
- void this.applePayHandle.show();
972
- this.close();
558
+ return html`<slot></slot>`;
973
559
  }
974
560
  render() {
975
561
  return html`
976
- <div class="backdrop" @click=${this.close}></div>
977
- <div class="modal" role="dialog" aria-label="Produkt">
978
- <div class="handle" @click=${this.close} aria-hidden="true"></div>
979
- <div class="topbar">
980
- <span class="topbar-brand">${this.product?.brand ?? ""}</span>
981
- <button class="close" @click=${this.close} aria-label="Lukk">×</button>
562
+ <div class="header">
563
+ <div class="header-text">
564
+ ${this.label ? html`<div class="label">${this.label}</div>` : ""}
565
+ <div class="heading-row">
566
+ ${this.heading ? html`<h2 class="heading">${this.heading}</h2>` : ""}
567
+ ${this.disclaimer ? html`<span class="disclaimer">(${this.disclaimer})</span>` : ""}
568
+ </div>
982
569
  </div>
983
-
984
- ${this.renderBody()}
570
+ ${this.seeAllHref ? html`<a class="see-all" href=${this.seeAllHref}>${this.seeAllLabel}</a>` : ""}
571
+ </div>
572
+ <div class="viewport">
573
+ <button
574
+ class="nav-btn prev"
575
+ @click=${() => this.scrollByDirection(-1)}
576
+ aria-label="Forrige"
577
+ >
578
+ ‹
579
+ </button>
580
+ <div class="scroller">${this.renderScrollerContent()}</div>
581
+ <button
582
+ class="nav-btn next"
583
+ @click=${() => this.scrollByDirection(1)}
584
+ aria-label="Neste"
585
+ >
586
+ ›
587
+ </button>
985
588
  </div>
589
+ ${this.fetchError && !this.fetched ? html`<div class="state error">${this.fetchError}</div>` : ""}
986
590
  `;
987
591
  }
988
- renderBody() {
989
- if (this.isLoading && !this.product) {
990
- return html`
991
- <div class="skeleton" role="status" aria-busy="true" aria-label="Laster produkt">
992
- <div class="sk-image"></div>
993
- <div class="sk-line sk-brand"></div>
994
- <div class="sk-line sk-title"></div>
995
- <div class="sk-line sk-title-2"></div>
996
- <div class="sk-line sk-price"></div>
997
- <div class="sk-button"></div>
998
- </div>
999
- `;
1000
- }
1001
- if (this.fetchError) {
1002
- return html`<div class="error">Kunne ikke laste produkt: ${this.fetchError}</div>`;
1003
- }
1004
- if (!this.product) return html``;
1005
- const p = this.product;
1006
- const stock = this.stockLabel();
1007
- const compare = this.compareAt;
1008
- const showCompare = compare !== null && compare > this.unitPrice;
1009
- const images = this.allImages();
1010
- const activeImage = this.activeImage;
1011
- return html`
1012
- <div class="body">
1013
- <div class="gallery">
1014
- <div class="gallery-main">
1015
- ${activeImage?.url ? html`<img src=${activeImage.url} alt=${p.title} />` : ""}
1016
- </div>
1017
- ${images.length > 1 ? html`
1018
- <div class="gallery-thumbs">
1019
- ${images.map(
1020
- (img, idx) => html`
1021
- <button
1022
- class="gallery-thumb ${idx === this.activeImageIndex ? "active" : ""}"
1023
- @click=${() => this.activeImageIndex = idx}
1024
- aria-label=${`Bilde ${idx + 1}`}
1025
- >
1026
- <img src=${img.url} alt="" />
1027
- </button>
1028
- `
1029
- )}
1030
- </div>
1031
- ` : ""}
1032
- </div>
1033
-
1034
- <div class="info">
1035
- ${p.brand ? html`<div class="brand">${p.brand}</div>` : ""}
1036
- <h2 class="name">${p.title}</h2>
1037
-
1038
- <div class="price-row">
1039
- <span class="price">${formatPrice(this.unitPrice, p.price.currency_code)}</span>
1040
- ${showCompare ? html`<span class="compare-at">${formatPrice(
1041
- compare,
1042
- p.price.currency_code
1043
- )}</span>` : ""}
1044
- </div>
1045
-
1046
- <div class="stock ${stock.cls}">${stock.text}</div>
1047
-
1048
- ${this.renderOptionPickers(p.options)}
1049
-
1050
- <div class="qty-cta-row">
1051
- <div class="qty-stepper">
1052
- <button
1053
- class="qty-btn"
1054
- @click=${() => this.quantity = Math.max(1, this.quantity - 1)}
1055
- aria-label="Reduser antall"
1056
- >
1057
- −
1058
- </button>
1059
- <span class="qty-value">${this.quantity}</span>
1060
- <button
1061
- class="qty-btn"
1062
- @click=${() => this.quantity = Math.min(this.availableQuantity, this.quantity + 1)}
1063
- aria-label="Øk antall"
1064
- ?disabled=${this.quantity >= this.availableQuantity}
1065
- >
1066
- +
1067
- </button>
1068
- </div>
1069
- <button
1070
- class="add-to-cart"
1071
- @click=${this.addToCart}
1072
- ?disabled=${this.availableQuantity <= 0 || this.adding}
1073
- >
1074
- ${this.adding ? "Legger til\u2026" : this.availableQuantity <= 0 ? "Utsolgt" : "Legg i handlekurv"}
1075
- </button>
1076
- </div>
1077
-
1078
- ${this.availableQuantity > 0 && this.applePayOk ? html`
1079
- <button
1080
- class="buy-applepay"
1081
- @click=${this.buyWithApplePay}
1082
- ?disabled=${this.adding}
1083
- aria-label="Kjøp nå med Apple Pay"
1084
- >
1085
- Kjøp nå med Pay
1086
- </button>
1087
- ` : ""}
1088
- ${this.availableQuantity > 0 ? html`
1089
- <button
1090
- class="buy-klarna"
1091
- @click=${this.buyWithKlarna}
1092
- ?disabled=${this.adding}
1093
- >
1094
- <span>Kjøp nå med</span>
1095
- <img
1096
- class="klarna-badge"
1097
- src="https://x.klarnacdn.net/payment-method/assets/badges/generic/klarna.svg"
1098
- alt="Klarna"
1099
- />
1100
- </button>
1101
- ` : ""}
1102
-
1103
- ${p.description ? html`
1104
- <div>
1105
- <div class="description-label">Beskrivelse</div>
1106
- <div class="description">${this.stripHtml(p.description)}</div>
1107
- </div>
1108
- ` : ""}
1109
- </div>
1110
- </div>
1111
- `;
1112
- }
1113
- renderOptionPickers(options) {
1114
- if (options.length === 0) return "";
1115
- return html`${options.map((opt) => {
1116
- const values = normalizeValues(opt.values);
1117
- if (values.length === 0) return "";
1118
- return html`
1119
- <div class="option-group">
1120
- <div class="option-label">${opt.name}</div>
1121
- <div class="option-values">
1122
- ${values.map(
1123
- (val) => html`
1124
- <button
1125
- class="option-pill"
1126
- @click=${() => this.selectOption(opt.name, val)}
1127
- aria-pressed=${this.selectedOptions[opt.name] === val}
1128
- >
1129
- ${val}
1130
- </button>
1131
- `
1132
- )}
1133
- </div>
1134
- </div>
1135
- `;
1136
- })}`;
1137
- }
1138
- /** Best-effort HTML strip for description. */
1139
- stripHtml(html6) {
1140
- return html6.replace(/<br\s*\/?>/gi, "\n").replace(/<\/p>/gi, "\n\n").replace(/<[^>]+>/g, "").replace(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").trim();
1141
- }
1142
592
  };
1143
- VioProductDetail.styles = css`
593
+ __decorateClass([
594
+ property({ type: String })
595
+ ], VioProductCarousel.prototype, "label", 2);
596
+ __decorateClass([
597
+ property({ type: String })
598
+ ], VioProductCarousel.prototype, "heading", 2);
599
+ __decorateClass([
600
+ property({ type: String })
601
+ ], VioProductCarousel.prototype, "disclaimer", 2);
602
+ __decorateClass([
603
+ property({ type: String, attribute: "see-all-href" })
604
+ ], VioProductCarousel.prototype, "seeAllHref", 2);
605
+ __decorateClass([
606
+ property({ type: String, attribute: "see-all-label" })
607
+ ], VioProductCarousel.prototype, "seeAllLabel", 2);
608
+ __decorateClass([
609
+ property({ type: String, attribute: "product-ids" })
610
+ ], VioProductCarousel.prototype, "productIds", 2);
611
+ __decorateClass([
612
+ property({ type: String, attribute: "commerce-key" })
613
+ ], VioProductCarousel.prototype, "commerceKey", 2);
614
+ __decorateClass([
615
+ property({ type: Number, attribute: "sponsor-id" })
616
+ ], VioProductCarousel.prototype, "sponsorId", 2);
617
+ __decorateClass([
618
+ property({ type: String, attribute: "product-refs" })
619
+ ], VioProductCarousel.prototype, "productRefs", 2);
620
+ __decorateClass([
621
+ property({ type: String })
622
+ ], VioProductCarousel.prototype, "currency", 2);
623
+ __decorateClass([
624
+ query(".scroller")
625
+ ], VioProductCarousel.prototype, "scroller", 2);
626
+ __decorateClass([
627
+ state()
628
+ ], VioProductCarousel.prototype, "fetched", 2);
629
+ __decorateClass([
630
+ state()
631
+ ], VioProductCarousel.prototype, "isLoading", 2);
632
+ __decorateClass([
633
+ state()
634
+ ], VioProductCarousel.prototype, "fetchError", 2);
635
+ function sponsorNameFromBootstrap(sponsorId) {
636
+ const boot = Vio.bootstrapCache;
637
+ if (!boot) return void 0;
638
+ if (boot.primarySponsor?.id === sponsorId) return boot.primarySponsor.name;
639
+ return boot.secondarySponsors?.find((s) => s.id === sponsorId)?.name;
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
+ );
644
+ var VioProductDetail = class extends LitElement {
645
+ constructor() {
646
+ super(...arguments);
647
+ this.open = false;
648
+ this.productId = "";
649
+ this.sponsorId = 0;
650
+ this.autoOpen = true;
651
+ this.currency = "NOK";
652
+ this.product = null;
653
+ this.isLoading = false;
654
+ this.fetchError = "";
655
+ this.selectedOptions = {};
656
+ this.quantity = 1;
657
+ this.activeImageIndex = 0;
658
+ this.adding = false;
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();
666
+ /** Pre-prepared Apple Pay handle — show() fires synchronously on tap. */
667
+ this.applePayHandle = null;
668
+ this.boundProductClick = (e) => {
669
+ if (!this.autoOpen) return;
670
+ const ev = e;
671
+ const productIdNum = parseInt(ev.detail.productId, 10);
672
+ if (Number.isNaN(productIdNum)) return;
673
+ const sponsorIdNum = parseInt(ev.detail.sponsorId, 10);
674
+ if (Number.isNaN(sponsorIdNum) || sponsorIdNum <= 0) return;
675
+ this.productId = ev.detail.productId;
676
+ this.sponsorId = sponsorIdNum;
677
+ this.currency = ev.detail.currency || getGlobalCurrency();
678
+ this.show();
679
+ void this.fetchProduct();
680
+ };
681
+ }
682
+ static {
683
+ this.styles = css`
1144
684
  :host { display: contents; }
1145
685
 
1146
686
  .backdrop {
@@ -1373,6 +913,13 @@ VioProductDetail.styles = css`
1373
913
  background: var(--vio-color-text, #0a0a0a);
1374
914
  color: var(--vio-color-text-on-primary, #fff);
1375
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); }
1376
923
 
1377
924
  /* Qty + CTA row */
1378
925
  .qty-cta-row {
@@ -1443,6 +990,35 @@ VioProductDetail.styles = css`
1443
990
  .buy-klarna .klarna-badge { height: 22px; width: auto; display: block; }
1444
991
  .buy-klarna:hover:not(:disabled) { background: #ffa0b9; }
1445
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; }
1446
1022
  /* Apple Pay express button — black, per Apple's button guidelines. Only
1447
1023
  shown in Safari (canApplePay). The glyph is the Apple logo (Safari). */
1448
1024
  .buy-applepay {
@@ -1524,329 +1100,761 @@ VioProductDetail.styles = css`
1524
1100
  font-size: 14px;
1525
1101
  }
1526
1102
  `;
1527
- __decorateClass([
1528
- property({ type: Boolean, reflect: true })
1529
- ], VioProductDetail.prototype, "open", 2);
1530
- __decorateClass([
1531
- property({ type: String, attribute: "product-id" })
1532
- ], VioProductDetail.prototype, "productId", 2);
1533
- __decorateClass([
1534
- property({ type: Number, attribute: "sponsor-id" })
1535
- ], VioProductDetail.prototype, "sponsorId", 2);
1536
- __decorateClass([
1537
- property({ type: Boolean, attribute: "auto-open" })
1538
- ], VioProductDetail.prototype, "autoOpen", 2);
1539
- __decorateClass([
1540
- property({ type: String })
1541
- ], VioProductDetail.prototype, "currency", 2);
1542
- __decorateClass([
1543
- state()
1544
- ], VioProductDetail.prototype, "product", 2);
1545
- __decorateClass([
1546
- state()
1547
- ], VioProductDetail.prototype, "isLoading", 2);
1548
- __decorateClass([
1549
- state()
1550
- ], VioProductDetail.prototype, "fetchError", 2);
1551
- __decorateClass([
1552
- state()
1553
- ], VioProductDetail.prototype, "selectedOptions", 2);
1554
- __decorateClass([
1555
- state()
1556
- ], VioProductDetail.prototype, "quantity", 2);
1557
- __decorateClass([
1558
- state()
1559
- ], VioProductDetail.prototype, "activeImageIndex", 2);
1560
- __decorateClass([
1561
- state()
1562
- ], VioProductDetail.prototype, "adding", 2);
1563
- __decorateClass([
1564
- state()
1565
- ], VioProductDetail.prototype, "applePayOk", 2);
1566
- VioProductDetail = __decorateClass([
1567
- customElement("vio-product-detail")
1568
- ], VioProductDetail);
1569
- function normalizeValues(raw) {
1570
- if (Array.isArray(raw)) {
1571
- return raw.map((v) => String(v).trim()).filter((v) => v.length > 0);
1572
1103
  }
1573
- if (typeof raw === "string") {
1574
- return raw.split(",").map((v) => v.trim()).filter((v) => v.length > 0);
1575
- }
1576
- return [];
1577
- }
1578
- var VioCart = class extends LitElement {
1579
- constructor() {
1580
- super(...arguments);
1581
- this.open = false;
1582
- this.heading = "Handlekurv";
1583
- this.checkoutLabel = "Til kassen";
1584
- this.emptyLabel = "Handlekurven er tom";
1585
- this.subtotalLabel = "Sum";
1586
- this.carts = /* @__PURE__ */ new Map();
1587
- this.itemCount = 0;
1588
- this.applePayAvailable = false;
1589
- this.confirmedOrder = null;
1590
- /** Pre-resolved Apple Pay handle; `show()` fires the sheet synchronously. */
1591
- this.applePayHandle = null;
1592
- /** Cart total Apple Pay was last prepared for (avoids redundant prep). */
1593
- this.preparedTotal = -1;
1594
- this.boundOnCartChange = (e) => {
1595
- const detail = e.detail;
1596
- this.carts = detail.cartsBySponsor;
1597
- this.itemCount = detail.itemCount;
1598
- };
1599
- }
1600
- connectedCallback() {
1601
- super.connectedCallback();
1602
- this.carts = Vio.cart.getAllCarts();
1603
- this.itemCount = Vio.cart.itemCount;
1604
- Vio.cart.addEventListener("change", this.boundOnCartChange);
1104
+ connectedCallback() {
1105
+ super.connectedCallback();
1106
+ document.addEventListener("vio:product-click", this.boundProductClick);
1605
1107
  }
1606
1108
  disconnectedCallback() {
1607
- Vio.cart.removeEventListener("change", this.boundOnCartChange);
1109
+ document.removeEventListener("vio:product-click", this.boundProductClick);
1608
1110
  super.disconnectedCallback();
1609
1111
  }
1610
- close() {
1611
- this.open = false;
1612
- }
1613
1112
  show() {
1614
1113
  this.open = true;
1615
1114
  }
1616
- updated(changed) {
1617
- if (changed.has("open") && this.open) {
1618
- this.confirmedOrder = null;
1619
- this.preparedTotal = -1;
1115
+ close() {
1116
+ this.open = false;
1117
+ setTimeout(() => {
1118
+ if (!this.open) {
1119
+ this.product = null;
1120
+ this.selectedOptions = {};
1121
+ this.quantity = 1;
1122
+ this.activeImageIndex = 0;
1123
+ this.fetchError = "";
1124
+ }
1125
+ }, 400);
1126
+ }
1127
+ async fetchProduct() {
1128
+ const productIdNum = parseInt(this.productId, 10);
1129
+ if (Number.isNaN(productIdNum) || !this.sponsorId) {
1130
+ this.product = null;
1131
+ return;
1620
1132
  }
1621
- if (this.open && this.itemCount > 0) {
1622
- const total = this.totalAcrossSponsors;
1623
- if (total !== this.preparedTotal) {
1624
- this.preparedTotal = total;
1625
- void this.refreshApplePay(total);
1133
+ const seq = ++this.fetchSeq;
1134
+ this.isLoading = true;
1135
+ this.fetchError = "";
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"
1147
+ });
1148
+ lastError = null;
1149
+ break;
1150
+ } catch (err) {
1151
+ lastError = err;
1152
+ await new Promise((resolve) => setTimeout(resolve, 300));
1626
1153
  }
1627
1154
  }
1628
- }
1629
- /** Pre-resolve the Apple Pay sheet for the current cart total. */
1630
- async refreshApplePay(total) {
1631
- if (total <= 0) {
1632
- this.applePayHandle = null;
1633
- this.applePayAvailable = false;
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);
1159
+ this.isLoading = false;
1634
1160
  return;
1635
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;
1636
1189
  try {
1637
- const handle = await Vio.checkout.prepareApplePayExpress(total, this.primaryCurrency);
1638
- if (this.preparedTotal !== total) return;
1639
- this.applePayHandle = handle;
1640
- this.applePayAvailable = handle.available;
1641
- } catch {
1642
- this.applePayHandle = null;
1643
- this.applePayAvailable = false;
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;
1644
1201
  }
1645
1202
  }
1646
- /** Apple Pay express — pay the whole cart, then show the confirmation. */
1647
- onApplePay() {
1648
- const handle = this.applePayHandle;
1649
- if (!handle || !handle.available || this.itemCount === 0) return;
1650
- const snapshot = this.buildConfirmation();
1651
- void handle.show().then((result) => {
1652
- if (!result) return;
1653
- this.confirmedOrder = snapshot;
1654
- for (const sponsorId of [...this.carts.keys()]) {
1655
- Vio.cart.clearSponsorCart(sponsorId);
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). */
1340
+ selectedVariant() {
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));
1388
+ });
1389
+ }
1390
+ get unitPrice() {
1391
+ const v = this.selectedVariant();
1392
+ const price = v?.price ?? this.product?.price;
1393
+ return price?.amount_incl_taxes ?? price?.amount ?? 0;
1394
+ }
1395
+ get compareAt() {
1396
+ const v = this.selectedVariant();
1397
+ const price = v?.price ?? this.product?.price;
1398
+ return price?.compare_at_incl_taxes ?? price?.compare_at ?? null;
1399
+ }
1400
+ get availableQuantity() {
1401
+ const v = this.selectedVariant();
1402
+ if (v) return this.variantSellableQty(v);
1403
+ return this.product?.quantity ?? 0;
1404
+ }
1405
+ stockLabel() {
1406
+ const qty = this.availableQuantity;
1407
+ if (qty <= 0) return { text: "Utsolgt", cls: "out-of-stock" };
1408
+ if (qty <= 3) return { text: `F\xE5 igjen \u2014 ${qty} stk`, cls: "low-stock" };
1409
+ return { text: "P\xE5 lager", cls: "in-stock" };
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). */
1414
+ selectOption(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
+ }
1656
1448
  }
1449
+ this.selectedOptions = updated;
1450
+ } else {
1451
+ this.selectedOptions = newSelection;
1452
+ }
1453
+ }
1454
+ /* eslint-enable @typescript-eslint/no-explicit-any */
1455
+ allImages() {
1456
+ if (!this.product) return [];
1457
+ const variantImages = this.selectedVariant()?.images ?? [];
1458
+ if (variantImages.length > 0) return variantImages;
1459
+ return this.product.images;
1460
+ }
1461
+ get activeImage() {
1462
+ return this.allImages()[this.activeImageIndex];
1463
+ }
1464
+ /** Add the current product + selected variant + qty to the cart. Returns the variant id used. */
1465
+ addCurrentToCart() {
1466
+ if (!this.product) return void 0;
1467
+ const variant = this.selectedVariant();
1468
+ const sponsor = Vio.bootstrapCache?.primarySponsor;
1469
+ const secondaries = Vio.bootstrapCache?.secondarySponsors ?? [];
1470
+ const sponsorName = sponsor?.id === this.sponsorId ? sponsor.name : secondaries.find((s) => s.id === this.sponsorId)?.name;
1471
+ Vio.cart.addProduct({
1472
+ product: this.product,
1473
+ sponsorId: this.sponsorId,
1474
+ sponsorName,
1475
+ quantity: this.quantity,
1476
+ variantId: variant?.id
1477
+ });
1478
+ return variant?.id;
1479
+ }
1480
+ async addToCart() {
1481
+ if (!this.product || this.availableQuantity <= 0 || this.adding) return;
1482
+ this.adding = true;
1483
+ try {
1484
+ const variantId = this.addCurrentToCart();
1657
1485
  this.dispatchEvent(
1658
- new CustomEvent("vio:payment-success", {
1486
+ new CustomEvent("vio:added-to-cart", {
1659
1487
  bubbles: true,
1660
1488
  composed: true,
1661
- detail: { method: "apple-pay", result }
1489
+ detail: {
1490
+ productId: this.product.id,
1491
+ sponsorId: this.sponsorId,
1492
+ quantity: this.quantity,
1493
+ variantId
1494
+ }
1662
1495
  })
1663
1496
  );
1664
- });
1497
+ this.close();
1498
+ } finally {
1499
+ this.adding = false;
1500
+ }
1665
1501
  }
1666
- buildConfirmation() {
1667
- const items = [];
1668
- for (const cart of this.carts.values()) {
1669
- for (const it of cart.items) {
1670
- items.push({
1671
- brand: it.brand,
1672
- name: it.name,
1673
- quantity: it.quantity,
1674
- unitPrice: it.unitPrice,
1675
- currency: it.currency
1676
- });
1677
- }
1502
+ /**
1503
+ * Express buy: add to cart, then open the checkout overlay with Klarna
1504
+ * preselected. The host app handles `vio:checkout-open` (same event the cart
1505
+ * uses) — the `paymentMethod` hint tells it to preselect Klarna so its widget
1506
+ * loads immediately. Reuses the working Klarna Payments flow (no express SDK).
1507
+ */
1508
+ async buyWithKlarna() {
1509
+ if (!this.product || this.availableQuantity <= 0 || this.adding) return;
1510
+ this.adding = true;
1511
+ try {
1512
+ this.addCurrentToCart();
1513
+ this.close();
1514
+ this.dispatchEvent(
1515
+ new CustomEvent("vio:checkout-open", {
1516
+ bubbles: true,
1517
+ composed: true,
1518
+ detail: { sponsorId: this.sponsorId, paymentMethod: "klarna", express: true }
1519
+ })
1520
+ );
1521
+ } finally {
1522
+ this.adding = false;
1678
1523
  }
1679
- return { items, total: this.totalAcrossSponsors, currency: this.primaryCurrency };
1680
1524
  }
1681
- closeConfirmation() {
1682
- this.confirmedOrder = null;
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();
1683
1529
  this.close();
1684
- }
1685
- /** Klarna express — opens the side-drawer Klarna checkout for the whole cart,
1686
- * skipping the address form (leveringsadresse) and method picker
1687
- * (betalingsmetode). The host's checkout-open handler closes this cart and
1688
- * opens <vio-checkout> in express mode with Klarna preselected. */
1689
- onKlarnaExpress() {
1690
- if (this.itemCount === 0) return;
1691
- const firstSponsorId = [...this.carts.keys()][0];
1692
- if (firstSponsorId === void 0) return;
1693
1530
  this.dispatchEvent(
1694
1531
  new CustomEvent("vio:checkout-open", {
1695
1532
  bubbles: true,
1696
1533
  composed: true,
1697
- detail: { sponsorId: firstSponsorId, paymentMethod: "klarna", express: true }
1534
+ detail: { sponsorId: this.sponsorId, paymentMethod: "vipps", express: false }
1698
1535
  })
1699
1536
  );
1700
1537
  }
1701
- get totalAcrossSponsors() {
1702
- let sum = 0;
1703
- for (const cart of this.carts.values()) {
1704
- for (const item of cart.items) sum += item.unitPrice * item.quantity;
1705
- }
1706
- return sum;
1707
- }
1708
- get primaryCurrency() {
1709
- for (const cart of this.carts.values()) return cart.currency;
1710
- return "NOK";
1711
- }
1712
- onIncrement(item) {
1713
- Vio.cart.updateQuantity(item.id, item.sponsorId, item.quantity + 1);
1714
- }
1715
- onDecrement(item) {
1716
- Vio.cart.updateQuantity(item.id, item.sponsorId, item.quantity - 1);
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
+ );
1717
1550
  }
1718
- onRemove(item) {
1719
- Vio.cart.removeItem(item.id, item.sponsorId);
1551
+ /**
1552
+ * Express buy with Apple Pay. Runs synchronously inside the click so the
1553
+ * Apple Pay sheet keeps the user gesture: add to cart → open checkout (sets
1554
+ * state) → launch the sheet. On authorize, `payment-complete` drives the
1555
+ * confirmation drawer (same path as the in-checkout Apple Pay button).
1556
+ */
1557
+ buyWithApplePay() {
1558
+ if (!this.product || this.availableQuantity <= 0 || !this.applePayHandle) return;
1559
+ this.addCurrentToCart();
1560
+ try {
1561
+ Vio.checkout.open(this.sponsorId);
1562
+ } catch (err) {
1563
+ if (typeof console !== "undefined") {
1564
+ console.warn("[VioProductDetail] checkout open failed:", err);
1565
+ }
1566
+ return;
1567
+ }
1568
+ void this.applePayHandle.show();
1569
+ this.close();
1720
1570
  }
1721
1571
  render() {
1722
- const isEmpty = this.itemCount === 0;
1723
- const sponsorCarts = [...this.carts.values()].filter((c) => c.items.length > 0);
1724
1572
  return html`
1725
1573
  <div class="backdrop" @click=${this.close}></div>
1726
- <aside class="drawer" role="dialog" aria-label=${this.heading}>
1574
+ <div class="modal" role="dialog" aria-label="Produkt">
1727
1575
  <div class="handle" @click=${this.close} aria-hidden="true"></div>
1728
- <div class="header">
1729
- <h2 class="heading">${this.confirmedOrder ? "Takk!" : this.heading}</h2>
1576
+ <div class="topbar">
1577
+ <span class="topbar-brand">${this.product?.brand ?? ""}</span>
1730
1578
  <button class="close" @click=${this.close} aria-label="Lukk">×</button>
1731
1579
  </div>
1732
1580
 
1733
- ${this.confirmedOrder ? this.renderConfirmation() : isEmpty ? html`<div class="empty">${this.emptyLabel}</div>` : html`
1734
- <div class="body">
1735
- ${sponsorCarts.map(
1736
- (cart) => html`
1737
- <div class="sponsor-group">
1738
- ${cart.sponsorName ? html`<div class="sponsor-label">${cart.sponsorName}</div>` : ""}
1739
- ${cart.items.map(
1740
- (item) => html`
1741
- <div class="item">
1742
- <div class="item-image">
1743
- ${item.imageUrl ? html`<img src=${item.imageUrl} alt=${item.name} />` : ""}
1744
- </div>
1745
- <div>
1746
- <div class="item-brand">${item.brand}</div>
1747
- <div class="item-name">${item.name}</div>
1748
- <div class="item-row">
1749
- <span class="item-qty">
1750
- <button
1751
- class="qty-btn"
1752
- @click=${() => this.onDecrement(item)}
1753
- aria-label="Reduser"
1754
- >
1755
- −
1756
- </button>
1757
- ${item.quantity}
1758
- <button
1759
- class="qty-btn"
1760
- @click=${() => this.onIncrement(item)}
1761
- aria-label="Øk"
1762
- >
1763
- +
1764
- </button>
1765
- <button
1766
- class="qty-btn"
1767
- @click=${() => this.onRemove(item)}
1768
- aria-label="Fjern"
1769
- style="margin-left: 8px;"
1770
- >
1771
- ×
1772
- </button>
1773
- </span>
1774
- <span class="item-price">
1775
- ${formatPrice(
1776
- item.unitPrice * item.quantity,
1777
- item.currency
1778
- )}
1779
- </span>
1780
- </div>
1781
- </div>
1782
- </div>
1783
- `
1784
- )}
1785
- </div>
1786
- `
1787
- )}
1788
- </div>
1789
- <div class="footer">
1790
- <div class="subtotal-row">
1791
- <span class="subtotal-label">${this.subtotalLabel}</span>
1792
- <span class="subtotal-value">
1793
- ${formatPrice(this.totalAcrossSponsors, this.primaryCurrency)}
1794
- </span>
1795
- </div>
1796
- ${this.applePayAvailable ? html`
1581
+ ${this.renderBody()}
1582
+ </div>
1583
+ `;
1584
+ }
1585
+ renderBody() {
1586
+ if (this.isLoading && !this.product) {
1587
+ return html`
1588
+ <div class="skeleton" role="status" aria-busy="true" aria-label="Laster produkt">
1589
+ <div class="sk-image"></div>
1590
+ <div class="sk-line sk-brand"></div>
1591
+ <div class="sk-line sk-title"></div>
1592
+ <div class="sk-line sk-title-2"></div>
1593
+ <div class="sk-line sk-price"></div>
1594
+ <div class="sk-button"></div>
1595
+ </div>
1596
+ `;
1597
+ }
1598
+ if (this.fetchError) {
1599
+ return html`<div class="error">Kunne ikke laste produkt: ${this.fetchError}</div>`;
1600
+ }
1601
+ if (!this.product) return html``;
1602
+ const p = this.product;
1603
+ const stock = this.stockLabel();
1604
+ const compare = this.compareAt;
1605
+ const showCompare = compare !== null && compare > this.unitPrice;
1606
+ const images = this.allImages();
1607
+ const activeImage = this.activeImage;
1608
+ return html`
1609
+ <div class="body">
1610
+ <div class="gallery">
1611
+ <div class="gallery-main">
1612
+ ${activeImage?.url ? html`<img src=${activeImage.url} alt=${p.title} />` : ""}
1613
+ </div>
1614
+ ${images.length > 1 ? html`
1615
+ <div class="gallery-thumbs">
1616
+ ${images.map(
1617
+ (img, idx) => html`
1797
1618
  <button
1798
- class="applepay-btn"
1799
- @click=${this.onApplePay}
1800
- aria-label="Betal med Apple Pay"
1619
+ class="gallery-thumb ${idx === this.activeImageIndex ? "active" : ""}"
1620
+ @click=${() => this.activeImageIndex = idx}
1621
+ aria-label=${`Bilde ${idx + 1}`}
1801
1622
  >
1802
- <span class="ap-logo">${"\uF8FF"}</span> Pay
1623
+ <img src=${img.url} alt="" />
1803
1624
  </button>
1804
- ` : ""}
1625
+ `
1626
+ )}
1627
+ </div>
1628
+ ` : ""}
1629
+ </div>
1630
+
1631
+ <div class="info">
1632
+ ${p.brand ? html`<div class="brand">${p.brand}</div>` : ""}
1633
+ <h2 class="name">${p.title}</h2>
1634
+
1635
+ <div class="price-row">
1636
+ <span class="price">${formatPrice(this.unitPrice, p.price.currency_code)}</span>
1637
+ ${showCompare ? html`<span class="compare-at">${formatPrice(
1638
+ compare,
1639
+ p.price.currency_code
1640
+ )}</span>` : ""}
1641
+ </div>
1642
+
1643
+ <div class="stock ${stock.cls}">${stock.text}</div>
1644
+
1645
+ ${this.renderOptionPickers(p.options)}
1646
+
1647
+ <div class="qty-cta-row">
1648
+ <div class="qty-stepper">
1649
+ <button
1650
+ class="qty-btn"
1651
+ @click=${() => this.quantity = Math.max(1, this.quantity - 1)}
1652
+ aria-label="Reduser antall"
1653
+ >
1654
+ −
1655
+ </button>
1656
+ <span class="qty-value">${this.quantity}</span>
1657
+ <button
1658
+ class="qty-btn"
1659
+ @click=${() => this.quantity = Math.min(this.availableQuantity, this.quantity + 1)}
1660
+ aria-label="Øk antall"
1661
+ ?disabled=${this.quantity >= this.availableQuantity}
1662
+ >
1663
+ +
1664
+ </button>
1665
+ </div>
1666
+ <button
1667
+ class="add-to-cart"
1668
+ @click=${this.addToCart}
1669
+ ?disabled=${this.availableQuantity <= 0 || this.adding}
1670
+ >
1671
+ ${this.adding ? "Legger til\u2026" : this.availableQuantity <= 0 ? "Utsolgt" : "Legg i handlekurv"}
1672
+ </button>
1673
+ </div>
1674
+
1675
+ ${this.availableQuantity > 0 && this.applePayOk && this.methodEnabled("apple-pay", "applepay") ? html`
1676
+ <button
1677
+ class="buy-applepay"
1678
+ @click=${this.buyWithApplePay}
1679
+ ?disabled=${this.adding}
1680
+ aria-label="Kjøp nå med Apple Pay"
1681
+ >
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
+ />
1701
+ </button>
1702
+ ` : ""}
1703
+ ${this.availableQuantity > 0 && this.methodEnabled("klarna") ? html`
1805
1704
  <button
1806
- class="klarna-btn"
1807
- @click=${this.onKlarnaExpress}
1808
- aria-label="Betal med Klarna"
1705
+ class="buy-klarna"
1706
+ @click=${this.buyWithKlarna}
1707
+ ?disabled=${this.adding}
1809
1708
  >
1810
- Kjøp med
1709
+ <span>Kjøp nå med</span>
1811
1710
  <img
1812
- class="klarna-logo"
1711
+ class="klarna-badge"
1813
1712
  src="https://x.klarnacdn.net/payment-method/assets/badges/generic/klarna.svg"
1814
1713
  alt="Klarna"
1815
1714
  />
1816
1715
  </button>
1817
- </div>
1818
- `}
1819
- </aside>
1716
+ ` : ""}
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
+
1730
+ ${p.description ? html`
1731
+ <div>
1732
+ <div class="description-label">Beskrivelse</div>
1733
+ <div class="description">${this.stripHtml(p.description)}</div>
1734
+ </div>
1735
+ ` : ""}
1736
+ </div>
1737
+ </div>
1820
1738
  `;
1821
1739
  }
1822
- renderConfirmation() {
1823
- const o = this.confirmedOrder;
1824
- if (!o) return html``;
1825
- return html`
1826
- <div class="confirm">
1827
- <div class="confirm-check" aria-hidden="true">✓</div>
1828
- <h3>Takk for handelen!</h3>
1829
- <p>Betaling med Apple Pay er bekreftet.</p>
1830
- <div class="confirm-summary">
1831
- ${o.items.map(
1832
- (it) => html`
1833
- <div class="order-row">
1834
- <span>${it.brand ? `${it.brand} ` : ""}${it.name} ×${it.quantity}</span>
1835
- <span>${formatPrice(it.unitPrice * it.quantity, it.currency)}</span>
1836
- </div>
1837
- `
1838
- )}
1839
- <div class="order-row total">
1840
- <span>Totalt</span>
1841
- <span>${formatPrice(o.total, o.currency)}</span>
1740
+ renderOptionPickers(options) {
1741
+ if (options.length === 0) return "";
1742
+ return html`${options.map((opt) => {
1743
+ const values = normalizeValues(opt.values);
1744
+ if (values.length === 0) return "";
1745
+ return html`
1746
+ <div class="option-group">
1747
+ <div class="option-label">${opt.name}</div>
1748
+ <div class="option-values">
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`
1754
+ <button
1755
+ class="option-pill ${available ? "" : "unavailable"}"
1756
+ @click=${() => this.selectOption(opt.name, val)}
1757
+ aria-pressed=${isSelected}
1758
+ ?disabled=${!available}
1759
+ title=${available ? "" : "Utsolgt"}
1760
+ >
1761
+ ${val}
1762
+ </button>
1763
+ `;
1764
+ })}
1842
1765
  </div>
1843
1766
  </div>
1844
- <button class="confirm-close" @click=${this.closeConfirmation}>Lukk</button>
1845
- </div>
1846
- `;
1767
+ `;
1768
+ })}`;
1769
+ }
1770
+ /** Best-effort HTML strip for description. */
1771
+ stripHtml(html6) {
1772
+ return html6.replace(/<br\s*\/?>/gi, "\n").replace(/<\/p>/gi, "\n\n").replace(/<[^>]+>/g, "").replace(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").trim();
1847
1773
  }
1848
1774
  };
1849
- VioCart.styles = css`
1775
+ __decorateClass([
1776
+ property({ type: Boolean, reflect: true })
1777
+ ], VioProductDetail.prototype, "open", 2);
1778
+ __decorateClass([
1779
+ property({ type: String, attribute: "product-id" })
1780
+ ], VioProductDetail.prototype, "productId", 2);
1781
+ __decorateClass([
1782
+ property({ type: Number, attribute: "sponsor-id" })
1783
+ ], VioProductDetail.prototype, "sponsorId", 2);
1784
+ __decorateClass([
1785
+ property({ type: Boolean, attribute: "auto-open" })
1786
+ ], VioProductDetail.prototype, "autoOpen", 2);
1787
+ __decorateClass([
1788
+ property({ type: String })
1789
+ ], VioProductDetail.prototype, "currency", 2);
1790
+ __decorateClass([
1791
+ state()
1792
+ ], VioProductDetail.prototype, "product", 2);
1793
+ __decorateClass([
1794
+ state()
1795
+ ], VioProductDetail.prototype, "isLoading", 2);
1796
+ __decorateClass([
1797
+ state()
1798
+ ], VioProductDetail.prototype, "fetchError", 2);
1799
+ __decorateClass([
1800
+ state()
1801
+ ], VioProductDetail.prototype, "selectedOptions", 2);
1802
+ __decorateClass([
1803
+ state()
1804
+ ], VioProductDetail.prototype, "quantity", 2);
1805
+ __decorateClass([
1806
+ state()
1807
+ ], VioProductDetail.prototype, "activeImageIndex", 2);
1808
+ __decorateClass([
1809
+ state()
1810
+ ], VioProductDetail.prototype, "adding", 2);
1811
+ __decorateClass([
1812
+ state()
1813
+ ], VioProductDetail.prototype, "applePayOk", 2);
1814
+ __decorateClass([
1815
+ state()
1816
+ ], VioProductDetail.prototype, "availableMethods", 2);
1817
+ function normalizeValues(raw) {
1818
+ if (Array.isArray(raw)) {
1819
+ return raw.map((v) => String(v).trim()).filter((v) => v.length > 0);
1820
+ }
1821
+ if (typeof raw === "string") {
1822
+ return raw.split(",").map((v) => v.trim()).filter((v) => v.length > 0);
1823
+ }
1824
+ return [];
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
+ );
1829
+ var VioCart = class extends LitElement {
1830
+ constructor() {
1831
+ super(...arguments);
1832
+ this.open = false;
1833
+ this.heading = "Handlekurv";
1834
+ this.emptyLabel = "Handlekurven er tom";
1835
+ this.subtotalLabel = "Sum";
1836
+ this.carts = /* @__PURE__ */ new Map();
1837
+ this.itemCount = 0;
1838
+ this.applePayAvailable = false;
1839
+ this.availableMethods = null;
1840
+ this.confirmedOrder = null;
1841
+ /** Pre-resolved Apple Pay handle; `show()` fires the sheet synchronously. */
1842
+ this.applePayHandle = null;
1843
+ /** Cart total Apple Pay was last prepared for (avoids redundant prep). */
1844
+ this.preparedTotal = -1;
1845
+ /** In-flight guard for loadAvailablePaymentMethods. */
1846
+ this.loadingPaymentMethods = false;
1847
+ this.boundOnCartChange = (e) => {
1848
+ const detail = e.detail;
1849
+ this.carts = detail.cartsBySponsor;
1850
+ this.itemCount = detail.itemCount;
1851
+ if (this.availableMethods === null) {
1852
+ void this.loadAvailablePaymentMethods();
1853
+ }
1854
+ };
1855
+ }
1856
+ static {
1857
+ this.styles = css`
1850
1858
  :host { display: contents; }
1851
1859
 
1852
1860
  .backdrop {
@@ -2029,27 +2037,7 @@ VioCart.styles = css`
2029
2037
  .subtotal-label { color: var(--vio-color-text-secondary, #666); }
2030
2038
  .subtotal-value { font-weight: 700; font-size: 16px; }
2031
2039
 
2032
- .checkout-btn {
2033
- width: 100%;
2034
- padding: 18px;
2035
- background: var(--vio-color-text, #0a0a0a);
2036
- color: var(--vio-color-text-on-primary, #fff);
2037
- border: none;
2038
- font-size: 12px;
2039
- text-transform: uppercase;
2040
- letter-spacing: var(--vio-tracking-label, 0.1em);
2041
- cursor: pointer;
2042
- font-weight: 600;
2043
- font-family: var(--vio-font-sans, -apple-system, sans-serif);
2044
- transition: background 0.15s;
2045
- }
2046
- .checkout-btn:hover:not(:disabled) { background: #222; }
2047
- .checkout-btn:disabled {
2048
- background: var(--vio-color-text-tertiary, #999);
2049
- cursor: not-allowed;
2050
- }
2051
-
2052
- /* Apple Pay express — pay the whole cart directly, above "Til kassen". */
2040
+ /* Apple Pay express — pay the whole cart directly. */
2053
2041
  .applepay-btn {
2054
2042
  width: 100%;
2055
2043
  padding: 15px;
@@ -2070,7 +2058,7 @@ VioCart.styles = css`
2070
2058
  transition: opacity 0.15s;
2071
2059
  }
2072
2060
  .applepay-btn:hover { opacity: 0.86; }
2073
- .applepay-btn .ap-logo { font-size: 19px; line-height: 1; }
2061
+ .applepay-btn .ap-logo { width: 20px; height: 20px; }
2074
2062
  /* Klarna express — pink brand button, same express row as Apple Pay. */
2075
2063
  .klarna-btn {
2076
2064
  width: 100%;
@@ -2092,23 +2080,50 @@ VioCart.styles = css`
2092
2080
  }
2093
2081
  .klarna-btn:hover { background: #ffa1ba; }
2094
2082
  .klarna-btn .klarna-logo { height: 15px; width: auto; display: inline-block; }
2095
- .express-divider {
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;
2096
2093
  display: flex;
2097
2094
  align-items: center;
2098
- gap: 12px;
2099
- margin: 4px 0 12px;
2100
- color: var(--vio-color-text-tertiary, #999);
2101
- font-size: 11px;
2102
- text-transform: uppercase;
2103
- letter-spacing: 0.1em;
2095
+ justify-content: center;
2096
+ font-family: var(--vio-font-sans, -apple-system, sans-serif);
2097
+ transition: background 0.15s;
2104
2098
  }
2105
- .express-divider::before,
2106
- .express-divider::after {
2107
- content: '';
2108
- flex: 1;
2109
- height: 1px;
2110
- background: var(--vio-color-border, #e5e5e5);
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;
2111
2124
  }
2125
+ .stripe-btn:hover { background: #5248ff; }
2126
+ .stripe-btn .stripe-logo { height: 20px; width: auto; display: inline-block; filter: brightness(0) invert(1); }
2112
2127
 
2113
2128
  /* In-drawer confirmation after Apple Pay (nothing else is shown). */
2114
2129
  .confirm {
@@ -2177,601 +2192,454 @@ VioCart.styles = css`
2177
2192
  }
2178
2193
  .confirm-close:hover { background: #222; }
2179
2194
  `;
2180
- __decorateClass([
2181
- property({ type: Boolean, reflect: true })
2182
- ], VioCart.prototype, "open", 2);
2183
- __decorateClass([
2184
- property({ type: String })
2185
- ], VioCart.prototype, "heading", 2);
2186
- __decorateClass([
2187
- property({ type: String, attribute: "checkout-label" })
2188
- ], VioCart.prototype, "checkoutLabel", 2);
2189
- __decorateClass([
2190
- property({ type: String, attribute: "empty-label" })
2191
- ], VioCart.prototype, "emptyLabel", 2);
2192
- __decorateClass([
2193
- property({ type: String, attribute: "subtotal-label" })
2194
- ], VioCart.prototype, "subtotalLabel", 2);
2195
- __decorateClass([
2196
- state()
2197
- ], VioCart.prototype, "carts", 2);
2198
- __decorateClass([
2199
- state()
2200
- ], VioCart.prototype, "itemCount", 2);
2201
- __decorateClass([
2202
- state()
2203
- ], VioCart.prototype, "applePayAvailable", 2);
2204
- __decorateClass([
2205
- state()
2206
- ], VioCart.prototype, "confirmedOrder", 2);
2207
- VioCart = __decorateClass([
2208
- customElement("vio-cart")
2209
- ], VioCart);
2210
- var VioCheckout = class extends LitElement {
2211
- constructor() {
2212
- super(...arguments);
2213
- this.open = false;
2214
- this.heading = "Kasse";
2215
- this.shippingLabel = "Beregnes ved kasse";
2216
- this.express = false;
2217
- this.checkoutState = null;
2218
- this.items = [];
2219
- this.applePayAvailable = false;
2220
- this.applePayInProgress = false;
2221
- this.klarnaAvailable = false;
2222
- this.orderConfirmed = false;
2223
- this.confirmedMethod = null;
2224
- this.paymentError = null;
2225
- this.confirmedOrder = null;
2226
- /** Active Klarna Payments widget handle + the subtotal it was mounted for. */
2227
- this.klarnaHandle = null;
2228
- this.klarnaMountedAmount = null;
2229
- this.klarnaMountedShipping = null;
2230
- this.klarnaMounting = false;
2231
- this.klarnaCategories = [];
2232
- this.klarnaSelectedCat = "";
2233
- this.klarnaAuthorizing = false;
2234
- this.selectedShipping = KLARNA_SHIPPING_OPTIONS.find((o) => o.preselected)?.id ?? KLARNA_SHIPPING_OPTIONS[0]?.id ?? "";
2235
- this.form = {
2236
- firstName: "",
2237
- lastName: "",
2238
- email: "",
2239
- address: "",
2240
- postalCode: "",
2241
- city: ""
2242
- };
2243
- this.boundOnCheckoutChange = (e) => {
2244
- const detail = e.detail;
2245
- this.checkoutState = detail.state;
2246
- this.items = Vio.checkout.items;
2247
- if (this.checkoutState) {
2248
- this.refreshApplePay();
2249
- this.refreshKlarna();
2250
- } else {
2251
- this.applePayAvailable = false;
2252
- this.klarnaAvailable = false;
2253
- this.orderConfirmed = false;
2254
- this.confirmedMethod = null;
2255
- this.confirmedOrder = null;
2256
- this.paymentError = null;
2257
- this.express = false;
2258
- this.unmountKlarna();
2259
- }
2260
- };
2261
- this.boundOnPaymentComplete = (e) => {
2262
- const detail = e.detail;
2263
- if (detail.method !== "klarna" && detail.method !== "apple-pay") return;
2264
- const sponsorId = detail.state?.sponsorId ?? this.checkoutState?.sponsorId;
2265
- if (sponsorId == null) return;
2266
- this.confirmOrder(detail.method, sponsorId, detail.result);
2267
- };
2268
- this.boundOnPaymentError = (e) => {
2269
- const detail = e.detail;
2270
- this.paymentError = detail.method === "klarna" ? `Klarna-betalingen kunne ikke fullf\xF8res: ${detail.error}` : detail.error;
2271
- this.open = true;
2272
- };
2273
2195
  }
2274
2196
  connectedCallback() {
2275
2197
  super.connectedCallback();
2276
- this.checkoutState = Vio.checkout.currentState;
2277
- this.items = Vio.checkout.items;
2278
- Vio.checkout.addEventListener("change", this.boundOnCheckoutChange);
2279
- Vio.checkout.addEventListener("payment-complete", this.boundOnPaymentComplete);
2280
- Vio.checkout.addEventListener("payment-error", this.boundOnPaymentError);
2281
- if (this.checkoutState) {
2282
- void this.refreshApplePay();
2283
- void this.refreshKlarna();
2284
- }
2285
- }
2286
- disconnectedCallback() {
2287
- Vio.checkout.removeEventListener("change", this.boundOnCheckoutChange);
2288
- Vio.checkout.removeEventListener("payment-complete", this.boundOnPaymentComplete);
2289
- Vio.checkout.removeEventListener("payment-error", this.boundOnPaymentError);
2290
- this.unmountKlarna();
2291
- super.disconnectedCallback();
2198
+ this.carts = Vio.cart.getAllCarts();
2199
+ this.itemCount = Vio.cart.itemCount;
2200
+ Vio.cart.addEventListener("change", this.boundOnCartChange);
2201
+ void this.loadAvailablePaymentMethods();
2292
2202
  }
2293
- updated() {
2294
- void this.mountKlarnaIfNeeded();
2203
+ disconnectedCallback() {
2204
+ Vio.cart.removeEventListener("change", this.boundOnCartChange);
2205
+ super.disconnectedCallback();
2295
2206
  }
2296
2207
  close() {
2297
2208
  this.open = false;
2298
- this.express = false;
2299
- Vio.checkout.close();
2300
2209
  }
2301
2210
  show() {
2302
2211
  this.open = true;
2212
+ void this.loadAvailablePaymentMethods();
2303
2213
  }
2304
- async refreshApplePay() {
2305
- Vio.checkout.clearApplePayCache();
2306
- this.applePayAvailable = await Vio.checkout.isApplePayAvailable("NO");
2307
- }
2308
- async refreshKlarna() {
2309
- const available = await Vio.checkout.klarnaPaymentsAvailable();
2310
- if (available !== this.klarnaAvailable) {
2311
- if (!available) this.unmountKlarna();
2312
- this.klarnaAvailable = available;
2313
- }
2314
- }
2315
- /** Mount (or re-mount on amount change) the Klarna Payments widget. */
2316
- async mountKlarnaIfNeeded() {
2317
- if (!this.klarnaAvailable || !this.open || !this.checkoutState) return;
2318
- if (this.checkoutState.paymentMethod !== "klarna" && !this.express) return;
2319
- if (this.klarnaMounting) return;
2320
- const amount = this.checkoutState.subtotal;
2321
- if (this.klarnaHandle && this.klarnaMountedAmount === amount && this.klarnaMountedShipping === this.selectedShipping) {
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;
2322
2222
  return;
2323
2223
  }
2324
- const container = this.renderRoot?.querySelector(
2325
- "#vio-klarna-payments-container"
2326
- );
2327
- if (!container) return;
2328
- this.klarnaMounting = true;
2329
- this.unmountKlarna();
2330
- container.innerHTML = "";
2331
2224
  try {
2332
- const handle = await Vio.checkout.mountKlarnaPayments(container, {
2333
- withShipping: this.express,
2334
- shippingId: this.selectedShipping
2335
- });
2336
- this.klarnaHandle = handle;
2337
- this.klarnaMountedAmount = amount;
2338
- this.klarnaMountedShipping = this.selectedShipping;
2339
- this.klarnaCategories = handle.categories;
2340
- this.klarnaSelectedCat = handle.selected;
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
+ }
2341
2231
  } catch (err) {
2342
2232
  if (typeof console !== "undefined") {
2343
- console.warn("[VioCheckout] Klarna Payments mount failed:", err);
2233
+ console.warn("[VioCart] Failed to load payment methods:", err);
2344
2234
  }
2345
- this.paymentError = `Kunne ikke laste Klarna: ${err instanceof Error ? err.message : String(err)}`;
2235
+ this.availableMethods = null;
2346
2236
  } finally {
2347
- this.klarnaMounting = false;
2348
- }
2349
- }
2350
- unmountKlarna() {
2351
- if (this.klarnaHandle) {
2352
- this.klarnaHandle.unmount();
2353
- this.klarnaHandle = null;
2237
+ this.loadingPaymentMethods = false;
2354
2238
  }
2355
- this.klarnaMountedAmount = null;
2356
- this.klarnaMountedShipping = null;
2357
- this.klarnaCategories = [];
2358
- this.klarnaSelectedCat = "";
2359
- this.klarnaAuthorizing = false;
2360
2239
  }
2361
- /** Selected shipping option (express flow). */
2362
- get shippingOption() {
2363
- return KLARNA_SHIPPING_OPTIONS.find((o) => o.id === this.selectedShipping) ?? null;
2364
- }
2365
- /** Pick a shipping option → re-mounts the Klarna widget with the new total. */
2366
- onSelectShipping(id) {
2367
- if (this.selectedShipping === id || this.klarnaMounting) return;
2368
- this.selectedShipping = id;
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));
2369
2245
  }
2370
- /** Switch the Klarna widget to a different payment_method_category. */
2371
- async onKlarnaCategory(identifier) {
2372
- if (!this.klarnaHandle || this.klarnaSelectedCat === identifier) return;
2373
- this.klarnaSelectedCat = identifier;
2374
- try {
2375
- await this.klarnaHandle.load(identifier);
2376
- } catch (err) {
2377
- if (typeof console !== "undefined") {
2378
- console.warn("[VioCheckout] Klarna category switch failed:", err);
2246
+ updated(changed) {
2247
+ if (changed.has("open") && this.open) {
2248
+ this.confirmedOrder = null;
2249
+ this.preparedTotal = -1;
2250
+ }
2251
+ if (this.open && this.itemCount > 0) {
2252
+ const total = this.totalAcrossSponsors;
2253
+ if (total !== this.preparedTotal) {
2254
+ this.preparedTotal = total;
2255
+ void this.refreshApplePay(total);
2379
2256
  }
2380
2257
  }
2381
2258
  }
2382
- /** Authorize the selected Klarna method → backend creates the order. */
2383
- async onKlarnaAuthorize() {
2384
- if (!this.klarnaHandle || this.klarnaAuthorizing) return;
2385
- this.paymentError = null;
2386
- this.klarnaAuthorizing = true;
2259
+ /** Pre-resolve the Apple Pay sheet for the current cart total. */
2260
+ async refreshApplePay(total) {
2261
+ if (total <= 0) {
2262
+ this.applePayHandle = null;
2263
+ this.applePayAvailable = false;
2264
+ return;
2265
+ }
2387
2266
  try {
2388
- await this.klarnaHandle.authorize();
2389
- } finally {
2390
- this.klarnaAuthorizing = false;
2267
+ const handle = await Vio.checkout.prepareApplePayExpress(total, this.primaryCurrency);
2268
+ if (this.preparedTotal !== total) return;
2269
+ this.applePayHandle = handle;
2270
+ this.applePayAvailable = handle.available;
2271
+ } catch {
2272
+ this.applePayHandle = null;
2273
+ this.applePayAvailable = false;
2391
2274
  }
2392
2275
  }
2393
- onFieldChange(field, value) {
2394
- this.form = { ...this.form, [field]: value };
2395
- Vio.checkout.setAddress(this.form);
2276
+ /** Apple Pay express — pay the whole cart, then show the confirmation. */
2277
+ onApplePay() {
2278
+ const handle = this.applePayHandle;
2279
+ if (!handle || !handle.available || this.itemCount === 0) return;
2280
+ const snapshot = this.buildConfirmation();
2281
+ void handle.show().then((result) => {
2282
+ if (!result) return;
2283
+ this.confirmedOrder = snapshot;
2284
+ for (const sponsorId of [...this.carts.keys()]) {
2285
+ Vio.cart.clearSponsorCart(sponsorId);
2286
+ }
2287
+ this.dispatchEvent(
2288
+ new CustomEvent("vio:payment-success", {
2289
+ bubbles: true,
2290
+ composed: true,
2291
+ detail: { method: "apple-pay", result }
2292
+ })
2293
+ );
2294
+ });
2396
2295
  }
2397
- async onApplePay() {
2398
- if (this.applePayInProgress) return;
2399
- this.applePayInProgress = true;
2400
- try {
2401
- await Vio.checkout.startApplePay({ country: "NO", label: "Vio" });
2402
- } catch (err) {
2403
- if (typeof console !== "undefined") {
2404
- console.warn("[VioCheckout] Apple Pay flow ended:", err);
2296
+ buildConfirmation() {
2297
+ const items = [];
2298
+ for (const cart of this.carts.values()) {
2299
+ for (const it of cart.items) {
2300
+ items.push({
2301
+ brand: it.brand,
2302
+ name: it.name,
2303
+ quantity: it.quantity,
2304
+ unitPrice: it.unitPrice,
2305
+ currency: it.currency
2306
+ });
2405
2307
  }
2406
- } finally {
2407
- this.applePayInProgress = false;
2408
2308
  }
2309
+ return { items, total: this.totalAcrossSponsors, currency: this.primaryCurrency };
2409
2310
  }
2410
- onPay(method) {
2411
- this.paymentError = null;
2412
- if (method === "apple-pay") {
2413
- void this.onApplePay();
2414
- return;
2415
- }
2416
- Vio.checkout.selectPaymentMethod(method);
2311
+ closeConfirmation() {
2312
+ this.confirmedOrder = null;
2313
+ this.close();
2417
2314
  }
2418
- /**
2419
- * Complete the order for the selected method (Vipps / Card / plain Klarna).
2420
- * Apple Pay and Klarna Express finish inside their own sheet/popup, so they
2421
- * never reach here. No real PSP is wired for Vipps/Card in this iteration —
2422
- * the capture happens backend-side in the full flow — so this confirms the
2423
- * order state-only and shows the confirmation screen.
2424
- */
2425
- onCompleteOrder() {
2426
- const s = this.checkoutState;
2427
- if (!s || !s.paymentMethod) return;
2428
- this.confirmOrder(s.paymentMethod, s.sponsorId);
2315
+ /** Klarna express — opens the side-drawer Klarna checkout for the whole cart,
2316
+ * skipping the address form (leveringsadresse) and method picker
2317
+ * (betalingsmetode). The host's checkout-open handler closes this cart and
2318
+ * opens <vio-checkout> in express mode with Klarna preselected. */
2319
+ onKlarnaExpress() {
2320
+ if (this.itemCount === 0) return;
2321
+ const firstSponsorId = [...this.carts.keys()][0];
2322
+ if (firstSponsorId === void 0) return;
2323
+ this.dispatchEvent(
2324
+ new CustomEvent("vio:checkout-open", {
2325
+ bubbles: true,
2326
+ composed: true,
2327
+ detail: { sponsorId: firstSponsorId, paymentMethod: "klarna", express: true }
2328
+ })
2329
+ );
2429
2330
  }
2430
- /**
2431
- * Shared order confirmation — reached from both the plain "Betal" CTA
2432
- * (Vipps/Card/Klarna) and the Klarna Express authorize callback. Shows
2433
- * the confirmation screen, emits `vio:payment-success`, and clears the
2434
- * paid sponsor's cart so the badge + drawer reflect the purchase.
2435
- */
2436
- confirmOrder(method, sponsorId, result) {
2437
- const r = result ?? {};
2438
- this.confirmedOrder = {
2439
- items: [...this.items],
2440
- currency: this.checkoutState?.currency ?? "",
2441
- total: typeof r.chargedTotal === "number" ? r.chargedTotal : this.checkoutState?.subtotal ?? 0,
2442
- orderId: r.order?.orderId
2443
- };
2444
- this.confirmedMethod = method;
2445
- this.orderConfirmed = true;
2446
- this.open = true;
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;
2447
2336
  this.dispatchEvent(
2448
- new CustomEvent("vio:payment-success", {
2337
+ new CustomEvent("vio:checkout-open", {
2449
2338
  bubbles: true,
2450
2339
  composed: true,
2451
- detail: { method, sponsorId, result }
2340
+ detail: { sponsorId: firstSponsorId, paymentMethod: "vipps", express: false }
2452
2341
  })
2453
2342
  );
2454
- Vio.cart.clearSponsorCart(sponsorId);
2455
2343
  }
2456
- methodLabel(method) {
2457
- switch (method) {
2458
- case "apple-pay":
2459
- return "Apple Pay";
2460
- case "klarna":
2461
- return "Klarna";
2462
- case "vipps":
2463
- return "Vipps";
2464
- case "card":
2465
- return "kort";
2466
- default:
2467
- return "";
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
+ }
2357
+ get totalAcrossSponsors() {
2358
+ let sum = 0;
2359
+ for (const cart of this.carts.values()) {
2360
+ for (const item of cart.items) sum += item.unitPrice * item.quantity;
2468
2361
  }
2362
+ return sum;
2469
2363
  }
2470
- /** True for methods that complete via a separate sheet/widget, not the CTA. */
2471
- isExpressMethod(method) {
2472
- return method === "apple-pay" || method === "klarna";
2364
+ get primaryCurrency() {
2365
+ for (const cart of this.carts.values()) return cart.currency;
2366
+ return "NOK";
2473
2367
  }
2474
- orderTotal() {
2475
- const s = this.checkoutState;
2476
- if (!s) return "";
2477
- return formatPrice(s.subtotal, s.currency);
2368
+ onIncrement(item) {
2369
+ Vio.cart.updateQuantity(item.id, item.sponsorId, item.quantity + 1);
2478
2370
  }
2479
- /** Shipping cost (major units) applied in express mode, else 0. */
2480
- shippingMajor() {
2481
- return this.express ? (this.shippingOption?.price ?? 0) / 100 : 0;
2371
+ onDecrement(item) {
2372
+ Vio.cart.updateQuantity(item.id, item.sponsorId, item.quantity - 1);
2482
2373
  }
2483
- /** Total to charge — items + shipping (express). */
2484
- payTotalLabel() {
2485
- const s = this.checkoutState;
2486
- if (!s) return "";
2487
- return formatPrice(s.subtotal + this.shippingMajor(), s.currency);
2374
+ onRemove(item) {
2375
+ Vio.cart.removeItem(item.id, item.sponsorId);
2488
2376
  }
2489
2377
  render() {
2378
+ const isEmpty = this.itemCount === 0;
2379
+ const sponsorCarts = [...this.carts.values()].filter((c) => c.items.length > 0);
2490
2380
  return html`
2491
2381
  <div class="backdrop" @click=${this.close}></div>
2492
- <div
2493
- class="modal ${this.express ? "as-side" : this.orderConfirmed ? "as-confirm" : ""}"
2494
- role="dialog"
2495
- aria-label=${this.heading}
2496
- >
2382
+ <aside class="drawer" role="dialog" aria-label=${this.heading}>
2497
2383
  <div class="handle" @click=${this.close} aria-hidden="true"></div>
2498
- <div class="topbar">
2499
- <h2 class="heading">${this.orderConfirmed ? "Takk!" : this.heading}</h2>
2384
+ <div class="header">
2385
+ <h2 class="heading">${this.confirmedOrder ? "Takk!" : this.heading}</h2>
2500
2386
  <button class="close" @click=${this.close} aria-label="Lukk">×</button>
2501
2387
  </div>
2502
2388
 
2503
- <div class="body">
2504
- ${this.orderConfirmed ? this.renderConfirmation() : this.express ? this.renderKlarnaExpress() : this.renderCheckoutBody()}
2505
- </div>
2506
- </div>
2507
- `;
2508
- }
2509
- renderConfirmation() {
2510
- const o = this.confirmedOrder;
2511
- return html`
2512
- <section class="confirmation">
2513
- <div class="confirm-check" aria-hidden="true">✓</div>
2514
- <h3 class="confirm-title">Takk for bestillingen!</h3>
2515
- <p class="confirm-text">
2516
- Betaling med ${this.methodLabel(this.confirmedMethod)} er bekreftet.${this.form.email ? html` Kvittering sendes til ${this.form.email}.` : ""}
2517
- </p>
2518
- ${o?.orderId ? html`<div class="confirm-order-id">Ordrenummer: <b>${o.orderId}</b></div>` : ""}
2519
- ${o && o.items.length > 0 ? html`
2520
- <div class="confirm-summary">
2521
- ${o.items.map(
2522
- (it) => html`
2523
- <div class="order-row">
2524
- <span>${it.brand ? `${it.brand} ` : ""}${it.name} ×${it.quantity}</span>
2525
- <span>${formatPrice(it.unitPrice * it.quantity, it.currency)}</span>
2389
+ ${this.confirmedOrder ? this.renderConfirmation() : isEmpty ? html`<div class="empty">${this.emptyLabel}</div>` : html`
2390
+ <div class="body">
2391
+ ${sponsorCarts.map(
2392
+ (cart) => html`
2393
+ <div class="sponsor-group">
2394
+ ${cart.sponsorName ? html`<div class="sponsor-label">${cart.sponsorName}</div>` : ""}
2395
+ ${cart.items.map(
2396
+ (item) => html`
2397
+ <div class="item">
2398
+ <div class="item-image">
2399
+ ${item.imageUrl ? html`<img src=${item.imageUrl} alt=${item.name} />` : ""}
2400
+ </div>
2401
+ <div>
2402
+ <div class="item-brand">${item.brand}</div>
2403
+ <div class="item-name">${item.name}</div>
2404
+ <div class="item-row">
2405
+ <span class="item-qty">
2406
+ <button
2407
+ class="qty-btn"
2408
+ @click=${() => this.onDecrement(item)}
2409
+ aria-label="Reduser"
2410
+ >
2411
+ −
2412
+ </button>
2413
+ ${item.quantity}
2414
+ <button
2415
+ class="qty-btn"
2416
+ @click=${() => this.onIncrement(item)}
2417
+ aria-label="Øk"
2418
+ >
2419
+ +
2420
+ </button>
2421
+ <button
2422
+ class="qty-btn"
2423
+ @click=${() => this.onRemove(item)}
2424
+ aria-label="Fjern"
2425
+ style="margin-left: 8px;"
2426
+ >
2427
+ ×
2428
+ </button>
2429
+ </span>
2430
+ <span class="item-price">
2431
+ ${formatPrice(
2432
+ item.unitPrice * item.quantity,
2433
+ item.currency
2434
+ )}
2435
+ </span>
2436
+ </div>
2437
+ </div>
2438
+ </div>
2439
+ `
2440
+ )}
2526
2441
  </div>
2527
2442
  `
2528
2443
  )}
2529
- <div class="order-row total">
2530
- <span>Totalt</span><span>${formatPrice(o.total, o.currency)}</span>
2444
+ </div>
2445
+ <div class="footer">
2446
+ <div class="subtotal-row">
2447
+ <span class="subtotal-label">${this.subtotalLabel}</span>
2448
+ <span class="subtotal-value">
2449
+ ${formatPrice(this.totalAcrossSponsors, this.primaryCurrency)}
2450
+ </span>
2531
2451
  </div>
2452
+ ${this.methodEnabled("apple-pay") && this.applePayAvailable ? html`
2453
+ <button
2454
+ class="applepay-btn"
2455
+ @click=${this.onApplePay}
2456
+ aria-label="Betal med Apple Pay"
2457
+ >
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>
2492
+ </button>
2493
+ ` : ""}
2532
2494
  </div>
2533
- ` : ""}
2534
- <button class="confirm-close" @click=${this.close}>Lukk</button>
2535
- </section>
2495
+ `}
2496
+ </aside>
2536
2497
  `;
2537
2498
  }
2538
- /** Klarna Payments widget panel: shipping + category chips + widget + pay button. */
2539
- renderKlarnaPanel() {
2540
- const currency = this.checkoutState?.currency ?? "NOK";
2499
+ renderConfirmation() {
2500
+ const o = this.confirmedOrder;
2501
+ if (!o) return html``;
2541
2502
  return html`
2542
- <div class="klarna-panel">
2543
- ${this.express ? html`
2544
- <div class="ship-select" role="radiogroup" aria-label="Frakt">
2545
- ${KLARNA_SHIPPING_OPTIONS.map(
2546
- (o) => html`
2547
- <button
2548
- class="ship-opt ${this.selectedShipping === o.id ? "active" : ""}"
2549
- role="radio"
2550
- aria-checked=${this.selectedShipping === o.id}
2551
- ?disabled=${this.klarnaMounting}
2552
- @click=${() => this.onSelectShipping(o.id)}
2553
- >
2554
- <span class="ship-meta">
2555
- <b>${o.method}</b><span>${o.description}</span>
2556
- </span>
2557
- <span class="ship-price">${formatPrice(o.price / 100, currency)}</span>
2558
- </button>
2559
- `
2560
- )}
2503
+ <div class="confirm">
2504
+ <div class="confirm-check" aria-hidden="true">✓</div>
2505
+ <h3>Takk for handelen!</h3>
2506
+ <p>Betaling med Apple Pay er bekreftet.</p>
2507
+ <div class="confirm-summary">
2508
+ ${o.items.map(
2509
+ (it) => html`
2510
+ <div class="order-row">
2511
+ <span>${it.brand ? `${it.brand} ` : ""}${it.name} ×${it.quantity}</span>
2512
+ <span>${formatPrice(it.unitPrice * it.quantity, it.currency)}</span>
2561
2513
  </div>
2562
- ` : ""}
2563
- ${this.klarnaCategories.length > 1 ? html`
2564
- <div class="klarna-cats">
2565
- ${this.klarnaCategories.map(
2566
- (c) => html`
2567
- <button
2568
- class="klarna-cat ${this.klarnaSelectedCat === c.identifier ? "active" : ""}"
2569
- @click=${() => void this.onKlarnaCategory(c.identifier)}
2570
- >
2571
- ${c.name}
2572
- </button>
2573
- `
2514
+ `
2574
2515
  )}
2575
- </div>
2576
- ` : ""}
2577
- <div class="klarna-widget" id="vio-klarna-payments-container"></div>
2578
- <button
2579
- class="payment-btn primary complete-cta"
2580
- @click=${() => void this.onKlarnaAuthorize()}
2581
- ?disabled=${this.klarnaAuthorizing || !this.klarnaHandle || this.klarnaMounting}
2582
- >
2583
- ${this.klarnaAuthorizing ? "Behandler\u2026" : `Betal ${this.payTotalLabel()} med Klarna`}
2584
- </button>
2585
- </div>
2586
- `;
2587
- }
2588
- /** Express layout — only the Klarna widget + a compact order summary. */
2589
- renderKlarnaExpress() {
2590
- return html`
2591
- <section class="section">
2592
- <h3 class="section-heading">Kjøp med Klarna</h3>
2593
- ${this.items.length > 0 ? html`
2594
- <div class="express-summary">
2595
- ${this.items.map(
2596
- (item) => html`
2597
- <div class="order-row">
2598
- <span>${item.brand ? `${item.brand} ` : ""}${item.name} ×${item.quantity}</span>
2599
- <span>${formatPrice(item.unitPrice * item.quantity, item.currency)}</span>
2600
- </div>
2601
- `
2602
- )}
2603
- <div class="order-row">
2604
- <span>Frakt${this.shippingOption ? ` \u2013 ${this.shippingOption.method}` : ""}</span>
2605
- <span>${formatPrice(this.shippingMajor(), this.checkoutState?.currency ?? "NOK")}</span>
2606
- </div>
2607
- <div class="order-row total">
2608
- <span>Totalt</span><span>${this.payTotalLabel()}</span>
2609
- </div>
2610
- </div>
2611
- ` : ""}
2612
- ${this.renderKlarnaPanel()}
2613
- ${this.paymentError ? html`<div class="payment-error">${this.paymentError}</div>` : ""}
2614
- </section>
2615
- `;
2616
- }
2617
- renderCheckoutBody() {
2618
- const method = this.checkoutState?.paymentMethod;
2619
- const showCompleteCta = !!method && !this.isExpressMethod(method);
2620
- return html`
2621
- <section class="section">
2622
- <div class="section-label">Steg 1</div>
2623
- <h3 class="section-heading">Leveringsadresse</h3>
2624
- <div class="form-row">
2625
- <div class="field">
2626
- <label>Fornavn</label>
2627
- <input
2628
- type="text"
2629
- autocomplete="given-name"
2630
- .value=${this.form.firstName}
2631
- @input=${(e) => this.onFieldChange("firstName", e.target.value)}
2632
- />
2633
- </div>
2634
- <div class="field">
2635
- <label>Etternavn</label>
2636
- <input
2637
- type="text"
2638
- autocomplete="family-name"
2639
- .value=${this.form.lastName}
2640
- @input=${(e) => this.onFieldChange("lastName", e.target.value)}
2641
- />
2642
- </div>
2643
- </div>
2644
- <div class="field">
2645
- <label>E-post</label>
2646
- <input
2647
- type="email"
2648
- autocomplete="email"
2649
- .value=${this.form.email}
2650
- @input=${(e) => this.onFieldChange("email", e.target.value)}
2651
- />
2652
- </div>
2653
- <div class="field">
2654
- <label>Adresse</label>
2655
- <input
2656
- type="text"
2657
- autocomplete="street-address"
2658
- .value=${this.form.address}
2659
- @input=${(e) => this.onFieldChange("address", e.target.value)}
2660
- />
2661
- </div>
2662
- <div class="form-row">
2663
- <div class="field">
2664
- <label>Postnummer</label>
2665
- <input
2666
- type="text"
2667
- autocomplete="postal-code"
2668
- .value=${this.form.postalCode}
2669
- @input=${(e) => this.onFieldChange("postalCode", e.target.value)}
2670
- />
2671
- </div>
2672
- <div class="field">
2673
- <label>Sted</label>
2674
- <input
2675
- type="text"
2676
- autocomplete="address-level2"
2677
- .value=${this.form.city}
2678
- @input=${(e) => this.onFieldChange("city", e.target.value)}
2679
- />
2680
- </div>
2681
- </div>
2682
- </section>
2683
-
2684
- <section class="section">
2685
- <div class="section-label">Steg 2</div>
2686
- <h3 class="section-heading">Velg betalingsmåte</h3>
2687
- <div class="payment-grid">
2688
- ${this.applePayAvailable ? html`
2689
- <button
2690
- class="payment-btn primary"
2691
- @click=${() => this.onPay("apple-pay")}
2692
- ?disabled=${this.applePayInProgress}
2693
- aria-pressed=${this.checkoutState?.paymentMethod === "apple-pay"}
2694
- >
2695
- ${this.applePayInProgress ? "\xC5pner\u2026" : " Pay"}
2696
- </button>
2697
- ` : ""}
2698
- ${this.klarnaAvailable ? html`
2699
- <button
2700
- class="payment-btn"
2701
- @click=${() => this.onPay("klarna")}
2702
- aria-pressed=${this.checkoutState?.paymentMethod === "klarna"}
2703
- >
2704
- Klarna
2705
- </button>
2706
- ` : ""}
2707
- <button
2708
- class="payment-btn"
2709
- @click=${() => this.onPay("vipps")}
2710
- aria-pressed=${this.checkoutState?.paymentMethod === "vipps"}
2711
- >
2712
- Vipps
2713
- </button>
2714
- <button
2715
- class="payment-btn"
2716
- @click=${() => this.onPay("card")}
2717
- aria-pressed=${this.checkoutState?.paymentMethod === "card"}
2718
- >
2719
- Kort
2720
- </button>
2721
- ${!this.applePayAvailable ? html`
2722
- <div class="apple-pay-note">
2723
- Pay krever Safari på iOS eller macOS med Apple Pay konfigurert.
2724
- </div>
2725
- ` : ""}
2726
- ${showCompleteCta ? html`
2727
- <button
2728
- class="payment-btn primary complete-cta"
2729
- @click=${this.onCompleteOrder}
2730
- >
2731
- Betal ${this.orderTotal()} med ${this.methodLabel(method ?? null)}
2732
- </button>
2733
- ` : ""}
2734
- ${method === "klarna" ? this.renderKlarnaPanel() : ""}
2735
- ${this.paymentError ? html`<div class="payment-error">${this.paymentError}</div>` : ""}
2736
- </div>
2737
- </section>
2738
-
2739
- ${this.items.length > 0 ? html`
2740
- <section class="order-summary">
2741
- ${this.items.map(
2742
- (item) => html`
2743
- <div class="order-line">
2744
- <div class="order-line-img">
2745
- ${item.imageUrl ? html`<img src=${item.imageUrl} alt=${item.name} />` : ""}
2746
- </div>
2747
- <div class="order-line-meta">
2748
- <span>${item.brand}</span>
2749
- <b>${item.name}</b>
2750
- <span>×${item.quantity}</span>
2751
- </div>
2752
- <div class="order-line-price">
2753
- ${formatPrice(item.unitPrice * item.quantity, item.currency)}
2754
- </div>
2755
- </div>
2756
- `
2757
- )}
2758
- <div class="order-row" style="margin-top: 16px;">
2759
- <span>Sum</span><span>${this.orderTotal()}</span>
2760
- </div>
2761
- <div class="order-row">
2762
- <span>Frakt</span><span>${this.shippingLabel}</span>
2763
- </div>
2764
- <div class="order-row total">
2765
- <span>Totalt</span><span>${this.orderTotal()}</span>
2766
- </div>
2767
- </section>
2768
- ` : ""}
2769
- </div>
2516
+ <div class="order-row total">
2517
+ <span>Totalt</span>
2518
+ <span>${formatPrice(o.total, o.currency)}</span>
2519
+ </div>
2520
+ </div>
2521
+ <button class="confirm-close" @click=${this.closeConfirmation}>Lukk</button>
2770
2522
  </div>
2771
2523
  `;
2772
2524
  }
2773
2525
  };
2774
- VioCheckout.styles = css`
2526
+ __decorateClass([
2527
+ property({ type: Boolean, reflect: true })
2528
+ ], VioCart.prototype, "open", 2);
2529
+ __decorateClass([
2530
+ property({ type: String })
2531
+ ], VioCart.prototype, "heading", 2);
2532
+ __decorateClass([
2533
+ property({ type: String, attribute: "empty-label" })
2534
+ ], VioCart.prototype, "emptyLabel", 2);
2535
+ __decorateClass([
2536
+ property({ type: String, attribute: "subtotal-label" })
2537
+ ], VioCart.prototype, "subtotalLabel", 2);
2538
+ __decorateClass([
2539
+ state()
2540
+ ], VioCart.prototype, "carts", 2);
2541
+ __decorateClass([
2542
+ state()
2543
+ ], VioCart.prototype, "itemCount", 2);
2544
+ __decorateClass([
2545
+ state()
2546
+ ], VioCart.prototype, "applePayAvailable", 2);
2547
+ __decorateClass([
2548
+ state()
2549
+ ], VioCart.prototype, "availableMethods", 2);
2550
+ __decorateClass([
2551
+ state()
2552
+ ], VioCart.prototype, "confirmedOrder", 2);
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 {
2557
+ constructor() {
2558
+ super(...arguments);
2559
+ this.open = false;
2560
+ this.heading = "Kasse";
2561
+ this.shippingLabel = "Beregnes ved kasse";
2562
+ this.express = false;
2563
+ this.checkoutState = null;
2564
+ this.items = [];
2565
+ this.applePayAvailable = false;
2566
+ this.applePayInProgress = false;
2567
+ this.klarnaAvailable = false;
2568
+ this.orderConfirmed = false;
2569
+ this.confirmedMethod = null;
2570
+ this.paymentError = null;
2571
+ this.paymentNotice = null;
2572
+ this.confirmedOrder = null;
2573
+ /** Active Klarna Payments widget handle + the subtotal it was mounted for. */
2574
+ this.klarnaHandle = null;
2575
+ this.klarnaMountedAmount = null;
2576
+ this.klarnaMountedShipping = null;
2577
+ this.klarnaMounting = false;
2578
+ this.klarnaCategories = [];
2579
+ this.klarnaSelectedCat = "";
2580
+ this.klarnaAuthorizing = false;
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;
2586
+ this.form = {
2587
+ firstName: "",
2588
+ lastName: "",
2589
+ email: "",
2590
+ address: "",
2591
+ postalCode: "",
2592
+ city: ""
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;
2600
+ this.boundOnCheckoutChange = (e) => {
2601
+ const detail = e.detail;
2602
+ const prevSponsorId = this.checkoutState?.sponsorId;
2603
+ const sponsorChanged = !prevSponsorId || prevSponsorId !== detail.state?.sponsorId;
2604
+ this.checkoutState = detail.state;
2605
+ this.items = Vio.checkout.items;
2606
+ if (this.checkoutState) {
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
+ }
2615
+ } else {
2616
+ this.applePayAvailable = false;
2617
+ this.klarnaAvailable = false;
2618
+ this.orderConfirmed = false;
2619
+ this.confirmedMethod = null;
2620
+ this.confirmedOrder = null;
2621
+ this.paymentError = null;
2622
+ this.paymentNotice = null;
2623
+ this.express = false;
2624
+ this.availableMethods = null;
2625
+ this.unmountKlarna();
2626
+ }
2627
+ };
2628
+ this.boundOnPaymentComplete = (e) => {
2629
+ const detail = e.detail;
2630
+ if (detail.method !== "klarna" && detail.method !== "apple-pay") return;
2631
+ const sponsorId = detail.state?.sponsorId ?? this.checkoutState?.sponsorId;
2632
+ if (sponsorId == null) return;
2633
+ this.confirmOrder(detail.method, sponsorId, detail.result);
2634
+ };
2635
+ this.boundOnPaymentError = (e) => {
2636
+ const detail = e.detail;
2637
+ this.paymentError = detail.method === "klarna" ? `Klarna-betalingen kunne ikke fullf\xF8res: ${detail.error}` : detail.error;
2638
+ this.open = true;
2639
+ };
2640
+ }
2641
+ static {
2642
+ this.styles = css`
2775
2643
  :host { display: contents; }
2776
2644
  .backdrop {
2777
2645
  position: fixed;
@@ -2834,6 +2702,21 @@ VioCheckout.styles = css`
2834
2702
  box-shadow: -8px 0 24px rgba(0, 0, 0, 0.1);
2835
2703
  }
2836
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
+ }
2837
2720
  @media (max-width: 600px) {
2838
2721
  /* On phones, keep it a bottom sheet for reachability. */
2839
2722
  .modal.as-side {
@@ -3014,8 +2897,9 @@ VioCheckout.styles = css`
3014
2897
  background: var(--vio-color-surface-hover, #fafafa);
3015
2898
  }
3016
2899
  .payment-btn:disabled {
3017
- cursor: progress;
3018
- opacity: 0.6;
2900
+ cursor: not-allowed;
2901
+ opacity: 0.45;
2902
+ pointer-events: none;
3019
2903
  }
3020
2904
  .payment-btn.primary {
3021
2905
  background: var(--vio-color-text, #0a0a0a);
@@ -3099,9 +2983,27 @@ VioCheckout.styles = css`
3099
2983
  color: var(--vio-color-accent, #c14a3b);
3100
2984
  text-align: center;
3101
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
+ }
3102
2996
  /* "Betal" CTA — sits full-width below the method grid once a method is
3103
2997
  picked. Reuses .payment-btn.primary (its grid-column spans the row). */
3104
- .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
+ }
3105
3007
 
3106
3008
  /* Order confirmation — compact, shown inside the drawer. */
3107
3009
  .confirmation {
@@ -3169,116 +3071,1152 @@ VioCheckout.styles = css`
3169
3071
  font-family: inherit;
3170
3072
  margin-top: 16px;
3171
3073
  }
3172
- .confirm-close:hover { background: #222; }
3074
+ .confirm-close:hover { background: #222; }
3075
+
3076
+ .order-summary {
3077
+ background: var(--vio-color-surface-muted, #f2f2f2);
3078
+ padding: var(--vio-space-lg, 24px);
3079
+ margin-top: var(--vio-space-xl, 32px);
3080
+ }
3081
+ .order-line {
3082
+ display: grid;
3083
+ grid-template-columns: 56px 1fr auto;
3084
+ gap: 12px;
3085
+ padding: 8px 0;
3086
+ align-items: center;
3087
+ }
3088
+ .order-line-img {
3089
+ aspect-ratio: 1;
3090
+ background: #fff;
3091
+ overflow: hidden;
3092
+ }
3093
+ .order-line-img img { width: 100%; height: 100%; object-fit: cover; display: block; }
3094
+ .order-line-meta { font-size: 13px; }
3095
+ .order-line-meta b { display: block; }
3096
+ .order-line-meta span {
3097
+ color: var(--vio-color-text-secondary, #666);
3098
+ font-size: 11px;
3099
+ text-transform: uppercase;
3100
+ letter-spacing: var(--vio-tracking-label, 0.1em);
3101
+ }
3102
+ .order-line-price { font-weight: 700; font-size: 14px; }
3103
+ .order-row {
3104
+ display: flex;
3105
+ justify-content: space-between;
3106
+ padding: 8px 0;
3107
+ font-size: 14px;
3108
+ }
3109
+ .order-row.total {
3110
+ border-top: 1px solid var(--vio-color-border-default, #ccc);
3111
+ margin-top: var(--vio-space-sm, 8px);
3112
+ padding-top: var(--vio-space-md, 16px);
3113
+ font-weight: 700;
3114
+ font-size: 16px;
3115
+ }
3116
+
3117
+ @media (max-width: 600px) {
3118
+ .form-row { grid-template-columns: 1fr; gap: 0; }
3119
+ .payment-grid { grid-template-columns: 1fr; }
3120
+ .body { padding: 32px var(--vio-space-lg, 24px) 64px; }
3121
+ }
3122
+ `;
3123
+ }
3124
+ connectedCallback() {
3125
+ super.connectedCallback();
3126
+ this.checkoutState = Vio.checkout.currentState;
3127
+ this.items = Vio.checkout.items;
3128
+ Vio.checkout.addEventListener("change", this.boundOnCheckoutChange);
3129
+ Vio.checkout.addEventListener("payment-complete", this.boundOnPaymentComplete);
3130
+ Vio.checkout.addEventListener("payment-error", this.boundOnPaymentError);
3131
+ if (this.checkoutState) {
3132
+ void this.refreshApplePay();
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 {
3337
+ }
3338
+ }
3339
+ disconnectedCallback() {
3340
+ Vio.checkout.removeEventListener("change", this.boundOnCheckoutChange);
3341
+ Vio.checkout.removeEventListener("payment-complete", this.boundOnPaymentComplete);
3342
+ Vio.checkout.removeEventListener("payment-error", this.boundOnPaymentError);
3343
+ this.unmountKlarna();
3344
+ super.disconnectedCallback();
3345
+ }
3346
+ updated(changed) {
3347
+ if (changed.has("open") && this.open && this.availableShippingsList.length === 0) {
3348
+ void this.loadAvailableShippings();
3349
+ }
3350
+ void this.mountKlarnaIfNeeded();
3351
+ }
3352
+ close() {
3353
+ this.open = false;
3354
+ this.express = false;
3355
+ Vio.checkout.close();
3356
+ }
3357
+ show() {
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));
3436
+ }
3437
+ async refreshApplePay() {
3438
+ Vio.checkout.clearApplePayCache();
3439
+ this.applePayAvailable = await Vio.checkout.isApplePayAvailable("NO");
3440
+ }
3441
+ async refreshKlarna() {
3442
+ const available = await Vio.checkout.klarnaPaymentsAvailable();
3443
+ if (available !== this.klarnaAvailable) {
3444
+ if (!available) this.unmountKlarna();
3445
+ this.klarnaAvailable = available;
3446
+ }
3447
+ }
3448
+ /** Mount (or re-mount on amount change) the Klarna Payments widget. */
3449
+ async mountKlarnaIfNeeded() {
3450
+ if (!this.klarnaAvailable || !this.open || !this.checkoutState) return;
3451
+ if (this.checkoutState.paymentMethod !== "klarna" && !this.express) return;
3452
+ if (this.klarnaMounting) return;
3453
+ const amount = this.checkoutState.subtotal;
3454
+ if (this.klarnaHandle && this.klarnaMountedAmount === amount && this.klarnaMountedShipping === this.selectedShipping) {
3455
+ return;
3456
+ }
3457
+ const container = this.renderRoot?.querySelector(
3458
+ "#vio-klarna-payments-container"
3459
+ );
3460
+ if (!container) return;
3461
+ this.klarnaMounting = true;
3462
+ this.unmountKlarna();
3463
+ container.innerHTML = "";
3464
+ try {
3465
+ const handle = await Vio.checkout.mountKlarnaPayments(container, {
3466
+ withShipping: this.express,
3467
+ shippingId: this.selectedShipping
3468
+ });
3469
+ this.klarnaHandle = handle;
3470
+ this.klarnaMountedAmount = amount;
3471
+ this.klarnaMountedShipping = this.selectedShipping;
3472
+ this.klarnaCategories = handle.categories;
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
+ }
3480
+ } catch (err) {
3481
+ if (typeof console !== "undefined") {
3482
+ console.warn("[VioCheckout] Klarna Payments mount failed:", err);
3483
+ }
3484
+ this.paymentError = `Kunne ikke laste Klarna: ${err instanceof Error ? err.message : String(err)}`;
3485
+ } finally {
3486
+ this.klarnaMounting = false;
3487
+ }
3488
+ }
3489
+ unmountKlarna() {
3490
+ if (this.klarnaHandle) {
3491
+ this.klarnaHandle.unmount();
3492
+ this.klarnaHandle = null;
3493
+ }
3494
+ this.klarnaMountedAmount = null;
3495
+ this.klarnaMountedShipping = null;
3496
+ this.klarnaCategories = [];
3497
+ this.klarnaSelectedCat = "";
3498
+ this.klarnaAuthorizing = false;
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
+ }
3504
+ /** Selected shipping option (express flow). */
3505
+ get shippingOption() {
3506
+ const list = this.shippingList;
3507
+ return list.find((o) => o.id === this.selectedShipping) ?? list[0] ?? null;
3508
+ }
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) {
3512
+ if (this.selectedShipping === id || this.klarnaMounting) return;
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
+ }
3527
+ }
3528
+ /** Switch the Klarna widget to a different payment_method_category. */
3529
+ async onKlarnaCategory(identifier) {
3530
+ if (!this.klarnaHandle || this.klarnaSelectedCat === identifier) return;
3531
+ this.klarnaSelectedCat = identifier;
3532
+ try {
3533
+ await this.klarnaHandle.load(identifier);
3534
+ } catch (err) {
3535
+ if (typeof console !== "undefined") {
3536
+ console.warn("[VioCheckout] Klarna category switch failed:", err);
3537
+ }
3538
+ }
3539
+ }
3540
+ /** Authorize the selected Klarna method → backend creates the order. */
3541
+ async onKlarnaAuthorize() {
3542
+ if (!this.klarnaHandle || this.klarnaAuthorizing) return;
3543
+ this.paymentError = null;
3544
+ this.paymentNotice = null;
3545
+ this.klarnaAuthorizing = true;
3546
+ try {
3547
+ await this.klarnaHandle.authorize();
3548
+ } finally {
3549
+ this.klarnaAuthorizing = false;
3550
+ }
3551
+ }
3552
+ onFieldChange(field, value) {
3553
+ this.form = { ...this.form, [field]: value };
3554
+ Vio.checkout.setAddress(this.form);
3555
+ }
3556
+ async onApplePay() {
3557
+ if (this.applePayInProgress) return;
3558
+ this.applePayInProgress = true;
3559
+ try {
3560
+ await Vio.checkout.startApplePay({ country: "NO", label: "Vio" });
3561
+ } catch (err) {
3562
+ if (typeof console !== "undefined") {
3563
+ console.warn("[VioCheckout] Apple Pay flow ended:", err);
3564
+ }
3565
+ } finally {
3566
+ this.applePayInProgress = false;
3567
+ }
3568
+ }
3569
+ async onPay(method) {
3570
+ this.paymentError = null;
3571
+ this.paymentNotice = null;
3572
+ if (method === "apple-pay") {
3573
+ void this.onApplePay();
3574
+ return;
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
+ }
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
+ }
3622
+ }
3623
+ /**
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.
3627
+ */
3628
+ onCompleteOrder() {
3629
+ const s = this.checkoutState;
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
+ }
3643
+ this.confirmOrder(s.paymentMethod, s.sponsorId);
3644
+ }
3645
+ /**
3646
+ * Shared order confirmation — reached from both the plain "Betal" CTA
3647
+ * (Vipps/Card/Klarna) and the Klarna Express authorize callback. Shows
3648
+ * the confirmation screen, emits `vio:payment-success`, and clears the
3649
+ * paid sponsor's cart so the badge + drawer reflect the purchase.
3650
+ */
3651
+ confirmOrder(method, sponsorId, result) {
3652
+ const r = result ?? {};
3653
+ this.confirmedOrder = {
3654
+ items: [...this.items],
3655
+ currency: this.checkoutState?.currency ?? "",
3656
+ total: typeof r.chargedTotal === "number" ? r.chargedTotal : this.checkoutState?.subtotal ?? 0,
3657
+ orderId: r.order?.orderId
3658
+ };
3659
+ this.confirmedMethod = method;
3660
+ this.orderConfirmed = true;
3661
+ this.open = true;
3662
+ this.dispatchEvent(
3663
+ new CustomEvent("vio:payment-success", {
3664
+ bubbles: true,
3665
+ composed: true,
3666
+ detail: { method, sponsorId, result }
3667
+ })
3668
+ );
3669
+ Vio.cart.clearSponsorCart(sponsorId);
3670
+ }
3671
+ methodLabel(method) {
3672
+ switch (method) {
3673
+ case "apple-pay":
3674
+ return "Apple Pay";
3675
+ case "klarna":
3676
+ return "Klarna";
3677
+ case "vipps":
3678
+ return "Vipps";
3679
+ case "stripe":
3680
+ return "Stripe";
3681
+ case "card":
3682
+ return "Kort";
3683
+ default:
3684
+ return "";
3685
+ }
3686
+ }
3687
+ /** True for methods that complete via a separate sheet/widget, not the CTA. */
3688
+ isExpressMethod(method) {
3689
+ return method === "apple-pay" || method === "klarna";
3690
+ }
3691
+ orderTotal() {
3692
+ const s = this.checkoutState;
3693
+ if (!s) return "";
3694
+ return formatPrice(s.subtotal, s.currency);
3695
+ }
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). */
3699
+ shippingMajor() {
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;
3704
+ }
3705
+ /** Total to charge — items + shipping (express). */
3706
+ payTotalLabel() {
3707
+ const s = this.checkoutState;
3708
+ if (!s) return "";
3709
+ return formatPrice(s.subtotal + this.shippingMajor(), s.currency);
3710
+ }
3711
+ render() {
3712
+ return html`
3713
+ <div class="backdrop" @click=${this.close}></div>
3714
+ <div
3715
+ class="modal ${this.express ? "as-side" : this.orderConfirmed ? "as-confirm" : ""}"
3716
+ role="dialog"
3717
+ aria-label=${this.heading}
3718
+ >
3719
+ <div class="handle" @click=${this.close} aria-hidden="true"></div>
3720
+ <div class="topbar">
3721
+ <h2 class="heading">${this.orderConfirmed ? "Takk!" : this.heading}</h2>
3722
+ <button class="close" @click=${this.close} aria-label="Lukk">×</button>
3723
+ </div>
3724
+
3725
+ <div class="body">
3726
+ ${this.orderConfirmed ? this.renderConfirmation() : this.express ? this.renderKlarnaExpress() : this.renderCheckoutBody()}
3727
+ </div>
3728
+ </div>
3729
+ `;
3730
+ }
3731
+ renderConfirmation() {
3732
+ const o = this.confirmedOrder;
3733
+ return html`
3734
+ <section class="confirmation">
3735
+ <div class="confirm-check" aria-hidden="true">✓</div>
3736
+ <h3 class="confirm-title">Takk for bestillingen!</h3>
3737
+ <p class="confirm-text">
3738
+ Betaling med ${this.methodLabel(this.confirmedMethod)} er bekreftet.${this.form.email ? html` Kvittering sendes til ${this.form.email}.` : ""}
3739
+ </p>
3740
+ ${o?.orderId ? html`<div class="confirm-order-id">Ordrenummer: <b>${o.orderId}</b></div>` : ""}
3741
+ ${o && o.items.length > 0 ? html`
3742
+ <div class="confirm-summary">
3743
+ ${o.items.map(
3744
+ (it) => html`
3745
+ <div class="order-row">
3746
+ <span>${it.brand ? `${it.brand} ` : ""}${it.name} ×${it.quantity}</span>
3747
+ <span>${formatPrice(it.unitPrice * it.quantity, it.currency)}</span>
3748
+ </div>
3749
+ `
3750
+ )}
3751
+ <div class="order-row total">
3752
+ <span>Totalt</span><span>${formatPrice(o.total, o.currency)}</span>
3753
+ </div>
3754
+ </div>
3755
+ ` : ""}
3756
+ <button class="confirm-close" @click=${this.close}>Lukk</button>
3757
+ </section>
3758
+ `;
3759
+ }
3760
+ /** Klarna Payments widget panel: shipping + category chips + widget + pay button. */
3761
+ renderKlarnaPanel() {
3762
+ const currency = this.checkoutState?.currency || getGlobalCurrency();
3763
+ return html`
3764
+ <div class="klarna-panel">
3765
+ ${this.express ? html`
3766
+ <div class="ship-select" role="radiogroup" aria-label="Frakt">
3767
+ ${this.shippingList.map(
3768
+ (o) => html`
3769
+ <button
3770
+ class="ship-opt ${this.selectedShipping === o.id ? "active" : ""}"
3771
+ role="radio"
3772
+ aria-checked=${this.selectedShipping === o.id}
3773
+ ?disabled=${this.klarnaMounting}
3774
+ @click=${() => void this.onSelectShipping(o.id)}
3775
+ >
3776
+ <span class="ship-meta">
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)}
3781
+ </span>
3782
+ </button>
3783
+ `
3784
+ )}
3785
+ </div>
3786
+ ` : ""}
3787
+ ${this.klarnaCategories.length > 1 ? html`
3788
+ <div class="klarna-cats">
3789
+ ${this.klarnaCategories.map(
3790
+ (c) => html`
3791
+ <button
3792
+ class="klarna-cat ${this.klarnaSelectedCat === c.identifier ? "active" : ""}"
3793
+ @click=${() => void this.onKlarnaCategory(c.identifier)}
3794
+ >
3795
+ ${c.name}
3796
+ </button>
3797
+ `
3798
+ )}
3799
+ </div>
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
+ ` : ""}
3806
+ <div class="klarna-widget" id="vio-klarna-payments-container"></div>
3807
+ <button
3808
+ class="payment-btn primary complete-cta"
3809
+ @click=${() => void this.onKlarnaAuthorize()}
3810
+ ?disabled=${this.klarnaAuthorizing || !this.klarnaHandle || this.klarnaMounting}
3811
+ >
3812
+ ${this.klarnaAuthorizing ? "Behandler\u2026" : this.klarnaMounting ? "Klargj\xF8r Klarna\u2026" : `Betal ${this.payTotalLabel()} med Klarna`}
3813
+ </button>
3814
+ </div>
3815
+ `;
3816
+ }
3817
+ /** Express layout — only the Klarna widget + a compact order summary. */
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
+ }
3838
+ return html`
3839
+ <section class="section">
3840
+ <h3 class="section-heading">Kjøp med Klarna</h3>
3841
+ ${this.items.length > 0 ? html`
3842
+ <div class="express-summary">
3843
+ ${this.items.map(
3844
+ (item) => html`
3845
+ <div class="order-row">
3846
+ <span>${item.brand ? `${item.brand} ` : ""}${item.name} ×${item.quantity}</span>
3847
+ <span>${formatPrice(item.unitPrice * item.quantity, item.currency || currency)}</span>
3848
+ </div>
3849
+ `
3850
+ )}
3851
+ <div class="order-row">
3852
+ <span>Frakt${this.shippingOption ? ` \u2013 ${this.shippingOption.method}` : ""}</span>
3853
+ <span>${formatPrice(this.shippingMajor(), currency)}</span>
3854
+ </div>
3855
+ <div class="order-row total">
3856
+ <span>Totalt</span><span>${this.payTotalLabel()}</span>
3857
+ </div>
3858
+ </div>
3859
+ ` : ""}
3860
+ ${this.renderKlarnaPanel()}
3861
+ ${this.paymentNotice ? html`<div class="payment-notice">${this.paymentNotice}</div>` : ""}
3862
+ ${this.paymentError ? html`<div class="payment-error">${this.paymentError}</div>` : ""}
3863
+ </section>
3864
+ `;
3865
+ }
3866
+ renderCheckoutBody() {
3867
+ const method = this.checkoutState?.paymentMethod;
3868
+ const showCompleteCta = !!method && !this.isExpressMethod(method);
3869
+ const isVipps = method === "vipps";
3870
+ return html`
3871
+ ${!isVipps ? html`
3872
+ <section class="section">
3873
+ <div class="section-label">Steg 1</div>
3874
+ <h3 class="section-heading">Leveringsadresse</h3>
3875
+ <div class="form-row">
3876
+ <div class="field">
3877
+ <label>Fornavn *</label>
3878
+ <input
3879
+ type="text"
3880
+ required
3881
+ autocomplete="given-name"
3882
+ .value=${this.form.firstName}
3883
+ @input=${(e) => this.onFieldChange("firstName", e.target.value)}
3884
+ />
3885
+ </div>
3886
+ <div class="field">
3887
+ <label>Etternavn *</label>
3888
+ <input
3889
+ type="text"
3890
+ required
3891
+ autocomplete="family-name"
3892
+ .value=${this.form.lastName}
3893
+ @input=${(e) => this.onFieldChange("lastName", e.target.value)}
3894
+ />
3895
+ </div>
3896
+ </div>
3897
+ <div class="field">
3898
+ <label>E-post *</label>
3899
+ <input
3900
+ type="email"
3901
+ required
3902
+ autocomplete="email"
3903
+ .value=${this.form.email}
3904
+ @input=${(e) => this.onFieldChange("email", e.target.value)}
3905
+ />
3906
+ </div>
3907
+ <div class="field">
3908
+ <label>Adresse *</label>
3909
+ <input
3910
+ type="text"
3911
+ required
3912
+ autocomplete="street-address"
3913
+ .value=${this.form.address}
3914
+ @input=${(e) => this.onFieldChange("address", e.target.value)}
3915
+ />
3916
+ </div>
3917
+ <div class="form-row">
3918
+ <div class="field">
3919
+ <label>Postnummer *</label>
3920
+ <input
3921
+ type="text"
3922
+ required
3923
+ autocomplete="postal-code"
3924
+ .value=${this.form.postalCode}
3925
+ @input=${(e) => this.onFieldChange("postalCode", e.target.value)}
3926
+ />
3927
+ </div>
3928
+ <div class="field">
3929
+ <label>Sted *</label>
3930
+ <input
3931
+ type="text"
3932
+ required
3933
+ autocomplete="address-level2"
3934
+ .value=${this.form.city}
3935
+ @input=${(e) => this.onFieldChange("city", e.target.value)}
3936
+ />
3937
+ </div>
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
+ ` : ""}
3968
+ </section>
3969
+ ` : ""}
3173
3970
 
3174
- .order-summary {
3175
- background: var(--vio-color-surface-muted, #f2f2f2);
3176
- padding: var(--vio-space-lg, 24px);
3177
- margin-top: var(--vio-space-xl, 32px);
3178
- }
3179
- .order-line {
3180
- display: grid;
3181
- grid-template-columns: 56px 1fr auto;
3182
- gap: 12px;
3183
- padding: 8px 0;
3184
- align-items: center;
3185
- }
3186
- .order-line-img {
3187
- aspect-ratio: 1;
3188
- background: #fff;
3189
- overflow: hidden;
3190
- }
3191
- .order-line-img img { width: 100%; height: 100%; object-fit: cover; display: block; }
3192
- .order-line-meta { font-size: 13px; }
3193
- .order-line-meta b { display: block; }
3194
- .order-line-meta span {
3195
- color: var(--vio-color-text-secondary, #666);
3196
- font-size: 11px;
3197
- text-transform: uppercase;
3198
- letter-spacing: var(--vio-tracking-label, 0.1em);
3199
- }
3200
- .order-line-price { font-weight: 700; font-size: 14px; }
3201
- .order-row {
3202
- display: flex;
3203
- justify-content: space-between;
3204
- padding: 8px 0;
3205
- font-size: 14px;
3206
- }
3207
- .order-row.total {
3208
- border-top: 1px solid var(--vio-color-border-default, #ccc);
3209
- margin-top: var(--vio-space-sm, 8px);
3210
- padding-top: var(--vio-space-md, 16px);
3211
- font-weight: 700;
3212
- font-size: 16px;
3213
- }
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
+ ` : ""}
3214
3986
 
3215
- @media (max-width: 600px) {
3216
- .form-row { grid-template-columns: 1fr; gap: 0; }
3217
- .payment-grid { grid-template-columns: 1fr; }
3218
- .body { padding: 32px var(--vio-space-lg, 24px) 64px; }
3219
- }
3220
- `;
3987
+ <section class="section">
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>
3994
+ <button
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
+ }}
4001
+ >
4002
+ Endre
4003
+ </button>
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>` : ""}
4095
+ </section>
4096
+
4097
+ ${this.items.length > 0 ? html`
4098
+ <section class="order-summary">
4099
+ ${this.items.map(
4100
+ (item) => html`
4101
+ <div class="order-line">
4102
+ <div class="order-line-img">
4103
+ ${item.imageUrl ? html`<img src=${item.imageUrl} alt=${item.name} />` : ""}
4104
+ </div>
4105
+ <div class="order-line-meta">
4106
+ <span>${item.brand}</span>
4107
+ <b>${item.name}</b>
4108
+ <span>×${item.quantity}</span>
4109
+ </div>
4110
+ <div class="order-line-price">
4111
+ ${formatPrice(item.unitPrice * item.quantity, item.currency)}
4112
+ </div>
4113
+ </div>
4114
+ `
4115
+ )}
4116
+ <div class="order-row" style="margin-top: 16px;">
4117
+ <span>Sum</span><span>${this.orderTotal()}</span>
4118
+ </div>
4119
+ <div class="order-row">
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>
4122
+ </div>
4123
+ <div class="order-row total">
4124
+ <span>Totalt</span><span>${this.payTotalLabel()}</span>
4125
+ </div>
4126
+ </section>
4127
+ ` : ""}
4128
+ </div>
4129
+ </div>
4130
+ `;
4131
+ }
4132
+ };
3221
4133
  __decorateClass([
3222
4134
  property({ type: Boolean, reflect: true })
3223
- ], VioCheckout.prototype, "open", 2);
4135
+ ], _VioCheckout.prototype, "open", 2);
3224
4136
  __decorateClass([
3225
4137
  property({ type: String })
3226
- ], VioCheckout.prototype, "heading", 2);
4138
+ ], _VioCheckout.prototype, "heading", 2);
3227
4139
  __decorateClass([
3228
4140
  property({ type: String, attribute: "shipping-label" })
3229
- ], VioCheckout.prototype, "shippingLabel", 2);
4141
+ ], _VioCheckout.prototype, "shippingLabel", 2);
3230
4142
  __decorateClass([
3231
4143
  property({ type: Boolean, reflect: true })
3232
- ], 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);
3233
4151
  __decorateClass([
3234
4152
  state()
3235
- ], VioCheckout.prototype, "checkoutState", 2);
4153
+ ], _VioCheckout.prototype, "applePayAvailable", 2);
3236
4154
  __decorateClass([
3237
4155
  state()
3238
- ], VioCheckout.prototype, "items", 2);
4156
+ ], _VioCheckout.prototype, "applePayInProgress", 2);
3239
4157
  __decorateClass([
3240
4158
  state()
3241
- ], VioCheckout.prototype, "applePayAvailable", 2);
4159
+ ], _VioCheckout.prototype, "klarnaAvailable", 2);
3242
4160
  __decorateClass([
3243
4161
  state()
3244
- ], VioCheckout.prototype, "applePayInProgress", 2);
4162
+ ], _VioCheckout.prototype, "orderConfirmed", 2);
3245
4163
  __decorateClass([
3246
4164
  state()
3247
- ], VioCheckout.prototype, "klarnaAvailable", 2);
4165
+ ], _VioCheckout.prototype, "confirmedMethod", 2);
3248
4166
  __decorateClass([
3249
4167
  state()
3250
- ], VioCheckout.prototype, "orderConfirmed", 2);
4168
+ ], _VioCheckout.prototype, "paymentError", 2);
3251
4169
  __decorateClass([
3252
4170
  state()
3253
- ], VioCheckout.prototype, "confirmedMethod", 2);
4171
+ ], _VioCheckout.prototype, "paymentNotice", 2);
3254
4172
  __decorateClass([
3255
4173
  state()
3256
- ], VioCheckout.prototype, "paymentError", 2);
4174
+ ], _VioCheckout.prototype, "confirmedOrder", 2);
3257
4175
  __decorateClass([
3258
4176
  state()
3259
- ], VioCheckout.prototype, "confirmedOrder", 2);
4177
+ ], _VioCheckout.prototype, "klarnaCategories", 2);
3260
4178
  __decorateClass([
3261
4179
  state()
3262
- ], VioCheckout.prototype, "klarnaCategories", 2);
4180
+ ], _VioCheckout.prototype, "klarnaSelectedCat", 2);
3263
4181
  __decorateClass([
3264
4182
  state()
3265
- ], VioCheckout.prototype, "klarnaSelectedCat", 2);
4183
+ ], _VioCheckout.prototype, "klarnaAuthorizing", 2);
3266
4184
  __decorateClass([
3267
4185
  state()
3268
- ], VioCheckout.prototype, "klarnaAuthorizing", 2);
4186
+ ], _VioCheckout.prototype, "selectedShipping", 2);
3269
4187
  __decorateClass([
3270
4188
  state()
3271
- ], VioCheckout.prototype, "selectedShipping", 2);
4189
+ ], _VioCheckout.prototype, "availableShippingsList", 2);
3272
4190
  __decorateClass([
3273
4191
  state()
3274
- ], VioCheckout.prototype, "form", 2);
3275
- VioCheckout = __decorateClass([
3276
- customElement("vio-checkout")
3277
- ], VioCheckout);
4192
+ ], _VioCheckout.prototype, "availableMethods", 2);
4193
+ __decorateClass([
4194
+ state()
4195
+ ], _VioCheckout.prototype, "stripeLoading", 2);
4196
+ __decorateClass([
4197
+ state()
4198
+ ], _VioCheckout.prototype, "vippsLoading", 2);
4199
+ __decorateClass([
4200
+ state()
4201
+ ], _VioCheckout.prototype, "form", 2);
4202
+ var VioCheckout = _VioCheckout;
3278
4203
 
3279
- // src/ui/index.ts
3280
- var VIO_UI_VERSION = "0.1.0";
4204
+ // src/ui/elements.ts
4205
+ function registerVioElements() {
4206
+ if (typeof customElements === "undefined" || typeof customElements.get !== "function" || typeof customElements.define !== "function")
4207
+ return;
4208
+ const defs = [
4209
+ ["vio-product", VioProduct],
4210
+ ["vio-product-carousel", VioProductCarousel],
4211
+ ["vio-product-detail", VioProductDetail],
4212
+ ["vio-cart", VioCart],
4213
+ ["vio-checkout", VioCheckout]
4214
+ ];
4215
+ for (const [tag, ctor] of defs) {
4216
+ if (!customElements.get(tag)) customElements.define(tag, ctor);
4217
+ }
4218
+ }
3281
4219
 
3282
- export { VIO_UI_VERSION, VioCart, VioCheckout, VioProduct, VioProductCarousel, VioProductDetail, VioTokens };
3283
- //# sourceMappingURL=chunk-4CCGE7DE.js.map
3284
- //# sourceMappingURL=chunk-4CCGE7DE.js.map
4220
+ export { VioCart, VioCheckout, VioProduct, VioProductCarousel, VioProductDetail, registerVioElements };
4221
+ //# sourceMappingURL=chunk-NWORYWIL.js.map
4222
+ //# sourceMappingURL=chunk-NWORYWIL.js.map