@vio-live/web-sdk 0.1.0 → 0.2.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, formatPrice, KLARNA_SHIPPING_OPTIONS } from './chunk-SOO2G6CF.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);
@@ -144,6 +18,116 @@ var VioProduct = class extends LitElement {
144
18
  this.justAdded = false;
145
19
  this.boundCartChange = () => this.updateInCart();
146
20
  }
21
+ static {
22
+ this.styles = css`
23
+ :host {
24
+ display: block;
25
+ box-sizing: border-box;
26
+ width: 100%;
27
+ cursor: pointer;
28
+ font-family: var(--vio-font-sans, -apple-system, sans-serif);
29
+ color: var(--vio-color-text, #0a0a0a);
30
+ -webkit-tap-highlight-color: transparent;
31
+ }
32
+ .card {
33
+ display: flex;
34
+ flex-direction: column;
35
+ height: 100%;
36
+ }
37
+ .image {
38
+ position: relative;
39
+ aspect-ratio: 1 / 1;
40
+ width: 100%;
41
+ background-color: var(--vio-color-surface-muted, #f2f2f2);
42
+ display: flex;
43
+ align-items: center;
44
+ justify-content: center;
45
+ overflow: hidden;
46
+ transition: opacity 0.2s ease;
47
+ }
48
+ :host(:hover) .image img { opacity: 0.92; }
49
+
50
+ /* Quick add-to-cart button — overlaid bottom-right of the image, like the
51
+ reference card's wishlist heart but wired to add the product to the cart. */
52
+ .add-btn {
53
+ position: absolute;
54
+ right: 10px;
55
+ bottom: 10px;
56
+ width: 40px;
57
+ height: 40px;
58
+ border-radius: 50%;
59
+ background: rgba(255, 255, 255, 0.96);
60
+ border: none;
61
+ display: flex;
62
+ align-items: center;
63
+ justify-content: center;
64
+ cursor: pointer;
65
+ color: #0a0a0a;
66
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.16);
67
+ transition: transform 0.14s ease, background 0.14s ease, color 0.14s ease;
68
+ padding: 0;
69
+ z-index: 2;
70
+ }
71
+ .add-btn:hover { background: #fff; transform: scale(1.08); }
72
+ .add-btn:active { transform: scale(0.95); }
73
+ /* In cart → black circle with a white tick (matches the floating cart). */
74
+ .add-btn.in-cart { background: #0a0a0a; color: #fff; }
75
+ .add-btn.in-cart:hover { background: #222; }
76
+ .add-btn.pop { animation: vio-add-pop 0.42s cubic-bezier(0.34, 1.56, 0.64, 1); }
77
+ .add-btn svg { width: 18px; height: 18px; }
78
+ .add-btn .add-count { font-size: 15px; font-weight: 700; line-height: 1; }
79
+ @keyframes vio-add-pop {
80
+ 0% { transform: scale(1); }
81
+ 35% { transform: scale(1.32); }
82
+ 60% { transform: scale(0.9); }
83
+ 100% { transform: scale(1); }
84
+ }
85
+ .image img {
86
+ width: 100%;
87
+ height: 100%;
88
+ object-fit: cover;
89
+ display: block;
90
+ }
91
+ .meta {
92
+ padding-top: 14px;
93
+ display: flex;
94
+ flex-direction: column;
95
+ flex: 1;
96
+ }
97
+ .brand {
98
+ font-size: var(--vio-size-xs, 12px);
99
+ letter-spacing: var(--vio-tracking-label, 0.1em);
100
+ text-transform: uppercase;
101
+ color: var(--vio-color-text-secondary, #666);
102
+ font-weight: 500;
103
+ margin-bottom: 6px;
104
+ }
105
+ .name {
106
+ font-size: var(--vio-size-md, 16px);
107
+ font-weight: 500;
108
+ line-height: 1.3;
109
+ margin: 0 0 10px;
110
+ display: -webkit-box;
111
+ -webkit-line-clamp: 2;
112
+ line-clamp: 2;
113
+ -webkit-box-orient: vertical;
114
+ overflow: hidden;
115
+ }
116
+ .price {
117
+ font-size: var(--vio-size-md, 16px);
118
+ font-weight: 700;
119
+ margin-bottom: 16px;
120
+ }
121
+ .retailer {
122
+ font-size: var(--vio-size-xs, 12px);
123
+ letter-spacing: var(--vio-tracking-label, 0.1em);
124
+ text-transform: uppercase;
125
+ color: var(--vio-color-text-tertiary, #999);
126
+ font-weight: 500;
127
+ margin-top: auto;
128
+ }
129
+ `;
130
+ }
147
131
  connectedCallback() {
148
132
  super.connectedCallback();
149
133
  this.updateInCart();
@@ -262,114 +246,6 @@ var VioProduct = class extends LitElement {
262
246
  `;
263
247
  }
264
248
  };
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
249
  __decorateClass([
374
250
  property({ type: String })
375
251
  ], VioProduct.prototype, "brand", 2);
@@ -403,9 +279,6 @@ __decorateClass([
403
279
  __decorateClass([
404
280
  state()
405
281
  ], VioProduct.prototype, "justAdded", 2);
406
- VioProduct = __decorateClass([
407
- customElement("vio-product")
408
- ], VioProduct);
409
282
  var VioProductCarousel = class extends LitElement {
410
283
  constructor() {
411
284
  super(...arguments);
@@ -423,158 +296,23 @@ var VioProductCarousel = class extends LitElement {
423
296
  this.isLoading = false;
424
297
  this.fetchError = "";
425
298
  }
426
- connectedCallback() {
427
- super.connectedCallback();
428
- if (this.productRefs.trim() || this.productIds.trim()) {
429
- void this.autoFetch();
430
- }
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();
299
+ static {
300
+ this.styles = css`
301
+ :host {
302
+ display: block;
303
+ box-sizing: border-box;
304
+ width: 100%;
305
+ font-family: var(--vio-font-sans, -apple-system, sans-serif);
306
+ color: var(--vio-color-text, #0a0a0a);
307
+ padding: var(--vio-space-xl, 32px) 0;
436
308
  }
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);
309
+ .header {
310
+ display: flex;
311
+ align-items: flex-end;
312
+ justify-content: space-between;
313
+ gap: var(--vio-space-md, 16px);
314
+ margin-bottom: var(--vio-space-lg, 24px);
315
+ padding: 0 var(--vio-space-lg, 24px);
578
316
  }
579
317
  .header-text { flex: 1; min-width: 0; }
580
318
  .label {
@@ -702,445 +440,224 @@ VioProductCarousel.styles = css`
702
440
  50% { opacity: 0.5; }
703
441
  }
704
442
  `;
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
443
  }
787
444
  connectedCallback() {
788
445
  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;
446
+ if (this.productRefs.trim() || this.productIds.trim()) {
447
+ void this.autoFetch();
448
+ }
797
449
  }
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);
450
+ updated(changed) {
451
+ const propChanged = changed.has("productRefs") || changed.has("productIds") || changed.has("commerceKey") || changed.has("sponsorId");
452
+ if (propChanged && (this.productRefs.trim() || this.productIds.trim())) {
453
+ void this.autoFetch();
454
+ }
809
455
  }
810
- async fetchProduct() {
811
- const productIdNum = parseInt(this.productId, 10);
812
- if (Number.isNaN(productIdNum) || !this.sponsorId) {
813
- this.product = null;
456
+ async autoFetch() {
457
+ if (!Vio.isInitialized) {
458
+ this.fetchError = "Vio not initialized \u2014 call Vio.init({ apiKey }) first";
814
459
  return;
815
460
  }
816
461
  this.isLoading = true;
817
462
  this.fetchError = "";
818
463
  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
- });
464
+ 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 }));
465
+ if (refs.length === 0) {
466
+ this.fetched = [];
467
+ return;
468
+ }
469
+ const bySponsor = /* @__PURE__ */ new Map();
470
+ for (const r of refs) {
471
+ const arr = bySponsor.get(r.sponsorId) ?? [];
472
+ arr.push(r.productId);
473
+ bySponsor.set(r.sponsorId, arr);
474
+ }
475
+ const fetched = await Promise.all(
476
+ Array.from(bySponsor.entries()).map(async ([sponsorId, ids]) => {
477
+ const commerce = this.commerceKey && this.productRefs.trim() === "" ? Vio.commerceWithKey(this.commerceKey) : Vio.commerceFor(sponsorId);
478
+ const products = await commerce.channel.product.getByIds({
479
+ product_ids: ids,
480
+ currency: this.currency,
481
+ image_size: "large"
482
+ });
483
+ return { sponsorId, products };
484
+ })
485
+ );
486
+ const lookup = /* @__PURE__ */ new Map();
487
+ for (const { sponsorId, products } of fetched) {
488
+ for (const p of products) lookup.set(`${sponsorId}:${p.id}`, p);
840
489
  }
490
+ this.fetched = refs.map(({ sponsorId, productId }) => {
491
+ const product = lookup.get(`${sponsorId}:${productId}`);
492
+ return product ? { product, sponsorId } : null;
493
+ }).filter((x) => x !== null);
841
494
  } catch (err) {
842
495
  this.fetchError = err instanceof Error ? err.message : String(err);
843
496
  } finally {
844
497
  this.isLoading = false;
845
498
  }
846
499
  }
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;
500
+ parseRefs(raw) {
501
+ return raw.split(",").map((pair) => pair.trim()).filter((s) => s.length > 0).map((pair) => {
502
+ const [sidStr, pidStr] = pair.split(":");
503
+ const sid = parseInt((sidStr ?? "").trim(), 10);
504
+ const pid = parseInt((pidStr ?? "").trim(), 10);
505
+ if (Number.isNaN(sid) || Number.isNaN(pid)) return null;
506
+ return { sponsorId: sid, productId: pid };
507
+ }).filter((x) => x !== null);
908
508
  }
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
- }
509
+ scrollByDirection(direction) {
510
+ if (!this.scroller) return;
511
+ const item = this.scroller.querySelector(":scope > *");
512
+ const step = (item?.offsetWidth ?? 240) + 16;
513
+ this.scroller.scrollBy({ left: direction * step * 2, behavior: "smooth" });
930
514
  }
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;
515
+ renderScrollerContent() {
516
+ if (this.isLoading && !this.fetched) {
517
+ return html`${Array.from({ length: 5 }).map(
518
+ () => html`
519
+ <div class="skeleton">
520
+ <div class="skeleton-image"></div>
521
+ <div class="skeleton-text"></div>
522
+ <div class="skeleton-text shorter"></div>
523
+ </div>
524
+ `
525
+ )}`;
952
526
  }
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;
527
+ if (this.fetched && this.fetched.length > 0) {
528
+ return html`${this.fetched.map(({ product: p, sponsorId }) => {
529
+ const img = primaryImageUrl(p) ?? "";
530
+ const retailer = sponsorNameFromBootstrap(sponsorId) ?? p.brand ?? "";
531
+ return html`
532
+ <vio-product
533
+ product-id=${String(p.id)}
534
+ sponsor-id=${String(sponsorId)}
535
+ brand=${p.brand ?? ""}
536
+ name=${p.title}
537
+ price=${displayPrice(p)}
538
+ retailer=${retailer}
539
+ image-url=${img}
540
+ ?has-variants=${(p.variants?.length ?? 0) > 1}
541
+ ></vio-product>
542
+ `;
543
+ })}`;
970
544
  }
971
- void this.applePayHandle.show();
972
- this.close();
545
+ return html`<slot></slot>`;
973
546
  }
974
547
  render() {
975
548
  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>
549
+ <div class="header">
550
+ <div class="header-text">
551
+ ${this.label ? html`<div class="label">${this.label}</div>` : ""}
552
+ <div class="heading-row">
553
+ ${this.heading ? html`<h2 class="heading">${this.heading}</h2>` : ""}
554
+ ${this.disclaimer ? html`<span class="disclaimer">(${this.disclaimer})</span>` : ""}
555
+ </div>
982
556
  </div>
983
-
984
- ${this.renderBody()}
557
+ ${this.seeAllHref ? html`<a class="see-all" href=${this.seeAllHref}>${this.seeAllLabel}</a>` : ""}
558
+ </div>
559
+ <div class="viewport">
560
+ <button
561
+ class="nav-btn prev"
562
+ @click=${() => this.scrollByDirection(-1)}
563
+ aria-label="Forrige"
564
+ >
565
+ ‹
566
+ </button>
567
+ <div class="scroller">${this.renderScrollerContent()}</div>
568
+ <button
569
+ class="nav-btn next"
570
+ @click=${() => this.scrollByDirection(1)}
571
+ aria-label="Neste"
572
+ >
573
+ ›
574
+ </button>
985
575
  </div>
576
+ ${this.fetchError && !this.fetched ? html`<div class="state error">${this.fetchError}</div>` : ""}
986
577
  `;
987
578
  }
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
579
  };
1143
- VioProductDetail.styles = css`
580
+ __decorateClass([
581
+ property({ type: String })
582
+ ], VioProductCarousel.prototype, "label", 2);
583
+ __decorateClass([
584
+ property({ type: String })
585
+ ], VioProductCarousel.prototype, "heading", 2);
586
+ __decorateClass([
587
+ property({ type: String })
588
+ ], VioProductCarousel.prototype, "disclaimer", 2);
589
+ __decorateClass([
590
+ property({ type: String, attribute: "see-all-href" })
591
+ ], VioProductCarousel.prototype, "seeAllHref", 2);
592
+ __decorateClass([
593
+ property({ type: String, attribute: "see-all-label" })
594
+ ], VioProductCarousel.prototype, "seeAllLabel", 2);
595
+ __decorateClass([
596
+ property({ type: String, attribute: "product-ids" })
597
+ ], VioProductCarousel.prototype, "productIds", 2);
598
+ __decorateClass([
599
+ property({ type: String, attribute: "commerce-key" })
600
+ ], VioProductCarousel.prototype, "commerceKey", 2);
601
+ __decorateClass([
602
+ property({ type: Number, attribute: "sponsor-id" })
603
+ ], VioProductCarousel.prototype, "sponsorId", 2);
604
+ __decorateClass([
605
+ property({ type: String, attribute: "product-refs" })
606
+ ], VioProductCarousel.prototype, "productRefs", 2);
607
+ __decorateClass([
608
+ property({ type: String })
609
+ ], VioProductCarousel.prototype, "currency", 2);
610
+ __decorateClass([
611
+ query(".scroller")
612
+ ], VioProductCarousel.prototype, "scroller", 2);
613
+ __decorateClass([
614
+ state()
615
+ ], VioProductCarousel.prototype, "fetched", 2);
616
+ __decorateClass([
617
+ state()
618
+ ], VioProductCarousel.prototype, "isLoading", 2);
619
+ __decorateClass([
620
+ state()
621
+ ], VioProductCarousel.prototype, "fetchError", 2);
622
+ function sponsorNameFromBootstrap(sponsorId) {
623
+ const boot = Vio.bootstrapCache;
624
+ if (!boot) return void 0;
625
+ if (boot.primarySponsor?.id === sponsorId) return boot.primarySponsor.name;
626
+ return boot.secondarySponsors?.find((s) => s.id === sponsorId)?.name;
627
+ }
628
+ var VioProductDetail = class extends LitElement {
629
+ constructor() {
630
+ super(...arguments);
631
+ this.open = false;
632
+ this.productId = "";
633
+ this.sponsorId = 0;
634
+ this.autoOpen = true;
635
+ this.currency = "NOK";
636
+ this.product = null;
637
+ this.isLoading = false;
638
+ this.fetchError = "";
639
+ this.selectedOptions = {};
640
+ this.quantity = 1;
641
+ this.activeImageIndex = 0;
642
+ this.adding = false;
643
+ this.applePayOk = false;
644
+ /** Pre-prepared Apple Pay handle — show() fires synchronously on tap. */
645
+ this.applePayHandle = null;
646
+ this.boundProductClick = (e) => {
647
+ if (!this.autoOpen) return;
648
+ const ev = e;
649
+ const productIdNum = parseInt(ev.detail.productId, 10);
650
+ if (Number.isNaN(productIdNum)) return;
651
+ const sponsorIdNum = parseInt(ev.detail.sponsorId, 10);
652
+ if (Number.isNaN(sponsorIdNum) || sponsorIdNum <= 0) return;
653
+ this.productId = ev.detail.productId;
654
+ this.sponsorId = sponsorIdNum;
655
+ this.show();
656
+ void this.fetchProduct();
657
+ };
658
+ }
659
+ static {
660
+ this.styles = css`
1144
661
  :host { display: contents; }
1145
662
 
1146
663
  .backdrop {
@@ -1524,329 +1041,434 @@ VioProductDetail.styles = css`
1524
1041
  font-size: 14px;
1525
1042
  }
1526
1043
  `;
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
- }
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
1044
  }
1600
1045
  connectedCallback() {
1601
1046
  super.connectedCallback();
1602
- this.carts = Vio.cart.getAllCarts();
1603
- this.itemCount = Vio.cart.itemCount;
1604
- Vio.cart.addEventListener("change", this.boundOnCartChange);
1047
+ document.addEventListener("vio:product-click", this.boundProductClick);
1605
1048
  }
1606
1049
  disconnectedCallback() {
1607
- Vio.cart.removeEventListener("change", this.boundOnCartChange);
1050
+ document.removeEventListener("vio:product-click", this.boundProductClick);
1608
1051
  super.disconnectedCallback();
1609
1052
  }
1610
- close() {
1611
- this.open = false;
1612
- }
1613
1053
  show() {
1614
1054
  this.open = true;
1615
1055
  }
1616
- updated(changed) {
1617
- if (changed.has("open") && this.open) {
1618
- this.confirmedOrder = null;
1619
- this.preparedTotal = -1;
1620
- }
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);
1056
+ close() {
1057
+ this.open = false;
1058
+ setTimeout(() => {
1059
+ if (!this.open) {
1060
+ this.product = null;
1061
+ this.selectedOptions = {};
1062
+ this.quantity = 1;
1063
+ this.activeImageIndex = 0;
1064
+ this.fetchError = "";
1626
1065
  }
1627
- }
1066
+ }, 400);
1628
1067
  }
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;
1068
+ async fetchProduct() {
1069
+ const productIdNum = parseInt(this.productId, 10);
1070
+ if (Number.isNaN(productIdNum) || !this.sponsorId) {
1071
+ this.product = null;
1634
1072
  return;
1635
1073
  }
1074
+ this.isLoading = true;
1075
+ this.fetchError = "";
1636
1076
  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;
1077
+ const commerce = Vio.commerceFor(this.sponsorId);
1078
+ const products = await commerce.channel.product.getByIds({
1079
+ product_ids: [productIdNum],
1080
+ currency: this.currency,
1081
+ image_size: "large"
1082
+ });
1083
+ this.product = products[0] ?? null;
1084
+ if (this.product) {
1085
+ const initial = {};
1086
+ for (const opt of this.product.options) {
1087
+ const values = normalizeValues(opt.values);
1088
+ if (values.length > 0) initial[opt.name] = values[0];
1089
+ }
1090
+ this.selectedOptions = initial;
1091
+ void Vio.checkout.prepareApplePayFor(this.unitPrice * this.quantity, this.currency).then((handle) => {
1092
+ this.applePayHandle = handle;
1093
+ this.applePayOk = handle.available;
1094
+ }).catch(() => {
1095
+ this.applePayHandle = null;
1096
+ this.applePayOk = false;
1097
+ });
1098
+ }
1099
+ } catch (err) {
1100
+ this.fetchError = err instanceof Error ? err.message : String(err);
1101
+ } finally {
1102
+ this.isLoading = false;
1644
1103
  }
1645
1104
  }
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);
1656
- }
1657
- this.dispatchEvent(
1658
- new CustomEvent("vio:payment-success", {
1659
- bubbles: true,
1660
- composed: true,
1661
- detail: { method: "apple-pay", result }
1662
- })
1105
+ /** Resolve the active variant from the selected option values. */
1106
+ selectedVariant() {
1107
+ if (!this.product || this.product.variants.length === 0) return null;
1108
+ const vals = Object.values(this.selectedOptions);
1109
+ if (vals.length === 0) return this.product.variants[0] ?? null;
1110
+ const found = this.product.variants.find((v) => {
1111
+ if (!v.title) return false;
1112
+ return vals.every(
1113
+ (val) => v.title.toLowerCase().includes(val.toLowerCase())
1663
1114
  );
1664
1115
  });
1116
+ return found ?? this.product.variants[0] ?? null;
1665
1117
  }
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
- }
1678
- }
1679
- return { items, total: this.totalAcrossSponsors, currency: this.primaryCurrency };
1118
+ get unitPrice() {
1119
+ const v = this.selectedVariant();
1120
+ const price = v?.price ?? this.product?.price;
1121
+ return price?.amount_incl_taxes ?? price?.amount ?? 0;
1680
1122
  }
1681
- closeConfirmation() {
1682
- this.confirmedOrder = null;
1683
- this.close();
1123
+ get compareAt() {
1124
+ const v = this.selectedVariant();
1125
+ const price = v?.price ?? this.product?.price;
1126
+ return price?.compare_at_incl_taxes ?? price?.compare_at ?? null;
1684
1127
  }
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
- this.dispatchEvent(
1694
- new CustomEvent("vio:checkout-open", {
1695
- bubbles: true,
1696
- composed: true,
1697
- detail: { sponsorId: firstSponsorId, paymentMethod: "klarna", express: true }
1698
- })
1699
- );
1128
+ get availableQuantity() {
1129
+ const v = this.selectedVariant();
1130
+ if (v) return v.quantity ?? 0;
1131
+ return this.product?.quantity ?? 0;
1700
1132
  }
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;
1133
+ stockLabel() {
1134
+ const qty = this.availableQuantity;
1135
+ if (qty <= 0) return { text: "Utsolgt", cls: "out-of-stock" };
1136
+ if (qty <= 3) return { text: `F\xE5 igjen \u2014 ${qty} stk`, cls: "low-stock" };
1137
+ return { text: "P\xE5 lager", cls: "in-stock" };
1707
1138
  }
1708
- get primaryCurrency() {
1709
- for (const cart of this.carts.values()) return cart.currency;
1710
- return "NOK";
1139
+ selectOption(optionName, value) {
1140
+ this.selectedOptions = { ...this.selectedOptions, [optionName]: value };
1711
1141
  }
1712
- onIncrement(item) {
1713
- Vio.cart.updateQuantity(item.id, item.sponsorId, item.quantity + 1);
1142
+ allImages() {
1143
+ if (!this.product) return [];
1144
+ const variantImages = this.selectedVariant()?.images ?? [];
1145
+ if (variantImages.length > 0) return variantImages;
1146
+ return this.product.images;
1714
1147
  }
1715
- onDecrement(item) {
1716
- Vio.cart.updateQuantity(item.id, item.sponsorId, item.quantity - 1);
1148
+ get activeImage() {
1149
+ return this.allImages()[this.activeImageIndex];
1717
1150
  }
1718
- onRemove(item) {
1719
- Vio.cart.removeItem(item.id, item.sponsorId);
1151
+ /** Add the current product + selected variant + qty to the cart. Returns the variant id used. */
1152
+ addCurrentToCart() {
1153
+ if (!this.product) return void 0;
1154
+ const variant = this.selectedVariant();
1155
+ const sponsor = Vio.bootstrapCache?.primarySponsor;
1156
+ const secondaries = Vio.bootstrapCache?.secondarySponsors ?? [];
1157
+ const sponsorName = sponsor?.id === this.sponsorId ? sponsor.name : secondaries.find((s) => s.id === this.sponsorId)?.name;
1158
+ Vio.cart.addProduct({
1159
+ product: this.product,
1160
+ sponsorId: this.sponsorId,
1161
+ sponsorName,
1162
+ quantity: this.quantity,
1163
+ variantId: variant?.id
1164
+ });
1165
+ return variant?.id;
1720
1166
  }
1721
- render() {
1722
- const isEmpty = this.itemCount === 0;
1723
- const sponsorCarts = [...this.carts.values()].filter((c) => c.items.length > 0);
1724
- return html`
1725
- <div class="backdrop" @click=${this.close}></div>
1726
- <aside class="drawer" role="dialog" aria-label=${this.heading}>
1727
- <div class="handle" @click=${this.close} aria-hidden="true"></div>
1728
- <div class="header">
1729
- <h2 class="heading">${this.confirmedOrder ? "Takk!" : this.heading}</h2>
1730
- <button class="close" @click=${this.close} aria-label="Lukk">×</button>
1731
- </div>
1732
-
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`
1167
+ async addToCart() {
1168
+ if (!this.product || this.availableQuantity <= 0 || this.adding) return;
1169
+ this.adding = true;
1170
+ try {
1171
+ const variantId = this.addCurrentToCart();
1172
+ this.dispatchEvent(
1173
+ new CustomEvent("vio:added-to-cart", {
1174
+ bubbles: true,
1175
+ composed: true,
1176
+ detail: {
1177
+ productId: this.product.id,
1178
+ sponsorId: this.sponsorId,
1179
+ quantity: this.quantity,
1180
+ variantId
1181
+ }
1182
+ })
1183
+ );
1184
+ this.close();
1185
+ } finally {
1186
+ this.adding = false;
1187
+ }
1188
+ }
1189
+ /**
1190
+ * Express buy: add to cart, then open the checkout overlay with Klarna
1191
+ * preselected. The host app handles `vio:checkout-open` (same event the cart
1192
+ * uses) — the `paymentMethod` hint tells it to preselect Klarna so its widget
1193
+ * loads immediately. Reuses the working Klarna Payments flow (no express SDK).
1194
+ */
1195
+ async buyWithKlarna() {
1196
+ if (!this.product || this.availableQuantity <= 0 || this.adding) return;
1197
+ this.adding = true;
1198
+ try {
1199
+ this.addCurrentToCart();
1200
+ this.close();
1201
+ this.dispatchEvent(
1202
+ new CustomEvent("vio:checkout-open", {
1203
+ bubbles: true,
1204
+ composed: true,
1205
+ detail: { sponsorId: this.sponsorId, paymentMethod: "klarna", express: true }
1206
+ })
1207
+ );
1208
+ } finally {
1209
+ this.adding = false;
1210
+ }
1211
+ }
1212
+ /**
1213
+ * Express buy with Apple Pay. Runs synchronously inside the click so the
1214
+ * Apple Pay sheet keeps the user gesture: add to cart → open checkout (sets
1215
+ * state) → launch the sheet. On authorize, `payment-complete` drives the
1216
+ * confirmation drawer (same path as the in-checkout Apple Pay button).
1217
+ */
1218
+ buyWithApplePay() {
1219
+ if (!this.product || this.availableQuantity <= 0 || !this.applePayHandle) return;
1220
+ this.addCurrentToCart();
1221
+ try {
1222
+ Vio.checkout.open(this.sponsorId);
1223
+ } catch (err) {
1224
+ if (typeof console !== "undefined") {
1225
+ console.warn("[VioProductDetail] checkout open failed:", err);
1226
+ }
1227
+ return;
1228
+ }
1229
+ void this.applePayHandle.show();
1230
+ this.close();
1231
+ }
1232
+ render() {
1233
+ return html`
1234
+ <div class="backdrop" @click=${this.close}></div>
1235
+ <div class="modal" role="dialog" aria-label="Produkt">
1236
+ <div class="handle" @click=${this.close} aria-hidden="true"></div>
1237
+ <div class="topbar">
1238
+ <span class="topbar-brand">${this.product?.brand ?? ""}</span>
1239
+ <button class="close" @click=${this.close} aria-label="Lukk">×</button>
1240
+ </div>
1241
+
1242
+ ${this.renderBody()}
1243
+ </div>
1244
+ `;
1245
+ }
1246
+ renderBody() {
1247
+ if (this.isLoading && !this.product) {
1248
+ return html`
1249
+ <div class="skeleton" role="status" aria-busy="true" aria-label="Laster produkt">
1250
+ <div class="sk-image"></div>
1251
+ <div class="sk-line sk-brand"></div>
1252
+ <div class="sk-line sk-title"></div>
1253
+ <div class="sk-line sk-title-2"></div>
1254
+ <div class="sk-line sk-price"></div>
1255
+ <div class="sk-button"></div>
1256
+ </div>
1257
+ `;
1258
+ }
1259
+ if (this.fetchError) {
1260
+ return html`<div class="error">Kunne ikke laste produkt: ${this.fetchError}</div>`;
1261
+ }
1262
+ if (!this.product) return html``;
1263
+ const p = this.product;
1264
+ const stock = this.stockLabel();
1265
+ const compare = this.compareAt;
1266
+ const showCompare = compare !== null && compare > this.unitPrice;
1267
+ const images = this.allImages();
1268
+ const activeImage = this.activeImage;
1269
+ return html`
1270
+ <div class="body">
1271
+ <div class="gallery">
1272
+ <div class="gallery-main">
1273
+ ${activeImage?.url ? html`<img src=${activeImage.url} alt=${p.title} />` : ""}
1274
+ </div>
1275
+ ${images.length > 1 ? html`
1276
+ <div class="gallery-thumbs">
1277
+ ${images.map(
1278
+ (img, idx) => html`
1797
1279
  <button
1798
- class="applepay-btn"
1799
- @click=${this.onApplePay}
1800
- aria-label="Betal med Apple Pay"
1280
+ class="gallery-thumb ${idx === this.activeImageIndex ? "active" : ""}"
1281
+ @click=${() => this.activeImageIndex = idx}
1282
+ aria-label=${`Bilde ${idx + 1}`}
1801
1283
  >
1802
- <span class="ap-logo">${"\uF8FF"}</span> Pay
1284
+ <img src=${img.url} alt="" />
1803
1285
  </button>
1804
- ` : ""}
1286
+ `
1287
+ )}
1288
+ </div>
1289
+ ` : ""}
1290
+ </div>
1291
+
1292
+ <div class="info">
1293
+ ${p.brand ? html`<div class="brand">${p.brand}</div>` : ""}
1294
+ <h2 class="name">${p.title}</h2>
1295
+
1296
+ <div class="price-row">
1297
+ <span class="price">${formatPrice(this.unitPrice, p.price.currency_code)}</span>
1298
+ ${showCompare ? html`<span class="compare-at">${formatPrice(
1299
+ compare,
1300
+ p.price.currency_code
1301
+ )}</span>` : ""}
1302
+ </div>
1303
+
1304
+ <div class="stock ${stock.cls}">${stock.text}</div>
1305
+
1306
+ ${this.renderOptionPickers(p.options)}
1307
+
1308
+ <div class="qty-cta-row">
1309
+ <div class="qty-stepper">
1310
+ <button
1311
+ class="qty-btn"
1312
+ @click=${() => this.quantity = Math.max(1, this.quantity - 1)}
1313
+ aria-label="Reduser antall"
1314
+ >
1315
+ −
1316
+ </button>
1317
+ <span class="qty-value">${this.quantity}</span>
1318
+ <button
1319
+ class="qty-btn"
1320
+ @click=${() => this.quantity = Math.min(this.availableQuantity, this.quantity + 1)}
1321
+ aria-label="Øk antall"
1322
+ ?disabled=${this.quantity >= this.availableQuantity}
1323
+ >
1324
+ +
1325
+ </button>
1326
+ </div>
1327
+ <button
1328
+ class="add-to-cart"
1329
+ @click=${this.addToCart}
1330
+ ?disabled=${this.availableQuantity <= 0 || this.adding}
1331
+ >
1332
+ ${this.adding ? "Legger til\u2026" : this.availableQuantity <= 0 ? "Utsolgt" : "Legg i handlekurv"}
1333
+ </button>
1334
+ </div>
1335
+
1336
+ ${this.availableQuantity > 0 && this.applePayOk ? html`
1805
1337
  <button
1806
- class="klarna-btn"
1807
- @click=${this.onKlarnaExpress}
1808
- aria-label="Betal med Klarna"
1338
+ class="buy-applepay"
1339
+ @click=${this.buyWithApplePay}
1340
+ ?disabled=${this.adding}
1341
+ aria-label="Kjøp nå med Apple Pay"
1809
1342
  >
1810
- Kjøp med
1343
+ Kjøp nå med Pay
1344
+ </button>
1345
+ ` : ""}
1346
+ ${this.availableQuantity > 0 ? html`
1347
+ <button
1348
+ class="buy-klarna"
1349
+ @click=${this.buyWithKlarna}
1350
+ ?disabled=${this.adding}
1351
+ >
1352
+ <span>Kjøp nå med</span>
1811
1353
  <img
1812
- class="klarna-logo"
1354
+ class="klarna-badge"
1813
1355
  src="https://x.klarnacdn.net/payment-method/assets/badges/generic/klarna.svg"
1814
1356
  alt="Klarna"
1815
1357
  />
1816
1358
  </button>
1817
- </div>
1818
- `}
1819
- </aside>
1359
+ ` : ""}
1360
+
1361
+ ${p.description ? html`
1362
+ <div>
1363
+ <div class="description-label">Beskrivelse</div>
1364
+ <div class="description">${this.stripHtml(p.description)}</div>
1365
+ </div>
1366
+ ` : ""}
1367
+ </div>
1368
+ </div>
1820
1369
  `;
1821
1370
  }
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>
1371
+ renderOptionPickers(options) {
1372
+ if (options.length === 0) return "";
1373
+ return html`${options.map((opt) => {
1374
+ const values = normalizeValues(opt.values);
1375
+ if (values.length === 0) return "";
1376
+ return html`
1377
+ <div class="option-group">
1378
+ <div class="option-label">${opt.name}</div>
1379
+ <div class="option-values">
1380
+ ${values.map(
1381
+ (val) => html`
1382
+ <button
1383
+ class="option-pill"
1384
+ @click=${() => this.selectOption(opt.name, val)}
1385
+ aria-pressed=${this.selectedOptions[opt.name] === val}
1386
+ >
1387
+ ${val}
1388
+ </button>
1389
+ `
1390
+ )}
1842
1391
  </div>
1843
1392
  </div>
1844
- <button class="confirm-close" @click=${this.closeConfirmation}>Lukk</button>
1845
- </div>
1846
- `;
1393
+ `;
1394
+ })}`;
1395
+ }
1396
+ /** Best-effort HTML strip for description. */
1397
+ stripHtml(html6) {
1398
+ 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
1399
  }
1848
1400
  };
1849
- VioCart.styles = css`
1401
+ __decorateClass([
1402
+ property({ type: Boolean, reflect: true })
1403
+ ], VioProductDetail.prototype, "open", 2);
1404
+ __decorateClass([
1405
+ property({ type: String, attribute: "product-id" })
1406
+ ], VioProductDetail.prototype, "productId", 2);
1407
+ __decorateClass([
1408
+ property({ type: Number, attribute: "sponsor-id" })
1409
+ ], VioProductDetail.prototype, "sponsorId", 2);
1410
+ __decorateClass([
1411
+ property({ type: Boolean, attribute: "auto-open" })
1412
+ ], VioProductDetail.prototype, "autoOpen", 2);
1413
+ __decorateClass([
1414
+ property({ type: String })
1415
+ ], VioProductDetail.prototype, "currency", 2);
1416
+ __decorateClass([
1417
+ state()
1418
+ ], VioProductDetail.prototype, "product", 2);
1419
+ __decorateClass([
1420
+ state()
1421
+ ], VioProductDetail.prototype, "isLoading", 2);
1422
+ __decorateClass([
1423
+ state()
1424
+ ], VioProductDetail.prototype, "fetchError", 2);
1425
+ __decorateClass([
1426
+ state()
1427
+ ], VioProductDetail.prototype, "selectedOptions", 2);
1428
+ __decorateClass([
1429
+ state()
1430
+ ], VioProductDetail.prototype, "quantity", 2);
1431
+ __decorateClass([
1432
+ state()
1433
+ ], VioProductDetail.prototype, "activeImageIndex", 2);
1434
+ __decorateClass([
1435
+ state()
1436
+ ], VioProductDetail.prototype, "adding", 2);
1437
+ __decorateClass([
1438
+ state()
1439
+ ], VioProductDetail.prototype, "applePayOk", 2);
1440
+ function normalizeValues(raw) {
1441
+ if (Array.isArray(raw)) {
1442
+ return raw.map((v) => String(v).trim()).filter((v) => v.length > 0);
1443
+ }
1444
+ if (typeof raw === "string") {
1445
+ return raw.split(",").map((v) => v.trim()).filter((v) => v.length > 0);
1446
+ }
1447
+ return [];
1448
+ }
1449
+ var VioCart = class extends LitElement {
1450
+ constructor() {
1451
+ super(...arguments);
1452
+ this.open = false;
1453
+ this.heading = "Handlekurv";
1454
+ this.emptyLabel = "Handlekurven er tom";
1455
+ this.subtotalLabel = "Sum";
1456
+ this.carts = /* @__PURE__ */ new Map();
1457
+ this.itemCount = 0;
1458
+ this.applePayAvailable = false;
1459
+ this.confirmedOrder = null;
1460
+ /** Pre-resolved Apple Pay handle; `show()` fires the sheet synchronously. */
1461
+ this.applePayHandle = null;
1462
+ /** Cart total Apple Pay was last prepared for (avoids redundant prep). */
1463
+ this.preparedTotal = -1;
1464
+ this.boundOnCartChange = (e) => {
1465
+ const detail = e.detail;
1466
+ this.carts = detail.cartsBySponsor;
1467
+ this.itemCount = detail.itemCount;
1468
+ };
1469
+ }
1470
+ static {
1471
+ this.styles = css`
1850
1472
  :host { display: contents; }
1851
1473
 
1852
1474
  .backdrop {
@@ -2029,27 +1651,7 @@ VioCart.styles = css`
2029
1651
  .subtotal-label { color: var(--vio-color-text-secondary, #666); }
2030
1652
  .subtotal-value { font-weight: 700; font-size: 16px; }
2031
1653
 
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". */
1654
+ /* Apple Pay express — pay the whole cart directly. */
2053
1655
  .applepay-btn {
2054
1656
  width: 100%;
2055
1657
  padding: 15px;
@@ -2092,23 +1694,6 @@ VioCart.styles = css`
2092
1694
  }
2093
1695
  .klarna-btn:hover { background: #ffa1ba; }
2094
1696
  .klarna-btn .klarna-logo { height: 15px; width: auto; display: inline-block; }
2095
- .express-divider {
2096
- display: flex;
2097
- 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;
2104
- }
2105
- .express-divider::before,
2106
- .express-divider::after {
2107
- content: '';
2108
- flex: 1;
2109
- height: 1px;
2110
- background: var(--vio-color-border, #e5e5e5);
2111
- }
2112
1697
 
2113
1698
  /* In-drawer confirmation after Apple Pay (nothing else is shown). */
2114
1699
  .confirm {
@@ -2177,601 +1762,346 @@ VioCart.styles = css`
2177
1762
  }
2178
1763
  .confirm-close:hover { background: #222; }
2179
1764
  `;
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
1765
  }
2274
1766
  connectedCallback() {
2275
1767
  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
- }
1768
+ this.carts = Vio.cart.getAllCarts();
1769
+ this.itemCount = Vio.cart.itemCount;
1770
+ Vio.cart.addEventListener("change", this.boundOnCartChange);
2285
1771
  }
2286
1772
  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();
1773
+ Vio.cart.removeEventListener("change", this.boundOnCartChange);
2291
1774
  super.disconnectedCallback();
2292
1775
  }
2293
- updated() {
2294
- void this.mountKlarnaIfNeeded();
2295
- }
2296
1776
  close() {
2297
1777
  this.open = false;
2298
- this.express = false;
2299
- Vio.checkout.close();
2300
1778
  }
2301
1779
  show() {
2302
1780
  this.open = true;
2303
1781
  }
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;
1782
+ updated(changed) {
1783
+ if (changed.has("open") && this.open) {
1784
+ this.confirmedOrder = null;
1785
+ this.preparedTotal = -1;
2313
1786
  }
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) {
2322
- return;
2323
- }
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
- 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;
2341
- } catch (err) {
2342
- if (typeof console !== "undefined") {
2343
- console.warn("[VioCheckout] Klarna Payments mount failed:", err);
1787
+ if (this.open && this.itemCount > 0) {
1788
+ const total = this.totalAcrossSponsors;
1789
+ if (total !== this.preparedTotal) {
1790
+ this.preparedTotal = total;
1791
+ void this.refreshApplePay(total);
2344
1792
  }
2345
- this.paymentError = `Kunne ikke laste Klarna: ${err instanceof Error ? err.message : String(err)}`;
2346
- } finally {
2347
- this.klarnaMounting = false;
2348
- }
2349
- }
2350
- unmountKlarna() {
2351
- if (this.klarnaHandle) {
2352
- this.klarnaHandle.unmount();
2353
- this.klarnaHandle = null;
2354
1793
  }
2355
- this.klarnaMountedAmount = null;
2356
- this.klarnaMountedShipping = null;
2357
- this.klarnaCategories = [];
2358
- this.klarnaSelectedCat = "";
2359
- this.klarnaAuthorizing = false;
2360
- }
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;
2369
1794
  }
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);
2379
- }
1795
+ /** Pre-resolve the Apple Pay sheet for the current cart total. */
1796
+ async refreshApplePay(total) {
1797
+ if (total <= 0) {
1798
+ this.applePayHandle = null;
1799
+ this.applePayAvailable = false;
1800
+ return;
2380
1801
  }
2381
- }
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;
2387
1802
  try {
2388
- await this.klarnaHandle.authorize();
2389
- } finally {
2390
- this.klarnaAuthorizing = false;
1803
+ const handle = await Vio.checkout.prepareApplePayExpress(total, this.primaryCurrency);
1804
+ if (this.preparedTotal !== total) return;
1805
+ this.applePayHandle = handle;
1806
+ this.applePayAvailable = handle.available;
1807
+ } catch {
1808
+ this.applePayHandle = null;
1809
+ this.applePayAvailable = false;
2391
1810
  }
2392
1811
  }
2393
- onFieldChange(field, value) {
2394
- this.form = { ...this.form, [field]: value };
2395
- Vio.checkout.setAddress(this.form);
2396
- }
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);
1812
+ /** Apple Pay express — pay the whole cart, then show the confirmation. */
1813
+ onApplePay() {
1814
+ const handle = this.applePayHandle;
1815
+ if (!handle || !handle.available || this.itemCount === 0) return;
1816
+ const snapshot = this.buildConfirmation();
1817
+ void handle.show().then((result) => {
1818
+ if (!result) return;
1819
+ this.confirmedOrder = snapshot;
1820
+ for (const sponsorId of [...this.carts.keys()]) {
1821
+ Vio.cart.clearSponsorCart(sponsorId);
2405
1822
  }
2406
- } finally {
2407
- this.applePayInProgress = false;
2408
- }
1823
+ this.dispatchEvent(
1824
+ new CustomEvent("vio:payment-success", {
1825
+ bubbles: true,
1826
+ composed: true,
1827
+ detail: { method: "apple-pay", result }
1828
+ })
1829
+ );
1830
+ });
2409
1831
  }
2410
- onPay(method) {
2411
- this.paymentError = null;
2412
- if (method === "apple-pay") {
2413
- void this.onApplePay();
2414
- return;
1832
+ buildConfirmation() {
1833
+ const items = [];
1834
+ for (const cart of this.carts.values()) {
1835
+ for (const it of cart.items) {
1836
+ items.push({
1837
+ brand: it.brand,
1838
+ name: it.name,
1839
+ quantity: it.quantity,
1840
+ unitPrice: it.unitPrice,
1841
+ currency: it.currency
1842
+ });
1843
+ }
2415
1844
  }
2416
- Vio.checkout.selectPaymentMethod(method);
1845
+ return { items, total: this.totalAcrossSponsors, currency: this.primaryCurrency };
2417
1846
  }
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);
1847
+ closeConfirmation() {
1848
+ this.confirmedOrder = null;
1849
+ this.close();
2429
1850
  }
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;
1851
+ /** Klarna express — opens the side-drawer Klarna checkout for the whole cart,
1852
+ * skipping the address form (leveringsadresse) and method picker
1853
+ * (betalingsmetode). The host's checkout-open handler closes this cart and
1854
+ * opens <vio-checkout> in express mode with Klarna preselected. */
1855
+ onKlarnaExpress() {
1856
+ if (this.itemCount === 0) return;
1857
+ const firstSponsorId = [...this.carts.keys()][0];
1858
+ if (firstSponsorId === void 0) return;
2447
1859
  this.dispatchEvent(
2448
- new CustomEvent("vio:payment-success", {
1860
+ new CustomEvent("vio:checkout-open", {
2449
1861
  bubbles: true,
2450
1862
  composed: true,
2451
- detail: { method, sponsorId, result }
1863
+ detail: { sponsorId: firstSponsorId, paymentMethod: "klarna", express: true }
2452
1864
  })
2453
1865
  );
2454
- Vio.cart.clearSponsorCart(sponsorId);
2455
1866
  }
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 "";
1867
+ get totalAcrossSponsors() {
1868
+ let sum = 0;
1869
+ for (const cart of this.carts.values()) {
1870
+ for (const item of cart.items) sum += item.unitPrice * item.quantity;
2468
1871
  }
1872
+ return sum;
2469
1873
  }
2470
- /** True for methods that complete via a separate sheet/widget, not the CTA. */
2471
- isExpressMethod(method) {
2472
- return method === "apple-pay" || method === "klarna";
1874
+ get primaryCurrency() {
1875
+ for (const cart of this.carts.values()) return cart.currency;
1876
+ return "NOK";
2473
1877
  }
2474
- orderTotal() {
2475
- const s = this.checkoutState;
2476
- if (!s) return "";
2477
- return formatPrice(s.subtotal, s.currency);
1878
+ onIncrement(item) {
1879
+ Vio.cart.updateQuantity(item.id, item.sponsorId, item.quantity + 1);
2478
1880
  }
2479
- /** Shipping cost (major units) applied in express mode, else 0. */
2480
- shippingMajor() {
2481
- return this.express ? (this.shippingOption?.price ?? 0) / 100 : 0;
1881
+ onDecrement(item) {
1882
+ Vio.cart.updateQuantity(item.id, item.sponsorId, item.quantity - 1);
2482
1883
  }
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);
1884
+ onRemove(item) {
1885
+ Vio.cart.removeItem(item.id, item.sponsorId);
2488
1886
  }
2489
1887
  render() {
1888
+ const isEmpty = this.itemCount === 0;
1889
+ const sponsorCarts = [...this.carts.values()].filter((c) => c.items.length > 0);
2490
1890
  return html`
2491
1891
  <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
- >
1892
+ <aside class="drawer" role="dialog" aria-label=${this.heading}>
2497
1893
  <div class="handle" @click=${this.close} aria-hidden="true"></div>
2498
- <div class="topbar">
2499
- <h2 class="heading">${this.orderConfirmed ? "Takk!" : this.heading}</h2>
1894
+ <div class="header">
1895
+ <h2 class="heading">${this.confirmedOrder ? "Takk!" : this.heading}</h2>
2500
1896
  <button class="close" @click=${this.close} aria-label="Lukk">×</button>
2501
1897
  </div>
2502
1898
 
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>
1899
+ ${this.confirmedOrder ? this.renderConfirmation() : isEmpty ? html`<div class="empty">${this.emptyLabel}</div>` : html`
1900
+ <div class="body">
1901
+ ${sponsorCarts.map(
1902
+ (cart) => html`
1903
+ <div class="sponsor-group">
1904
+ ${cart.sponsorName ? html`<div class="sponsor-label">${cart.sponsorName}</div>` : ""}
1905
+ ${cart.items.map(
1906
+ (item) => html`
1907
+ <div class="item">
1908
+ <div class="item-image">
1909
+ ${item.imageUrl ? html`<img src=${item.imageUrl} alt=${item.name} />` : ""}
1910
+ </div>
1911
+ <div>
1912
+ <div class="item-brand">${item.brand}</div>
1913
+ <div class="item-name">${item.name}</div>
1914
+ <div class="item-row">
1915
+ <span class="item-qty">
1916
+ <button
1917
+ class="qty-btn"
1918
+ @click=${() => this.onDecrement(item)}
1919
+ aria-label="Reduser"
1920
+ >
1921
+ −
1922
+ </button>
1923
+ ${item.quantity}
1924
+ <button
1925
+ class="qty-btn"
1926
+ @click=${() => this.onIncrement(item)}
1927
+ aria-label="Øk"
1928
+ >
1929
+ +
1930
+ </button>
1931
+ <button
1932
+ class="qty-btn"
1933
+ @click=${() => this.onRemove(item)}
1934
+ aria-label="Fjern"
1935
+ style="margin-left: 8px;"
1936
+ >
1937
+ ×
1938
+ </button>
1939
+ </span>
1940
+ <span class="item-price">
1941
+ ${formatPrice(
1942
+ item.unitPrice * item.quantity,
1943
+ item.currency
1944
+ )}
1945
+ </span>
1946
+ </div>
1947
+ </div>
1948
+ </div>
1949
+ `
1950
+ )}
2526
1951
  </div>
2527
1952
  `
2528
1953
  )}
2529
- <div class="order-row total">
2530
- <span>Totalt</span><span>${formatPrice(o.total, o.currency)}</span>
1954
+ </div>
1955
+ <div class="footer">
1956
+ <div class="subtotal-row">
1957
+ <span class="subtotal-label">${this.subtotalLabel}</span>
1958
+ <span class="subtotal-value">
1959
+ ${formatPrice(this.totalAcrossSponsors, this.primaryCurrency)}
1960
+ </span>
2531
1961
  </div>
1962
+ ${this.applePayAvailable ? html`
1963
+ <button
1964
+ class="applepay-btn"
1965
+ @click=${this.onApplePay}
1966
+ aria-label="Betal med Apple Pay"
1967
+ >
1968
+ <span class="ap-logo">${"\uF8FF"}</span> Pay
1969
+ </button>
1970
+ ` : ""}
1971
+ <button
1972
+ class="klarna-btn"
1973
+ @click=${this.onKlarnaExpress}
1974
+ aria-label="Betal med Klarna"
1975
+ >
1976
+ Kjøp med
1977
+ <img
1978
+ class="klarna-logo"
1979
+ src="https://x.klarnacdn.net/payment-method/assets/badges/generic/klarna.svg"
1980
+ alt="Klarna"
1981
+ />
1982
+ </button>
2532
1983
  </div>
2533
- ` : ""}
2534
- <button class="confirm-close" @click=${this.close}>Lukk</button>
2535
- </section>
1984
+ `}
1985
+ </aside>
2536
1986
  `;
2537
1987
  }
2538
- /** Klarna Payments widget panel: shipping + category chips + widget + pay button. */
2539
- renderKlarnaPanel() {
2540
- const currency = this.checkoutState?.currency ?? "NOK";
1988
+ renderConfirmation() {
1989
+ const o = this.confirmedOrder;
1990
+ if (!o) return html``;
2541
1991
  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
- )}
1992
+ <div class="confirm">
1993
+ <div class="confirm-check" aria-hidden="true">✓</div>
1994
+ <h3>Takk for handelen!</h3>
1995
+ <p>Betaling med Apple Pay er bekreftet.</p>
1996
+ <div class="confirm-summary">
1997
+ ${o.items.map(
1998
+ (it) => html`
1999
+ <div class="order-row">
2000
+ <span>${it.brand ? `${it.brand} ` : ""}${it.name} ×${it.quantity}</span>
2001
+ <span>${formatPrice(it.unitPrice * it.quantity, it.currency)}</span>
2561
2002
  </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
- `
2003
+ `
2574
2004
  )}
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>
2005
+ <div class="order-row total">
2006
+ <span>Totalt</span>
2007
+ <span>${formatPrice(o.total, o.currency)}</span>
2008
+ </div>
2009
+ </div>
2010
+ <button class="confirm-close" @click=${this.closeConfirmation}>Lukk</button>
2770
2011
  </div>
2771
2012
  `;
2772
2013
  }
2773
2014
  };
2774
- VioCheckout.styles = css`
2015
+ __decorateClass([
2016
+ property({ type: Boolean, reflect: true })
2017
+ ], VioCart.prototype, "open", 2);
2018
+ __decorateClass([
2019
+ property({ type: String })
2020
+ ], VioCart.prototype, "heading", 2);
2021
+ __decorateClass([
2022
+ property({ type: String, attribute: "empty-label" })
2023
+ ], VioCart.prototype, "emptyLabel", 2);
2024
+ __decorateClass([
2025
+ property({ type: String, attribute: "subtotal-label" })
2026
+ ], VioCart.prototype, "subtotalLabel", 2);
2027
+ __decorateClass([
2028
+ state()
2029
+ ], VioCart.prototype, "carts", 2);
2030
+ __decorateClass([
2031
+ state()
2032
+ ], VioCart.prototype, "itemCount", 2);
2033
+ __decorateClass([
2034
+ state()
2035
+ ], VioCart.prototype, "applePayAvailable", 2);
2036
+ __decorateClass([
2037
+ state()
2038
+ ], VioCart.prototype, "confirmedOrder", 2);
2039
+ var VioCheckout = class extends LitElement {
2040
+ constructor() {
2041
+ super(...arguments);
2042
+ this.open = false;
2043
+ this.heading = "Kasse";
2044
+ this.shippingLabel = "Beregnes ved kasse";
2045
+ this.express = false;
2046
+ this.checkoutState = null;
2047
+ this.items = [];
2048
+ this.applePayAvailable = false;
2049
+ this.applePayInProgress = false;
2050
+ this.klarnaAvailable = false;
2051
+ this.orderConfirmed = false;
2052
+ this.confirmedMethod = null;
2053
+ this.paymentError = null;
2054
+ this.confirmedOrder = null;
2055
+ /** Active Klarna Payments widget handle + the subtotal it was mounted for. */
2056
+ this.klarnaHandle = null;
2057
+ this.klarnaMountedAmount = null;
2058
+ this.klarnaMountedShipping = null;
2059
+ this.klarnaMounting = false;
2060
+ this.klarnaCategories = [];
2061
+ this.klarnaSelectedCat = "";
2062
+ this.klarnaAuthorizing = false;
2063
+ this.selectedShipping = KLARNA_SHIPPING_OPTIONS.find((o) => o.preselected)?.id ?? KLARNA_SHIPPING_OPTIONS[0]?.id ?? "";
2064
+ this.form = {
2065
+ firstName: "",
2066
+ lastName: "",
2067
+ email: "",
2068
+ address: "",
2069
+ postalCode: "",
2070
+ city: ""
2071
+ };
2072
+ this.boundOnCheckoutChange = (e) => {
2073
+ const detail = e.detail;
2074
+ this.checkoutState = detail.state;
2075
+ this.items = Vio.checkout.items;
2076
+ if (this.checkoutState) {
2077
+ this.refreshApplePay();
2078
+ this.refreshKlarna();
2079
+ } else {
2080
+ this.applePayAvailable = false;
2081
+ this.klarnaAvailable = false;
2082
+ this.orderConfirmed = false;
2083
+ this.confirmedMethod = null;
2084
+ this.confirmedOrder = null;
2085
+ this.paymentError = null;
2086
+ this.express = false;
2087
+ this.unmountKlarna();
2088
+ }
2089
+ };
2090
+ this.boundOnPaymentComplete = (e) => {
2091
+ const detail = e.detail;
2092
+ if (detail.method !== "klarna" && detail.method !== "apple-pay") return;
2093
+ const sponsorId = detail.state?.sponsorId ?? this.checkoutState?.sponsorId;
2094
+ if (sponsorId == null) return;
2095
+ this.confirmOrder(detail.method, sponsorId, detail.result);
2096
+ };
2097
+ this.boundOnPaymentError = (e) => {
2098
+ const detail = e.detail;
2099
+ this.paymentError = detail.method === "klarna" ? `Klarna-betalingen kunne ikke fullf\xF8res: ${detail.error}` : detail.error;
2100
+ this.open = true;
2101
+ };
2102
+ }
2103
+ static {
2104
+ this.styles = css`
2775
2105
  :host { display: contents; }
2776
2106
  .backdrop {
2777
2107
  position: fixed;
@@ -3171,53 +2501,554 @@ VioCheckout.styles = css`
3171
2501
  }
3172
2502
  .confirm-close:hover { background: #222; }
3173
2503
 
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
- }
2504
+ .order-summary {
2505
+ background: var(--vio-color-surface-muted, #f2f2f2);
2506
+ padding: var(--vio-space-lg, 24px);
2507
+ margin-top: var(--vio-space-xl, 32px);
2508
+ }
2509
+ .order-line {
2510
+ display: grid;
2511
+ grid-template-columns: 56px 1fr auto;
2512
+ gap: 12px;
2513
+ padding: 8px 0;
2514
+ align-items: center;
2515
+ }
2516
+ .order-line-img {
2517
+ aspect-ratio: 1;
2518
+ background: #fff;
2519
+ overflow: hidden;
2520
+ }
2521
+ .order-line-img img { width: 100%; height: 100%; object-fit: cover; display: block; }
2522
+ .order-line-meta { font-size: 13px; }
2523
+ .order-line-meta b { display: block; }
2524
+ .order-line-meta span {
2525
+ color: var(--vio-color-text-secondary, #666);
2526
+ font-size: 11px;
2527
+ text-transform: uppercase;
2528
+ letter-spacing: var(--vio-tracking-label, 0.1em);
2529
+ }
2530
+ .order-line-price { font-weight: 700; font-size: 14px; }
2531
+ .order-row {
2532
+ display: flex;
2533
+ justify-content: space-between;
2534
+ padding: 8px 0;
2535
+ font-size: 14px;
2536
+ }
2537
+ .order-row.total {
2538
+ border-top: 1px solid var(--vio-color-border-default, #ccc);
2539
+ margin-top: var(--vio-space-sm, 8px);
2540
+ padding-top: var(--vio-space-md, 16px);
2541
+ font-weight: 700;
2542
+ font-size: 16px;
2543
+ }
2544
+
2545
+ @media (max-width: 600px) {
2546
+ .form-row { grid-template-columns: 1fr; gap: 0; }
2547
+ .payment-grid { grid-template-columns: 1fr; }
2548
+ .body { padding: 32px var(--vio-space-lg, 24px) 64px; }
2549
+ }
2550
+ `;
2551
+ }
2552
+ connectedCallback() {
2553
+ super.connectedCallback();
2554
+ this.checkoutState = Vio.checkout.currentState;
2555
+ this.items = Vio.checkout.items;
2556
+ Vio.checkout.addEventListener("change", this.boundOnCheckoutChange);
2557
+ Vio.checkout.addEventListener("payment-complete", this.boundOnPaymentComplete);
2558
+ Vio.checkout.addEventListener("payment-error", this.boundOnPaymentError);
2559
+ if (this.checkoutState) {
2560
+ void this.refreshApplePay();
2561
+ void this.refreshKlarna();
2562
+ }
2563
+ }
2564
+ disconnectedCallback() {
2565
+ Vio.checkout.removeEventListener("change", this.boundOnCheckoutChange);
2566
+ Vio.checkout.removeEventListener("payment-complete", this.boundOnPaymentComplete);
2567
+ Vio.checkout.removeEventListener("payment-error", this.boundOnPaymentError);
2568
+ this.unmountKlarna();
2569
+ super.disconnectedCallback();
2570
+ }
2571
+ updated() {
2572
+ void this.mountKlarnaIfNeeded();
2573
+ }
2574
+ close() {
2575
+ this.open = false;
2576
+ this.express = false;
2577
+ Vio.checkout.close();
2578
+ }
2579
+ show() {
2580
+ this.open = true;
2581
+ }
2582
+ async refreshApplePay() {
2583
+ Vio.checkout.clearApplePayCache();
2584
+ this.applePayAvailable = await Vio.checkout.isApplePayAvailable("NO");
2585
+ }
2586
+ async refreshKlarna() {
2587
+ const available = await Vio.checkout.klarnaPaymentsAvailable();
2588
+ if (available !== this.klarnaAvailable) {
2589
+ if (!available) this.unmountKlarna();
2590
+ this.klarnaAvailable = available;
2591
+ }
2592
+ }
2593
+ /** Mount (or re-mount on amount change) the Klarna Payments widget. */
2594
+ async mountKlarnaIfNeeded() {
2595
+ if (!this.klarnaAvailable || !this.open || !this.checkoutState) return;
2596
+ if (this.checkoutState.paymentMethod !== "klarna" && !this.express) return;
2597
+ if (this.klarnaMounting) return;
2598
+ const amount = this.checkoutState.subtotal;
2599
+ if (this.klarnaHandle && this.klarnaMountedAmount === amount && this.klarnaMountedShipping === this.selectedShipping) {
2600
+ return;
2601
+ }
2602
+ const container = this.renderRoot?.querySelector(
2603
+ "#vio-klarna-payments-container"
2604
+ );
2605
+ if (!container) return;
2606
+ this.klarnaMounting = true;
2607
+ this.unmountKlarna();
2608
+ container.innerHTML = "";
2609
+ try {
2610
+ const handle = await Vio.checkout.mountKlarnaPayments(container, {
2611
+ withShipping: this.express,
2612
+ shippingId: this.selectedShipping
2613
+ });
2614
+ this.klarnaHandle = handle;
2615
+ this.klarnaMountedAmount = amount;
2616
+ this.klarnaMountedShipping = this.selectedShipping;
2617
+ this.klarnaCategories = handle.categories;
2618
+ this.klarnaSelectedCat = handle.selected;
2619
+ } catch (err) {
2620
+ if (typeof console !== "undefined") {
2621
+ console.warn("[VioCheckout] Klarna Payments mount failed:", err);
2622
+ }
2623
+ this.paymentError = `Kunne ikke laste Klarna: ${err instanceof Error ? err.message : String(err)}`;
2624
+ } finally {
2625
+ this.klarnaMounting = false;
2626
+ }
2627
+ }
2628
+ unmountKlarna() {
2629
+ if (this.klarnaHandle) {
2630
+ this.klarnaHandle.unmount();
2631
+ this.klarnaHandle = null;
2632
+ }
2633
+ this.klarnaMountedAmount = null;
2634
+ this.klarnaMountedShipping = null;
2635
+ this.klarnaCategories = [];
2636
+ this.klarnaSelectedCat = "";
2637
+ this.klarnaAuthorizing = false;
2638
+ }
2639
+ /** Selected shipping option (express flow). */
2640
+ get shippingOption() {
2641
+ return KLARNA_SHIPPING_OPTIONS.find((o) => o.id === this.selectedShipping) ?? null;
2642
+ }
2643
+ /** Pick a shipping option → re-mounts the Klarna widget with the new total. */
2644
+ onSelectShipping(id) {
2645
+ if (this.selectedShipping === id || this.klarnaMounting) return;
2646
+ this.selectedShipping = id;
2647
+ }
2648
+ /** Switch the Klarna widget to a different payment_method_category. */
2649
+ async onKlarnaCategory(identifier) {
2650
+ if (!this.klarnaHandle || this.klarnaSelectedCat === identifier) return;
2651
+ this.klarnaSelectedCat = identifier;
2652
+ try {
2653
+ await this.klarnaHandle.load(identifier);
2654
+ } catch (err) {
2655
+ if (typeof console !== "undefined") {
2656
+ console.warn("[VioCheckout] Klarna category switch failed:", err);
2657
+ }
2658
+ }
2659
+ }
2660
+ /** Authorize the selected Klarna method → backend creates the order. */
2661
+ async onKlarnaAuthorize() {
2662
+ if (!this.klarnaHandle || this.klarnaAuthorizing) return;
2663
+ this.paymentError = null;
2664
+ this.klarnaAuthorizing = true;
2665
+ try {
2666
+ await this.klarnaHandle.authorize();
2667
+ } finally {
2668
+ this.klarnaAuthorizing = false;
2669
+ }
2670
+ }
2671
+ onFieldChange(field, value) {
2672
+ this.form = { ...this.form, [field]: value };
2673
+ Vio.checkout.setAddress(this.form);
2674
+ }
2675
+ async onApplePay() {
2676
+ if (this.applePayInProgress) return;
2677
+ this.applePayInProgress = true;
2678
+ try {
2679
+ await Vio.checkout.startApplePay({ country: "NO", label: "Vio" });
2680
+ } catch (err) {
2681
+ if (typeof console !== "undefined") {
2682
+ console.warn("[VioCheckout] Apple Pay flow ended:", err);
2683
+ }
2684
+ } finally {
2685
+ this.applePayInProgress = false;
2686
+ }
2687
+ }
2688
+ onPay(method) {
2689
+ this.paymentError = null;
2690
+ if (method === "apple-pay") {
2691
+ void this.onApplePay();
2692
+ return;
2693
+ }
2694
+ Vio.checkout.selectPaymentMethod(method);
2695
+ }
2696
+ /**
2697
+ * Complete the order for the selected method (Vipps / Card / plain Klarna).
2698
+ * Apple Pay and Klarna Express finish inside their own sheet/popup, so they
2699
+ * never reach here. No real PSP is wired for Vipps/Card in this iteration —
2700
+ * the capture happens backend-side in the full flow — so this confirms the
2701
+ * order state-only and shows the confirmation screen.
2702
+ */
2703
+ onCompleteOrder() {
2704
+ const s = this.checkoutState;
2705
+ if (!s || !s.paymentMethod) return;
2706
+ this.confirmOrder(s.paymentMethod, s.sponsorId);
2707
+ }
2708
+ /**
2709
+ * Shared order confirmation — reached from both the plain "Betal" CTA
2710
+ * (Vipps/Card/Klarna) and the Klarna Express authorize callback. Shows
2711
+ * the confirmation screen, emits `vio:payment-success`, and clears the
2712
+ * paid sponsor's cart so the badge + drawer reflect the purchase.
2713
+ */
2714
+ confirmOrder(method, sponsorId, result) {
2715
+ const r = result ?? {};
2716
+ this.confirmedOrder = {
2717
+ items: [...this.items],
2718
+ currency: this.checkoutState?.currency ?? "",
2719
+ total: typeof r.chargedTotal === "number" ? r.chargedTotal : this.checkoutState?.subtotal ?? 0,
2720
+ orderId: r.order?.orderId
2721
+ };
2722
+ this.confirmedMethod = method;
2723
+ this.orderConfirmed = true;
2724
+ this.open = true;
2725
+ this.dispatchEvent(
2726
+ new CustomEvent("vio:payment-success", {
2727
+ bubbles: true,
2728
+ composed: true,
2729
+ detail: { method, sponsorId, result }
2730
+ })
2731
+ );
2732
+ Vio.cart.clearSponsorCart(sponsorId);
2733
+ }
2734
+ methodLabel(method) {
2735
+ switch (method) {
2736
+ case "apple-pay":
2737
+ return "Apple Pay";
2738
+ case "klarna":
2739
+ return "Klarna";
2740
+ case "vipps":
2741
+ return "Vipps";
2742
+ case "card":
2743
+ return "kort";
2744
+ default:
2745
+ return "";
2746
+ }
2747
+ }
2748
+ /** True for methods that complete via a separate sheet/widget, not the CTA. */
2749
+ isExpressMethod(method) {
2750
+ return method === "apple-pay" || method === "klarna";
2751
+ }
2752
+ orderTotal() {
2753
+ const s = this.checkoutState;
2754
+ if (!s) return "";
2755
+ return formatPrice(s.subtotal, s.currency);
2756
+ }
2757
+ /** Shipping cost (major units) applied in express mode, else 0. */
2758
+ shippingMajor() {
2759
+ return this.express ? (this.shippingOption?.price ?? 0) / 100 : 0;
2760
+ }
2761
+ /** Total to charge — items + shipping (express). */
2762
+ payTotalLabel() {
2763
+ const s = this.checkoutState;
2764
+ if (!s) return "";
2765
+ return formatPrice(s.subtotal + this.shippingMajor(), s.currency);
2766
+ }
2767
+ render() {
2768
+ return html`
2769
+ <div class="backdrop" @click=${this.close}></div>
2770
+ <div
2771
+ class="modal ${this.express ? "as-side" : this.orderConfirmed ? "as-confirm" : ""}"
2772
+ role="dialog"
2773
+ aria-label=${this.heading}
2774
+ >
2775
+ <div class="handle" @click=${this.close} aria-hidden="true"></div>
2776
+ <div class="topbar">
2777
+ <h2 class="heading">${this.orderConfirmed ? "Takk!" : this.heading}</h2>
2778
+ <button class="close" @click=${this.close} aria-label="Lukk">×</button>
2779
+ </div>
2780
+
2781
+ <div class="body">
2782
+ ${this.orderConfirmed ? this.renderConfirmation() : this.express ? this.renderKlarnaExpress() : this.renderCheckoutBody()}
2783
+ </div>
2784
+ </div>
2785
+ `;
2786
+ }
2787
+ renderConfirmation() {
2788
+ const o = this.confirmedOrder;
2789
+ return html`
2790
+ <section class="confirmation">
2791
+ <div class="confirm-check" aria-hidden="true">✓</div>
2792
+ <h3 class="confirm-title">Takk for bestillingen!</h3>
2793
+ <p class="confirm-text">
2794
+ Betaling med ${this.methodLabel(this.confirmedMethod)} er bekreftet.${this.form.email ? html` Kvittering sendes til ${this.form.email}.` : ""}
2795
+ </p>
2796
+ ${o?.orderId ? html`<div class="confirm-order-id">Ordrenummer: <b>${o.orderId}</b></div>` : ""}
2797
+ ${o && o.items.length > 0 ? html`
2798
+ <div class="confirm-summary">
2799
+ ${o.items.map(
2800
+ (it) => html`
2801
+ <div class="order-row">
2802
+ <span>${it.brand ? `${it.brand} ` : ""}${it.name} ×${it.quantity}</span>
2803
+ <span>${formatPrice(it.unitPrice * it.quantity, it.currency)}</span>
2804
+ </div>
2805
+ `
2806
+ )}
2807
+ <div class="order-row total">
2808
+ <span>Totalt</span><span>${formatPrice(o.total, o.currency)}</span>
2809
+ </div>
2810
+ </div>
2811
+ ` : ""}
2812
+ <button class="confirm-close" @click=${this.close}>Lukk</button>
2813
+ </section>
2814
+ `;
2815
+ }
2816
+ /** Klarna Payments widget panel: shipping + category chips + widget + pay button. */
2817
+ renderKlarnaPanel() {
2818
+ const currency = this.checkoutState?.currency ?? "NOK";
2819
+ return html`
2820
+ <div class="klarna-panel">
2821
+ ${this.express ? html`
2822
+ <div class="ship-select" role="radiogroup" aria-label="Frakt">
2823
+ ${KLARNA_SHIPPING_OPTIONS.map(
2824
+ (o) => html`
2825
+ <button
2826
+ class="ship-opt ${this.selectedShipping === o.id ? "active" : ""}"
2827
+ role="radio"
2828
+ aria-checked=${this.selectedShipping === o.id}
2829
+ ?disabled=${this.klarnaMounting}
2830
+ @click=${() => this.onSelectShipping(o.id)}
2831
+ >
2832
+ <span class="ship-meta">
2833
+ <b>${o.method}</b><span>${o.description}</span>
2834
+ </span>
2835
+ <span class="ship-price">${formatPrice(o.price / 100, currency)}</span>
2836
+ </button>
2837
+ `
2838
+ )}
2839
+ </div>
2840
+ ` : ""}
2841
+ ${this.klarnaCategories.length > 1 ? html`
2842
+ <div class="klarna-cats">
2843
+ ${this.klarnaCategories.map(
2844
+ (c) => html`
2845
+ <button
2846
+ class="klarna-cat ${this.klarnaSelectedCat === c.identifier ? "active" : ""}"
2847
+ @click=${() => void this.onKlarnaCategory(c.identifier)}
2848
+ >
2849
+ ${c.name}
2850
+ </button>
2851
+ `
2852
+ )}
2853
+ </div>
2854
+ ` : ""}
2855
+ <div class="klarna-widget" id="vio-klarna-payments-container"></div>
2856
+ <button
2857
+ class="payment-btn primary complete-cta"
2858
+ @click=${() => void this.onKlarnaAuthorize()}
2859
+ ?disabled=${this.klarnaAuthorizing || !this.klarnaHandle || this.klarnaMounting}
2860
+ >
2861
+ ${this.klarnaAuthorizing ? "Behandler\u2026" : `Betal ${this.payTotalLabel()} med Klarna`}
2862
+ </button>
2863
+ </div>
2864
+ `;
2865
+ }
2866
+ /** Express layout — only the Klarna widget + a compact order summary. */
2867
+ renderKlarnaExpress() {
2868
+ return html`
2869
+ <section class="section">
2870
+ <h3 class="section-heading">Kjøp med Klarna</h3>
2871
+ ${this.items.length > 0 ? html`
2872
+ <div class="express-summary">
2873
+ ${this.items.map(
2874
+ (item) => html`
2875
+ <div class="order-row">
2876
+ <span>${item.brand ? `${item.brand} ` : ""}${item.name} ×${item.quantity}</span>
2877
+ <span>${formatPrice(item.unitPrice * item.quantity, item.currency)}</span>
2878
+ </div>
2879
+ `
2880
+ )}
2881
+ <div class="order-row">
2882
+ <span>Frakt${this.shippingOption ? ` \u2013 ${this.shippingOption.method}` : ""}</span>
2883
+ <span>${formatPrice(this.shippingMajor(), this.checkoutState?.currency ?? "NOK")}</span>
2884
+ </div>
2885
+ <div class="order-row total">
2886
+ <span>Totalt</span><span>${this.payTotalLabel()}</span>
2887
+ </div>
2888
+ </div>
2889
+ ` : ""}
2890
+ ${this.renderKlarnaPanel()}
2891
+ ${this.paymentError ? html`<div class="payment-error">${this.paymentError}</div>` : ""}
2892
+ </section>
2893
+ `;
2894
+ }
2895
+ renderCheckoutBody() {
2896
+ const method = this.checkoutState?.paymentMethod;
2897
+ const showCompleteCta = !!method && !this.isExpressMethod(method);
2898
+ return html`
2899
+ <section class="section">
2900
+ <div class="section-label">Steg 1</div>
2901
+ <h3 class="section-heading">Leveringsadresse</h3>
2902
+ <div class="form-row">
2903
+ <div class="field">
2904
+ <label>Fornavn</label>
2905
+ <input
2906
+ type="text"
2907
+ autocomplete="given-name"
2908
+ .value=${this.form.firstName}
2909
+ @input=${(e) => this.onFieldChange("firstName", e.target.value)}
2910
+ />
2911
+ </div>
2912
+ <div class="field">
2913
+ <label>Etternavn</label>
2914
+ <input
2915
+ type="text"
2916
+ autocomplete="family-name"
2917
+ .value=${this.form.lastName}
2918
+ @input=${(e) => this.onFieldChange("lastName", e.target.value)}
2919
+ />
2920
+ </div>
2921
+ </div>
2922
+ <div class="field">
2923
+ <label>E-post</label>
2924
+ <input
2925
+ type="email"
2926
+ autocomplete="email"
2927
+ .value=${this.form.email}
2928
+ @input=${(e) => this.onFieldChange("email", e.target.value)}
2929
+ />
2930
+ </div>
2931
+ <div class="field">
2932
+ <label>Adresse</label>
2933
+ <input
2934
+ type="text"
2935
+ autocomplete="street-address"
2936
+ .value=${this.form.address}
2937
+ @input=${(e) => this.onFieldChange("address", e.target.value)}
2938
+ />
2939
+ </div>
2940
+ <div class="form-row">
2941
+ <div class="field">
2942
+ <label>Postnummer</label>
2943
+ <input
2944
+ type="text"
2945
+ autocomplete="postal-code"
2946
+ .value=${this.form.postalCode}
2947
+ @input=${(e) => this.onFieldChange("postalCode", e.target.value)}
2948
+ />
2949
+ </div>
2950
+ <div class="field">
2951
+ <label>Sted</label>
2952
+ <input
2953
+ type="text"
2954
+ autocomplete="address-level2"
2955
+ .value=${this.form.city}
2956
+ @input=${(e) => this.onFieldChange("city", e.target.value)}
2957
+ />
2958
+ </div>
2959
+ </div>
2960
+ </section>
2961
+
2962
+ <section class="section">
2963
+ <div class="section-label">Steg 2</div>
2964
+ <h3 class="section-heading">Velg betalingsmåte</h3>
2965
+ <div class="payment-grid">
2966
+ ${this.applePayAvailable ? html`
2967
+ <button
2968
+ class="payment-btn primary"
2969
+ @click=${() => this.onPay("apple-pay")}
2970
+ ?disabled=${this.applePayInProgress}
2971
+ aria-pressed=${this.checkoutState?.paymentMethod === "apple-pay"}
2972
+ >
2973
+ ${this.applePayInProgress ? "\xC5pner\u2026" : " Pay"}
2974
+ </button>
2975
+ ` : ""}
2976
+ ${this.klarnaAvailable ? html`
2977
+ <button
2978
+ class="payment-btn"
2979
+ @click=${() => this.onPay("klarna")}
2980
+ aria-pressed=${this.checkoutState?.paymentMethod === "klarna"}
2981
+ >
2982
+ Klarna
2983
+ </button>
2984
+ ` : ""}
2985
+ <button
2986
+ class="payment-btn"
2987
+ @click=${() => this.onPay("vipps")}
2988
+ aria-pressed=${this.checkoutState?.paymentMethod === "vipps"}
2989
+ >
2990
+ Vipps
2991
+ </button>
2992
+ <button
2993
+ class="payment-btn"
2994
+ @click=${() => this.onPay("card")}
2995
+ aria-pressed=${this.checkoutState?.paymentMethod === "card"}
2996
+ >
2997
+ Kort
2998
+ </button>
2999
+ ${!this.applePayAvailable ? html`
3000
+ <div class="apple-pay-note">
3001
+ Pay krever Safari på iOS eller macOS med Apple Pay konfigurert.
3002
+ </div>
3003
+ ` : ""}
3004
+ ${showCompleteCta ? html`
3005
+ <button
3006
+ class="payment-btn primary complete-cta"
3007
+ @click=${this.onCompleteOrder}
3008
+ >
3009
+ Betal ${this.orderTotal()} med ${this.methodLabel(method ?? null)}
3010
+ </button>
3011
+ ` : ""}
3012
+ ${method === "klarna" ? this.renderKlarnaPanel() : ""}
3013
+ ${this.paymentError ? html`<div class="payment-error">${this.paymentError}</div>` : ""}
3014
+ </div>
3015
+ </section>
3214
3016
 
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
- `;
3017
+ ${this.items.length > 0 ? html`
3018
+ <section class="order-summary">
3019
+ ${this.items.map(
3020
+ (item) => html`
3021
+ <div class="order-line">
3022
+ <div class="order-line-img">
3023
+ ${item.imageUrl ? html`<img src=${item.imageUrl} alt=${item.name} />` : ""}
3024
+ </div>
3025
+ <div class="order-line-meta">
3026
+ <span>${item.brand}</span>
3027
+ <b>${item.name}</b>
3028
+ <span>×${item.quantity}</span>
3029
+ </div>
3030
+ <div class="order-line-price">
3031
+ ${formatPrice(item.unitPrice * item.quantity, item.currency)}
3032
+ </div>
3033
+ </div>
3034
+ `
3035
+ )}
3036
+ <div class="order-row" style="margin-top: 16px;">
3037
+ <span>Sum</span><span>${this.orderTotal()}</span>
3038
+ </div>
3039
+ <div class="order-row">
3040
+ <span>Frakt</span><span>${this.shippingLabel}</span>
3041
+ </div>
3042
+ <div class="order-row total">
3043
+ <span>Totalt</span><span>${this.orderTotal()}</span>
3044
+ </div>
3045
+ </section>
3046
+ ` : ""}
3047
+ </div>
3048
+ </div>
3049
+ `;
3050
+ }
3051
+ };
3221
3052
  __decorateClass([
3222
3053
  property({ type: Boolean, reflect: true })
3223
3054
  ], VioCheckout.prototype, "open", 2);
@@ -3272,13 +3103,22 @@ __decorateClass([
3272
3103
  __decorateClass([
3273
3104
  state()
3274
3105
  ], VioCheckout.prototype, "form", 2);
3275
- VioCheckout = __decorateClass([
3276
- customElement("vio-checkout")
3277
- ], VioCheckout);
3278
3106
 
3279
- // src/ui/index.ts
3280
- var VIO_UI_VERSION = "0.1.0";
3107
+ // src/ui/elements.ts
3108
+ function registerVioElements() {
3109
+ if (typeof customElements === "undefined") return;
3110
+ const defs = [
3111
+ ["vio-product", VioProduct],
3112
+ ["vio-product-carousel", VioProductCarousel],
3113
+ ["vio-product-detail", VioProductDetail],
3114
+ ["vio-cart", VioCart],
3115
+ ["vio-checkout", VioCheckout]
3116
+ ];
3117
+ for (const [tag, ctor] of defs) {
3118
+ if (!customElements.get(tag)) customElements.define(tag, ctor);
3119
+ }
3120
+ }
3281
3121
 
3282
- export { VIO_UI_VERSION, VioCart, VioCheckout, VioProduct, VioProductCarousel, VioProductDetail, VioTokens };
3283
- //# sourceMappingURL=chunk-4CCGE7DE.js.map
3284
- //# sourceMappingURL=chunk-4CCGE7DE.js.map
3122
+ export { VioCart, VioCheckout, VioProduct, VioProductCarousel, VioProductDetail, registerVioElements };
3123
+ //# sourceMappingURL=chunk-7JSSYPRN.js.map
3124
+ //# sourceMappingURL=chunk-7JSSYPRN.js.map