@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.
- package/README.md +66 -24
- package/dist/{chunk-4CCGE7DE.js → chunk-7JSSYPRN.js} +1548 -1708
- package/dist/chunk-7JSSYPRN.js.map +1 -0
- package/dist/{chunk-5SPY7TH6.js → chunk-SOO2G6CF.js} +4 -4
- package/dist/chunk-SOO2G6CF.js.map +1 -0
- package/dist/core/index.js +1 -1
- package/dist/index.d.ts +0 -3
- package/dist/index.js +1 -2
- package/dist/react/index.d.ts +23 -0
- package/dist/react/index.js +74 -0
- package/dist/react/index.js.map +1 -0
- package/dist/ui/index.d.ts +29 -4
- package/dist/ui/index.js +136 -2
- package/dist/ui/index.js.map +1 -1
- package/dist/{vio-checkout-D8B4Upk8.d.ts → vio-checkout-CrC5tvmh.d.ts} +0 -1
- package/package.json +18 -2
- package/dist/chunk-4CCGE7DE.js.map +0 -1
- package/dist/chunk-5SPY7TH6.js.map +0 -1
|
@@ -1,133 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { property, state,
|
|
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
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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
|
-
|
|
790
|
-
|
|
791
|
-
|
|
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
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
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
|
|
811
|
-
|
|
812
|
-
|
|
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
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
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
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
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
|
-
|
|
910
|
-
if (!this.
|
|
911
|
-
this.
|
|
912
|
-
|
|
913
|
-
|
|
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
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
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
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
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
|
-
|
|
972
|
-
this.close();
|
|
545
|
+
return html`<slot></slot>`;
|
|
973
546
|
}
|
|
974
547
|
render() {
|
|
975
548
|
return html`
|
|
976
|
-
<div class="
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
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
|
-
|
|
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(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").trim();
|
|
1141
|
-
}
|
|
1142
579
|
};
|
|
1143
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
this.
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
this.
|
|
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
|
-
|
|
1630
|
-
|
|
1631
|
-
if (
|
|
1632
|
-
this.
|
|
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
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
this.
|
|
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
|
-
/**
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
if (!
|
|
1653
|
-
|
|
1654
|
-
|
|
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
|
-
|
|
1667
|
-
const
|
|
1668
|
-
|
|
1669
|
-
|
|
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
|
-
|
|
1682
|
-
|
|
1683
|
-
this.
|
|
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
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
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
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
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
|
-
|
|
1709
|
-
|
|
1710
|
-
return "NOK";
|
|
1139
|
+
selectOption(optionName, value) {
|
|
1140
|
+
this.selectedOptions = { ...this.selectedOptions, [optionName]: value };
|
|
1711
1141
|
}
|
|
1712
|
-
|
|
1713
|
-
|
|
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
|
-
|
|
1716
|
-
|
|
1148
|
+
get activeImage() {
|
|
1149
|
+
return this.allImages()[this.activeImageIndex];
|
|
1717
1150
|
}
|
|
1718
|
-
|
|
1719
|
-
|
|
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
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
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="
|
|
1799
|
-
@click=${this.
|
|
1800
|
-
aria-label
|
|
1280
|
+
class="gallery-thumb ${idx === this.activeImageIndex ? "active" : ""}"
|
|
1281
|
+
@click=${() => this.activeImageIndex = idx}
|
|
1282
|
+
aria-label=${`Bilde ${idx + 1}`}
|
|
1801
1283
|
>
|
|
1802
|
-
<
|
|
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="
|
|
1807
|
-
@click=${this.
|
|
1808
|
-
|
|
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-
|
|
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
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
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
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
<
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
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
|
-
|
|
1845
|
-
|
|
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(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").trim();
|
|
1847
1399
|
}
|
|
1848
1400
|
};
|
|
1849
|
-
|
|
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
|
-
.
|
|
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.
|
|
2277
|
-
this.
|
|
2278
|
-
Vio.
|
|
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.
|
|
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
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
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
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
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
|
-
/**
|
|
2371
|
-
async
|
|
2372
|
-
if (
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
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
|
|
2389
|
-
|
|
2390
|
-
this.
|
|
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
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
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
|
-
|
|
2407
|
-
|
|
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
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
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
|
-
|
|
1845
|
+
return { items, total: this.totalAcrossSponsors, currency: this.primaryCurrency };
|
|
2417
1846
|
}
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
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
|
-
*
|
|
2432
|
-
* (
|
|
2433
|
-
*
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
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:
|
|
1860
|
+
new CustomEvent("vio:checkout-open", {
|
|
2449
1861
|
bubbles: true,
|
|
2450
1862
|
composed: true,
|
|
2451
|
-
detail: {
|
|
1863
|
+
detail: { sponsorId: firstSponsorId, paymentMethod: "klarna", express: true }
|
|
2452
1864
|
})
|
|
2453
1865
|
);
|
|
2454
|
-
Vio.cart.clearSponsorCart(sponsorId);
|
|
2455
1866
|
}
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
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
|
-
|
|
2471
|
-
|
|
2472
|
-
return
|
|
1874
|
+
get primaryCurrency() {
|
|
1875
|
+
for (const cart of this.carts.values()) return cart.currency;
|
|
1876
|
+
return "NOK";
|
|
2473
1877
|
}
|
|
2474
|
-
|
|
2475
|
-
|
|
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
|
-
|
|
2480
|
-
|
|
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
|
-
|
|
2484
|
-
|
|
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
|
-
<
|
|
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="
|
|
2499
|
-
<h2 class="heading">${this.
|
|
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
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
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
|
-
|
|
2530
|
-
|
|
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
|
-
|
|
2535
|
-
</section>
|
|
1984
|
+
`}
|
|
1985
|
+
</aside>
|
|
2536
1986
|
`;
|
|
2537
1987
|
}
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
1988
|
+
renderConfirmation() {
|
|
1989
|
+
const o = this.confirmedOrder;
|
|
1990
|
+
if (!o) return html``;
|
|
2541
1991
|
return html`
|
|
2542
|
-
<div class="
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
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
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
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/
|
|
3280
|
-
|
|
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 {
|
|
3283
|
-
//# sourceMappingURL=chunk-
|
|
3284
|
-
//# sourceMappingURL=chunk-
|
|
3122
|
+
export { VioCart, VioCheckout, VioProduct, VioProductCarousel, VioProductDetail, registerVioElements };
|
|
3123
|
+
//# sourceMappingURL=chunk-7JSSYPRN.js.map
|
|
3124
|
+
//# sourceMappingURL=chunk-7JSSYPRN.js.map
|