@vio-live/web-sdk 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -24
- package/dist/chunk-NP2KCM7X.js +3859 -0
- package/dist/chunk-NP2KCM7X.js.map +1 -0
- package/dist/{chunk-4CCGE7DE.js → chunk-NWORYWIL.js} +2649 -1711
- package/dist/chunk-NWORYWIL.js.map +1 -0
- package/dist/chunk-PBQSHIID.js +3 -0
- package/dist/{chunk-GGM2AIK5.js.map → chunk-PBQSHIID.js.map} +1 -1
- package/dist/core/index.d.ts +314 -12
- package/dist/core/index.js +2 -2
- package/dist/index.d.ts +1 -4
- package/dist/index.js +2 -3
- 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-T9R5hpDx.d.ts} +137 -10
- package/package.json +18 -2
- package/dist/chunk-4CCGE7DE.js.map +0 -1
- package/dist/chunk-5SPY7TH6.js +0 -1618
- package/dist/chunk-5SPY7TH6.js.map +0 -1
- package/dist/chunk-GGM2AIK5.js +0 -3
|
@@ -1,133 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { property, state,
|
|
1
|
+
import { Vio, __decorateClass, primaryImageUrl, displayPrice, getGlobalCurrency, formatPrice, KLARNA_SHIPPING_OPTIONS } from './chunk-NP2KCM7X.js';
|
|
2
|
+
import { LitElement, css, html } from 'lit';
|
|
3
|
+
import { property, state, query } from 'lit/decorators.js';
|
|
4
4
|
|
|
5
|
-
// src/ui/tokens.ts
|
|
6
|
-
var VioTokens = {
|
|
7
|
-
color: {
|
|
8
|
-
text: {
|
|
9
|
-
primary: "#0a0a0a",
|
|
10
|
-
secondary: "#666666",
|
|
11
|
-
tertiary: "#999999",
|
|
12
|
-
onPrimary: "#ffffff"
|
|
13
|
-
},
|
|
14
|
-
accent: {
|
|
15
|
-
editorial: "#c14a3b"
|
|
16
|
-
// "REDAKSJONENS FAVORITTER" red
|
|
17
|
-
},
|
|
18
|
-
surface: {
|
|
19
|
-
base: "#ffffff",
|
|
20
|
-
muted: "#f2f2f2",
|
|
21
|
-
// product image background
|
|
22
|
-
hover: "#fafafa"
|
|
23
|
-
},
|
|
24
|
-
border: {
|
|
25
|
-
subtle: "#e5e5e5",
|
|
26
|
-
default: "#cccccc"
|
|
27
|
-
}
|
|
28
|
-
},
|
|
29
|
-
font: {
|
|
30
|
-
serif: '"Tinos", "Playfair Display", Georgia, serif',
|
|
31
|
-
sans: '"Inter", -apple-system, "Helvetica Neue", Arial, sans-serif'
|
|
32
|
-
},
|
|
33
|
-
size: {
|
|
34
|
-
xs: "12px",
|
|
35
|
-
sm: "14px",
|
|
36
|
-
md: "16px",
|
|
37
|
-
lg: "20px",
|
|
38
|
-
xl: "24px",
|
|
39
|
-
"2xl": "32px",
|
|
40
|
-
"3xl": "40px"
|
|
41
|
-
},
|
|
42
|
-
weight: {
|
|
43
|
-
regular: 400,
|
|
44
|
-
medium: 500,
|
|
45
|
-
semibold: 600,
|
|
46
|
-
bold: 700
|
|
47
|
-
},
|
|
48
|
-
tracking: {
|
|
49
|
-
label: "0.1em",
|
|
50
|
-
// uppercase labels (BRAND, RETAILER, REDAKSJONENS FAVORITTER)
|
|
51
|
-
title: "0"
|
|
52
|
-
},
|
|
53
|
-
lineHeight: {
|
|
54
|
-
tight: 1.1,
|
|
55
|
-
snug: 1.3,
|
|
56
|
-
normal: 1.5
|
|
57
|
-
},
|
|
58
|
-
space: {
|
|
59
|
-
xs: "4px",
|
|
60
|
-
sm: "8px",
|
|
61
|
-
md: "16px",
|
|
62
|
-
lg: "24px",
|
|
63
|
-
xl: "32px",
|
|
64
|
-
"2xl": "48px",
|
|
65
|
-
"3xl": "64px"
|
|
66
|
-
},
|
|
67
|
-
radius: {
|
|
68
|
-
none: "0",
|
|
69
|
-
sm: "2px",
|
|
70
|
-
md: "4px",
|
|
71
|
-
lg: "8px",
|
|
72
|
-
full: "9999px"
|
|
73
|
-
}
|
|
74
|
-
};
|
|
75
|
-
var VIO_TOKENS_CSS = `
|
|
76
|
-
:root {
|
|
77
|
-
/* color \u2014 text */
|
|
78
|
-
--vio-color-text: ${VioTokens.color.text.primary};
|
|
79
|
-
--vio-color-text-secondary: ${VioTokens.color.text.secondary};
|
|
80
|
-
--vio-color-text-tertiary: ${VioTokens.color.text.tertiary};
|
|
81
|
-
--vio-color-text-on-primary:${VioTokens.color.text.onPrimary};
|
|
82
|
-
|
|
83
|
-
/* color \u2014 accent */
|
|
84
|
-
--vio-color-accent: ${VioTokens.color.accent.editorial};
|
|
85
|
-
|
|
86
|
-
/* color \u2014 surface */
|
|
87
|
-
--vio-color-surface: ${VioTokens.color.surface.base};
|
|
88
|
-
--vio-color-surface-muted: ${VioTokens.color.surface.muted};
|
|
89
|
-
--vio-color-surface-hover: ${VioTokens.color.surface.hover};
|
|
90
|
-
|
|
91
|
-
/* color \u2014 border */
|
|
92
|
-
--vio-color-border: ${VioTokens.color.border.subtle};
|
|
93
|
-
--vio-color-border-default: ${VioTokens.color.border.default};
|
|
94
|
-
|
|
95
|
-
/* font */
|
|
96
|
-
--vio-font-serif: ${VioTokens.font.serif};
|
|
97
|
-
--vio-font-sans: ${VioTokens.font.sans};
|
|
98
|
-
|
|
99
|
-
/* size */
|
|
100
|
-
--vio-size-xs: ${VioTokens.size.xs};
|
|
101
|
-
--vio-size-sm: ${VioTokens.size.sm};
|
|
102
|
-
--vio-size-md: ${VioTokens.size.md};
|
|
103
|
-
--vio-size-lg: ${VioTokens.size.lg};
|
|
104
|
-
--vio-size-xl: ${VioTokens.size.xl};
|
|
105
|
-
|
|
106
|
-
/* tracking (uppercase letterspacing) */
|
|
107
|
-
--vio-tracking-label: ${VioTokens.tracking.label};
|
|
108
|
-
|
|
109
|
-
/* space */
|
|
110
|
-
--vio-space-xs: ${VioTokens.space.xs};
|
|
111
|
-
--vio-space-sm: ${VioTokens.space.sm};
|
|
112
|
-
--vio-space-md: ${VioTokens.space.md};
|
|
113
|
-
--vio-space-lg: ${VioTokens.space.lg};
|
|
114
|
-
--vio-space-xl: ${VioTokens.space.xl};
|
|
115
|
-
|
|
116
|
-
/* radius */
|
|
117
|
-
--vio-radius-sm: ${VioTokens.radius.sm};
|
|
118
|
-
--vio-radius-md: ${VioTokens.radius.md};
|
|
119
|
-
--vio-radius-lg: ${VioTokens.radius.lg};
|
|
120
|
-
}
|
|
121
|
-
`;
|
|
122
|
-
function injectTokens() {
|
|
123
|
-
if (typeof document === "undefined") return;
|
|
124
|
-
if (document.querySelector('style[data-vio="tokens"]') !== null) return;
|
|
125
|
-
const style = document.createElement("style");
|
|
126
|
-
style.setAttribute("data-vio", "tokens");
|
|
127
|
-
style.textContent = VIO_TOKENS_CSS;
|
|
128
|
-
document.head.appendChild(style);
|
|
129
|
-
}
|
|
130
|
-
injectTokens();
|
|
131
5
|
var VioProduct = class extends LitElement {
|
|
132
6
|
constructor() {
|
|
133
7
|
super(...arguments);
|
|
@@ -139,11 +13,124 @@ var VioProduct = class extends LitElement {
|
|
|
139
13
|
this.productId = "";
|
|
140
14
|
this.sponsorId = "";
|
|
141
15
|
this.hasVariants = false;
|
|
16
|
+
this.variants = void 0;
|
|
17
|
+
this.options = void 0;
|
|
18
|
+
this.hideMeta = false;
|
|
142
19
|
this.inCart = false;
|
|
143
20
|
this.cartQty = 0;
|
|
144
21
|
this.justAdded = false;
|
|
145
22
|
this.boundCartChange = () => this.updateInCart();
|
|
146
23
|
}
|
|
24
|
+
static {
|
|
25
|
+
this.styles = css`
|
|
26
|
+
:host {
|
|
27
|
+
display: block;
|
|
28
|
+
box-sizing: border-box;
|
|
29
|
+
width: 100%;
|
|
30
|
+
cursor: pointer;
|
|
31
|
+
font-family: var(--vio-font-sans, -apple-system, sans-serif);
|
|
32
|
+
color: var(--vio-color-text, #0a0a0a);
|
|
33
|
+
-webkit-tap-highlight-color: transparent;
|
|
34
|
+
}
|
|
35
|
+
.card {
|
|
36
|
+
display: flex;
|
|
37
|
+
flex-direction: column;
|
|
38
|
+
height: 100%;
|
|
39
|
+
}
|
|
40
|
+
.image {
|
|
41
|
+
position: relative;
|
|
42
|
+
aspect-ratio: 1 / 1;
|
|
43
|
+
width: 100%;
|
|
44
|
+
background-color: var(--vio-color-surface-muted, #f2f2f2);
|
|
45
|
+
display: flex;
|
|
46
|
+
align-items: center;
|
|
47
|
+
justify-content: center;
|
|
48
|
+
overflow: hidden;
|
|
49
|
+
transition: opacity 0.2s ease;
|
|
50
|
+
}
|
|
51
|
+
:host(:hover) .image img { opacity: 0.92; }
|
|
52
|
+
|
|
53
|
+
/* Quick add-to-cart button — overlaid bottom-right of the image, like the
|
|
54
|
+
reference card's wishlist heart but wired to add the product to the cart. */
|
|
55
|
+
.add-btn {
|
|
56
|
+
position: absolute;
|
|
57
|
+
right: 10px;
|
|
58
|
+
bottom: 10px;
|
|
59
|
+
width: 40px;
|
|
60
|
+
height: 40px;
|
|
61
|
+
border-radius: 50%;
|
|
62
|
+
background: rgba(255, 255, 255, 0.96);
|
|
63
|
+
border: none;
|
|
64
|
+
display: flex;
|
|
65
|
+
align-items: center;
|
|
66
|
+
justify-content: center;
|
|
67
|
+
cursor: pointer;
|
|
68
|
+
color: #0a0a0a;
|
|
69
|
+
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.16);
|
|
70
|
+
transition: transform 0.14s ease, background 0.14s ease, color 0.14s ease;
|
|
71
|
+
padding: 0;
|
|
72
|
+
z-index: 2;
|
|
73
|
+
}
|
|
74
|
+
.add-btn:hover { background: #fff; transform: scale(1.08); }
|
|
75
|
+
.add-btn:active { transform: scale(0.95); }
|
|
76
|
+
/* In cart → black circle with a white tick (matches the floating cart). */
|
|
77
|
+
.add-btn.in-cart { background: #0a0a0a; color: #fff; }
|
|
78
|
+
.add-btn.in-cart:hover { background: #222; }
|
|
79
|
+
.add-btn.pop { animation: vio-add-pop 0.42s cubic-bezier(0.34, 1.56, 0.64, 1); }
|
|
80
|
+
.add-btn svg { width: 18px; height: 18px; }
|
|
81
|
+
.add-btn .add-count { font-size: 15px; font-weight: 700; line-height: 1; }
|
|
82
|
+
@keyframes vio-add-pop {
|
|
83
|
+
0% { transform: scale(1); }
|
|
84
|
+
35% { transform: scale(1.32); }
|
|
85
|
+
60% { transform: scale(0.9); }
|
|
86
|
+
100% { transform: scale(1); }
|
|
87
|
+
}
|
|
88
|
+
.image img {
|
|
89
|
+
width: 100%;
|
|
90
|
+
height: 100%;
|
|
91
|
+
object-fit: cover;
|
|
92
|
+
display: block;
|
|
93
|
+
}
|
|
94
|
+
.meta {
|
|
95
|
+
padding-top: 14px;
|
|
96
|
+
display: flex;
|
|
97
|
+
flex-direction: column;
|
|
98
|
+
flex: 1;
|
|
99
|
+
}
|
|
100
|
+
.brand {
|
|
101
|
+
font-size: var(--vio-size-xs, 12px);
|
|
102
|
+
letter-spacing: var(--vio-tracking-label, 0.1em);
|
|
103
|
+
text-transform: uppercase;
|
|
104
|
+
color: var(--vio-color-text-secondary, #666);
|
|
105
|
+
font-weight: 500;
|
|
106
|
+
margin-bottom: 6px;
|
|
107
|
+
}
|
|
108
|
+
.name {
|
|
109
|
+
font-size: var(--vio-size-md, 16px);
|
|
110
|
+
font-weight: 500;
|
|
111
|
+
line-height: 1.3;
|
|
112
|
+
margin: 0 0 10px;
|
|
113
|
+
display: -webkit-box;
|
|
114
|
+
-webkit-line-clamp: 2;
|
|
115
|
+
line-clamp: 2;
|
|
116
|
+
-webkit-box-orient: vertical;
|
|
117
|
+
overflow: hidden;
|
|
118
|
+
}
|
|
119
|
+
.price {
|
|
120
|
+
font-size: var(--vio-size-md, 16px);
|
|
121
|
+
font-weight: 700;
|
|
122
|
+
margin-bottom: 16px;
|
|
123
|
+
}
|
|
124
|
+
.retailer {
|
|
125
|
+
font-size: var(--vio-size-xs, 12px);
|
|
126
|
+
letter-spacing: var(--vio-tracking-label, 0.1em);
|
|
127
|
+
text-transform: uppercase;
|
|
128
|
+
color: var(--vio-color-text-tertiary, #999);
|
|
129
|
+
font-weight: 500;
|
|
130
|
+
margin-top: auto;
|
|
131
|
+
}
|
|
132
|
+
`;
|
|
133
|
+
}
|
|
147
134
|
connectedCallback() {
|
|
148
135
|
super.connectedCallback();
|
|
149
136
|
this.updateInCart();
|
|
@@ -201,7 +188,7 @@ var VioProduct = class extends LitElement {
|
|
|
201
188
|
* `vio:product-add` so the host adds it (bumping quantity on repeat taps). */
|
|
202
189
|
onAdd(e) {
|
|
203
190
|
e.stopPropagation();
|
|
204
|
-
if (this.hasVariants) {
|
|
191
|
+
if (this.hasVariants || Array.isArray(this.variants) && this.variants.length > 1 || Array.isArray(this.options) && this.options.length > 0) {
|
|
205
192
|
this.onClick();
|
|
206
193
|
return;
|
|
207
194
|
}
|
|
@@ -252,124 +239,17 @@ var VioProduct = class extends LitElement {
|
|
|
252
239
|
</svg>`}
|
|
253
240
|
</button>
|
|
254
241
|
</div>
|
|
242
|
+
${this.hideMeta ? "" : html`
|
|
255
243
|
<div class="meta">
|
|
256
244
|
${this.brand ? html`<div class="brand">${this.brand}</div>` : ""}
|
|
257
245
|
${this.name ? html`<div class="name">${this.name}</div>` : ""}
|
|
258
246
|
${this.price ? html`<div class="price">${this.price}</div>` : ""}
|
|
259
247
|
${this.retailer ? html`<div class="retailer">${this.retailer}</div>` : ""}
|
|
260
|
-
</div
|
|
248
|
+
</div>`}
|
|
261
249
|
</div>
|
|
262
250
|
`;
|
|
263
251
|
}
|
|
264
252
|
};
|
|
265
|
-
VioProduct.styles = css`
|
|
266
|
-
:host {
|
|
267
|
-
display: block;
|
|
268
|
-
box-sizing: border-box;
|
|
269
|
-
width: 100%;
|
|
270
|
-
cursor: pointer;
|
|
271
|
-
font-family: var(--vio-font-sans, -apple-system, sans-serif);
|
|
272
|
-
color: var(--vio-color-text, #0a0a0a);
|
|
273
|
-
-webkit-tap-highlight-color: transparent;
|
|
274
|
-
}
|
|
275
|
-
.card {
|
|
276
|
-
display: flex;
|
|
277
|
-
flex-direction: column;
|
|
278
|
-
height: 100%;
|
|
279
|
-
}
|
|
280
|
-
.image {
|
|
281
|
-
position: relative;
|
|
282
|
-
aspect-ratio: 1 / 1;
|
|
283
|
-
width: 100%;
|
|
284
|
-
background-color: var(--vio-color-surface-muted, #f2f2f2);
|
|
285
|
-
display: flex;
|
|
286
|
-
align-items: center;
|
|
287
|
-
justify-content: center;
|
|
288
|
-
overflow: hidden;
|
|
289
|
-
transition: opacity 0.2s ease;
|
|
290
|
-
}
|
|
291
|
-
:host(:hover) .image img { opacity: 0.92; }
|
|
292
|
-
|
|
293
|
-
/* Quick add-to-cart button — overlaid bottom-right of the image, like the
|
|
294
|
-
reference card's wishlist heart but wired to add the product to the cart. */
|
|
295
|
-
.add-btn {
|
|
296
|
-
position: absolute;
|
|
297
|
-
right: 10px;
|
|
298
|
-
bottom: 10px;
|
|
299
|
-
width: 40px;
|
|
300
|
-
height: 40px;
|
|
301
|
-
border-radius: 50%;
|
|
302
|
-
background: rgba(255, 255, 255, 0.96);
|
|
303
|
-
border: none;
|
|
304
|
-
display: flex;
|
|
305
|
-
align-items: center;
|
|
306
|
-
justify-content: center;
|
|
307
|
-
cursor: pointer;
|
|
308
|
-
color: #0a0a0a;
|
|
309
|
-
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.16);
|
|
310
|
-
transition: transform 0.14s ease, background 0.14s ease, color 0.14s ease;
|
|
311
|
-
padding: 0;
|
|
312
|
-
z-index: 2;
|
|
313
|
-
}
|
|
314
|
-
.add-btn:hover { background: #fff; transform: scale(1.08); }
|
|
315
|
-
.add-btn:active { transform: scale(0.95); }
|
|
316
|
-
/* In cart → black circle with a white tick (matches the floating cart). */
|
|
317
|
-
.add-btn.in-cart { background: #0a0a0a; color: #fff; }
|
|
318
|
-
.add-btn.in-cart:hover { background: #222; }
|
|
319
|
-
.add-btn.pop { animation: vio-add-pop 0.42s cubic-bezier(0.34, 1.56, 0.64, 1); }
|
|
320
|
-
.add-btn svg { width: 18px; height: 18px; }
|
|
321
|
-
.add-btn .add-count { font-size: 15px; font-weight: 700; line-height: 1; }
|
|
322
|
-
@keyframes vio-add-pop {
|
|
323
|
-
0% { transform: scale(1); }
|
|
324
|
-
35% { transform: scale(1.32); }
|
|
325
|
-
60% { transform: scale(0.9); }
|
|
326
|
-
100% { transform: scale(1); }
|
|
327
|
-
}
|
|
328
|
-
.image img {
|
|
329
|
-
width: 100%;
|
|
330
|
-
height: 100%;
|
|
331
|
-
object-fit: cover;
|
|
332
|
-
display: block;
|
|
333
|
-
}
|
|
334
|
-
.meta {
|
|
335
|
-
padding-top: 14px;
|
|
336
|
-
display: flex;
|
|
337
|
-
flex-direction: column;
|
|
338
|
-
flex: 1;
|
|
339
|
-
}
|
|
340
|
-
.brand {
|
|
341
|
-
font-size: var(--vio-size-xs, 12px);
|
|
342
|
-
letter-spacing: var(--vio-tracking-label, 0.1em);
|
|
343
|
-
text-transform: uppercase;
|
|
344
|
-
color: var(--vio-color-text-secondary, #666);
|
|
345
|
-
font-weight: 500;
|
|
346
|
-
margin-bottom: 6px;
|
|
347
|
-
}
|
|
348
|
-
.name {
|
|
349
|
-
font-size: var(--vio-size-md, 16px);
|
|
350
|
-
font-weight: 500;
|
|
351
|
-
line-height: 1.3;
|
|
352
|
-
margin: 0 0 10px;
|
|
353
|
-
display: -webkit-box;
|
|
354
|
-
-webkit-line-clamp: 2;
|
|
355
|
-
line-clamp: 2;
|
|
356
|
-
-webkit-box-orient: vertical;
|
|
357
|
-
overflow: hidden;
|
|
358
|
-
}
|
|
359
|
-
.price {
|
|
360
|
-
font-size: var(--vio-size-md, 16px);
|
|
361
|
-
font-weight: 700;
|
|
362
|
-
margin-bottom: 16px;
|
|
363
|
-
}
|
|
364
|
-
.retailer {
|
|
365
|
-
font-size: var(--vio-size-xs, 12px);
|
|
366
|
-
letter-spacing: var(--vio-tracking-label, 0.1em);
|
|
367
|
-
text-transform: uppercase;
|
|
368
|
-
color: var(--vio-color-text-tertiary, #999);
|
|
369
|
-
font-weight: 500;
|
|
370
|
-
margin-top: auto;
|
|
371
|
-
}
|
|
372
|
-
`;
|
|
373
253
|
__decorateClass([
|
|
374
254
|
property({ type: String })
|
|
375
255
|
], VioProduct.prototype, "brand", 2);
|
|
@@ -394,6 +274,15 @@ __decorateClass([
|
|
|
394
274
|
__decorateClass([
|
|
395
275
|
property({ type: Boolean, attribute: "has-variants" })
|
|
396
276
|
], VioProduct.prototype, "hasVariants", 2);
|
|
277
|
+
__decorateClass([
|
|
278
|
+
property({ attribute: false })
|
|
279
|
+
], VioProduct.prototype, "variants", 2);
|
|
280
|
+
__decorateClass([
|
|
281
|
+
property({ attribute: false })
|
|
282
|
+
], VioProduct.prototype, "options", 2);
|
|
283
|
+
__decorateClass([
|
|
284
|
+
property({ type: Boolean, attribute: "hide-meta" })
|
|
285
|
+
], VioProduct.prototype, "hideMeta", 2);
|
|
397
286
|
__decorateClass([
|
|
398
287
|
state()
|
|
399
288
|
], VioProduct.prototype, "inCart", 2);
|
|
@@ -403,9 +292,6 @@ __decorateClass([
|
|
|
403
292
|
__decorateClass([
|
|
404
293
|
state()
|
|
405
294
|
], VioProduct.prototype, "justAdded", 2);
|
|
406
|
-
VioProduct = __decorateClass([
|
|
407
|
-
customElement("vio-product")
|
|
408
|
-
], VioProduct);
|
|
409
295
|
var VioProductCarousel = class extends LitElement {
|
|
410
296
|
constructor() {
|
|
411
297
|
super(...arguments);
|
|
@@ -423,158 +309,23 @@ var VioProductCarousel = class extends LitElement {
|
|
|
423
309
|
this.isLoading = false;
|
|
424
310
|
this.fetchError = "";
|
|
425
311
|
}
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
312
|
+
static {
|
|
313
|
+
this.styles = css`
|
|
314
|
+
:host {
|
|
315
|
+
display: block;
|
|
316
|
+
box-sizing: border-box;
|
|
317
|
+
width: 100%;
|
|
318
|
+
font-family: var(--vio-font-sans, -apple-system, sans-serif);
|
|
319
|
+
color: var(--vio-color-text, #0a0a0a);
|
|
320
|
+
padding: var(--vio-space-xl, 32px) 0;
|
|
430
321
|
}
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
async autoFetch() {
|
|
439
|
-
if (!Vio.isInitialized) {
|
|
440
|
-
this.fetchError = "Vio not initialized \u2014 call Vio.init({ apiKey }) first";
|
|
441
|
-
return;
|
|
442
|
-
}
|
|
443
|
-
this.isLoading = true;
|
|
444
|
-
this.fetchError = "";
|
|
445
|
-
try {
|
|
446
|
-
const refs = this.productRefs.trim() ? this.parseRefs(this.productRefs) : this.productIds.split(",").map((s) => parseInt(s.trim(), 10)).filter((n) => !Number.isNaN(n)).map((id) => ({ sponsorId: this.sponsorId, productId: id }));
|
|
447
|
-
if (refs.length === 0) {
|
|
448
|
-
this.fetched = [];
|
|
449
|
-
return;
|
|
450
|
-
}
|
|
451
|
-
const bySponsor = /* @__PURE__ */ new Map();
|
|
452
|
-
for (const r of refs) {
|
|
453
|
-
const arr = bySponsor.get(r.sponsorId) ?? [];
|
|
454
|
-
arr.push(r.productId);
|
|
455
|
-
bySponsor.set(r.sponsorId, arr);
|
|
456
|
-
}
|
|
457
|
-
const fetched = await Promise.all(
|
|
458
|
-
Array.from(bySponsor.entries()).map(async ([sponsorId, ids]) => {
|
|
459
|
-
const commerce = this.commerceKey && this.productRefs.trim() === "" ? Vio.commerceWithKey(this.commerceKey) : Vio.commerceFor(sponsorId);
|
|
460
|
-
const products = await commerce.channel.product.getByIds({
|
|
461
|
-
product_ids: ids,
|
|
462
|
-
currency: this.currency,
|
|
463
|
-
image_size: "large"
|
|
464
|
-
});
|
|
465
|
-
return { sponsorId, products };
|
|
466
|
-
})
|
|
467
|
-
);
|
|
468
|
-
const lookup = /* @__PURE__ */ new Map();
|
|
469
|
-
for (const { sponsorId, products } of fetched) {
|
|
470
|
-
for (const p of products) lookup.set(`${sponsorId}:${p.id}`, p);
|
|
471
|
-
}
|
|
472
|
-
this.fetched = refs.map(({ sponsorId, productId }) => {
|
|
473
|
-
const product = lookup.get(`${sponsorId}:${productId}`);
|
|
474
|
-
return product ? { product, sponsorId } : null;
|
|
475
|
-
}).filter((x) => x !== null);
|
|
476
|
-
} catch (err) {
|
|
477
|
-
this.fetchError = err instanceof Error ? err.message : String(err);
|
|
478
|
-
} finally {
|
|
479
|
-
this.isLoading = false;
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
parseRefs(raw) {
|
|
483
|
-
return raw.split(",").map((pair) => pair.trim()).filter((s) => s.length > 0).map((pair) => {
|
|
484
|
-
const [sidStr, pidStr] = pair.split(":");
|
|
485
|
-
const sid = parseInt((sidStr ?? "").trim(), 10);
|
|
486
|
-
const pid = parseInt((pidStr ?? "").trim(), 10);
|
|
487
|
-
if (Number.isNaN(sid) || Number.isNaN(pid)) return null;
|
|
488
|
-
return { sponsorId: sid, productId: pid };
|
|
489
|
-
}).filter((x) => x !== null);
|
|
490
|
-
}
|
|
491
|
-
scrollByDirection(direction) {
|
|
492
|
-
if (!this.scroller) return;
|
|
493
|
-
const item = this.scroller.querySelector(":scope > *");
|
|
494
|
-
const step = (item?.offsetWidth ?? 240) + 16;
|
|
495
|
-
this.scroller.scrollBy({ left: direction * step * 2, behavior: "smooth" });
|
|
496
|
-
}
|
|
497
|
-
renderScrollerContent() {
|
|
498
|
-
if (this.isLoading && !this.fetched) {
|
|
499
|
-
return html`${Array.from({ length: 5 }).map(
|
|
500
|
-
() => html`
|
|
501
|
-
<div class="skeleton">
|
|
502
|
-
<div class="skeleton-image"></div>
|
|
503
|
-
<div class="skeleton-text"></div>
|
|
504
|
-
<div class="skeleton-text shorter"></div>
|
|
505
|
-
</div>
|
|
506
|
-
`
|
|
507
|
-
)}`;
|
|
508
|
-
}
|
|
509
|
-
if (this.fetched && this.fetched.length > 0) {
|
|
510
|
-
return html`${this.fetched.map(({ product: p, sponsorId }) => {
|
|
511
|
-
const img = primaryImageUrl(p) ?? "";
|
|
512
|
-
const retailer = sponsorNameFromBootstrap(sponsorId) ?? p.brand ?? "";
|
|
513
|
-
return html`
|
|
514
|
-
<vio-product
|
|
515
|
-
product-id=${String(p.id)}
|
|
516
|
-
sponsor-id=${String(sponsorId)}
|
|
517
|
-
brand=${p.brand ?? ""}
|
|
518
|
-
name=${p.title}
|
|
519
|
-
price=${displayPrice(p)}
|
|
520
|
-
retailer=${retailer}
|
|
521
|
-
image-url=${img}
|
|
522
|
-
?has-variants=${(p.variants?.length ?? 0) > 1}
|
|
523
|
-
></vio-product>
|
|
524
|
-
`;
|
|
525
|
-
})}`;
|
|
526
|
-
}
|
|
527
|
-
return html`<slot></slot>`;
|
|
528
|
-
}
|
|
529
|
-
render() {
|
|
530
|
-
return html`
|
|
531
|
-
<div class="header">
|
|
532
|
-
<div class="header-text">
|
|
533
|
-
${this.label ? html`<div class="label">${this.label}</div>` : ""}
|
|
534
|
-
<div class="heading-row">
|
|
535
|
-
${this.heading ? html`<h2 class="heading">${this.heading}</h2>` : ""}
|
|
536
|
-
${this.disclaimer ? html`<span class="disclaimer">(${this.disclaimer})</span>` : ""}
|
|
537
|
-
</div>
|
|
538
|
-
</div>
|
|
539
|
-
${this.seeAllHref ? html`<a class="see-all" href=${this.seeAllHref}>${this.seeAllLabel}</a>` : ""}
|
|
540
|
-
</div>
|
|
541
|
-
<div class="viewport">
|
|
542
|
-
<button
|
|
543
|
-
class="nav-btn prev"
|
|
544
|
-
@click=${() => this.scrollByDirection(-1)}
|
|
545
|
-
aria-label="Forrige"
|
|
546
|
-
>
|
|
547
|
-
‹
|
|
548
|
-
</button>
|
|
549
|
-
<div class="scroller">${this.renderScrollerContent()}</div>
|
|
550
|
-
<button
|
|
551
|
-
class="nav-btn next"
|
|
552
|
-
@click=${() => this.scrollByDirection(1)}
|
|
553
|
-
aria-label="Neste"
|
|
554
|
-
>
|
|
555
|
-
›
|
|
556
|
-
</button>
|
|
557
|
-
</div>
|
|
558
|
-
${this.fetchError && !this.fetched ? html`<div class="state error">${this.fetchError}</div>` : ""}
|
|
559
|
-
`;
|
|
560
|
-
}
|
|
561
|
-
};
|
|
562
|
-
VioProductCarousel.styles = css`
|
|
563
|
-
:host {
|
|
564
|
-
display: block;
|
|
565
|
-
box-sizing: border-box;
|
|
566
|
-
width: 100%;
|
|
567
|
-
font-family: var(--vio-font-sans, -apple-system, sans-serif);
|
|
568
|
-
color: var(--vio-color-text, #0a0a0a);
|
|
569
|
-
padding: var(--vio-space-xl, 32px) 0;
|
|
570
|
-
}
|
|
571
|
-
.header {
|
|
572
|
-
display: flex;
|
|
573
|
-
align-items: flex-end;
|
|
574
|
-
justify-content: space-between;
|
|
575
|
-
gap: var(--vio-space-md, 16px);
|
|
576
|
-
margin-bottom: var(--vio-space-lg, 24px);
|
|
577
|
-
padding: 0 var(--vio-space-lg, 24px);
|
|
322
|
+
.header {
|
|
323
|
+
display: flex;
|
|
324
|
+
align-items: flex-end;
|
|
325
|
+
justify-content: space-between;
|
|
326
|
+
gap: var(--vio-space-md, 16px);
|
|
327
|
+
margin-bottom: var(--vio-space-lg, 24px);
|
|
328
|
+
padding: 0 var(--vio-space-lg, 24px);
|
|
578
329
|
}
|
|
579
330
|
.header-text { flex: 1; min-width: 0; }
|
|
580
331
|
.label {
|
|
@@ -702,445 +453,234 @@ VioProductCarousel.styles = css`
|
|
|
702
453
|
50% { opacity: 0.5; }
|
|
703
454
|
}
|
|
704
455
|
`;
|
|
705
|
-
__decorateClass([
|
|
706
|
-
property({ type: String })
|
|
707
|
-
], VioProductCarousel.prototype, "label", 2);
|
|
708
|
-
__decorateClass([
|
|
709
|
-
property({ type: String })
|
|
710
|
-
], VioProductCarousel.prototype, "heading", 2);
|
|
711
|
-
__decorateClass([
|
|
712
|
-
property({ type: String })
|
|
713
|
-
], VioProductCarousel.prototype, "disclaimer", 2);
|
|
714
|
-
__decorateClass([
|
|
715
|
-
property({ type: String, attribute: "see-all-href" })
|
|
716
|
-
], VioProductCarousel.prototype, "seeAllHref", 2);
|
|
717
|
-
__decorateClass([
|
|
718
|
-
property({ type: String, attribute: "see-all-label" })
|
|
719
|
-
], VioProductCarousel.prototype, "seeAllLabel", 2);
|
|
720
|
-
__decorateClass([
|
|
721
|
-
property({ type: String, attribute: "product-ids" })
|
|
722
|
-
], VioProductCarousel.prototype, "productIds", 2);
|
|
723
|
-
__decorateClass([
|
|
724
|
-
property({ type: String, attribute: "commerce-key" })
|
|
725
|
-
], VioProductCarousel.prototype, "commerceKey", 2);
|
|
726
|
-
__decorateClass([
|
|
727
|
-
property({ type: Number, attribute: "sponsor-id" })
|
|
728
|
-
], VioProductCarousel.prototype, "sponsorId", 2);
|
|
729
|
-
__decorateClass([
|
|
730
|
-
property({ type: String, attribute: "product-refs" })
|
|
731
|
-
], VioProductCarousel.prototype, "productRefs", 2);
|
|
732
|
-
__decorateClass([
|
|
733
|
-
property({ type: String })
|
|
734
|
-
], VioProductCarousel.prototype, "currency", 2);
|
|
735
|
-
__decorateClass([
|
|
736
|
-
query(".scroller")
|
|
737
|
-
], VioProductCarousel.prototype, "scroller", 2);
|
|
738
|
-
__decorateClass([
|
|
739
|
-
state()
|
|
740
|
-
], VioProductCarousel.prototype, "fetched", 2);
|
|
741
|
-
__decorateClass([
|
|
742
|
-
state()
|
|
743
|
-
], VioProductCarousel.prototype, "isLoading", 2);
|
|
744
|
-
__decorateClass([
|
|
745
|
-
state()
|
|
746
|
-
], VioProductCarousel.prototype, "fetchError", 2);
|
|
747
|
-
VioProductCarousel = __decorateClass([
|
|
748
|
-
customElement("vio-product-carousel")
|
|
749
|
-
], VioProductCarousel);
|
|
750
|
-
function sponsorNameFromBootstrap(sponsorId) {
|
|
751
|
-
const boot = Vio.bootstrapCache;
|
|
752
|
-
if (!boot) return void 0;
|
|
753
|
-
if (boot.primarySponsor?.id === sponsorId) return boot.primarySponsor.name;
|
|
754
|
-
return boot.secondarySponsors?.find((s) => s.id === sponsorId)?.name;
|
|
755
|
-
}
|
|
756
|
-
var VioProductDetail = class extends LitElement {
|
|
757
|
-
constructor() {
|
|
758
|
-
super(...arguments);
|
|
759
|
-
this.open = false;
|
|
760
|
-
this.productId = "";
|
|
761
|
-
this.sponsorId = 0;
|
|
762
|
-
this.autoOpen = true;
|
|
763
|
-
this.currency = "NOK";
|
|
764
|
-
this.product = null;
|
|
765
|
-
this.isLoading = false;
|
|
766
|
-
this.fetchError = "";
|
|
767
|
-
this.selectedOptions = {};
|
|
768
|
-
this.quantity = 1;
|
|
769
|
-
this.activeImageIndex = 0;
|
|
770
|
-
this.adding = false;
|
|
771
|
-
this.applePayOk = false;
|
|
772
|
-
/** Pre-prepared Apple Pay handle — show() fires synchronously on tap. */
|
|
773
|
-
this.applePayHandle = null;
|
|
774
|
-
this.boundProductClick = (e) => {
|
|
775
|
-
if (!this.autoOpen) return;
|
|
776
|
-
const ev = e;
|
|
777
|
-
const productIdNum = parseInt(ev.detail.productId, 10);
|
|
778
|
-
if (Number.isNaN(productIdNum)) return;
|
|
779
|
-
const sponsorIdNum = parseInt(ev.detail.sponsorId, 10);
|
|
780
|
-
if (Number.isNaN(sponsorIdNum) || sponsorIdNum <= 0) return;
|
|
781
|
-
this.productId = ev.detail.productId;
|
|
782
|
-
this.sponsorId = sponsorIdNum;
|
|
783
|
-
this.show();
|
|
784
|
-
void this.fetchProduct();
|
|
785
|
-
};
|
|
786
456
|
}
|
|
787
457
|
connectedCallback() {
|
|
788
458
|
super.connectedCallback();
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
document.removeEventListener("vio:product-click", this.boundProductClick);
|
|
793
|
-
super.disconnectedCallback();
|
|
794
|
-
}
|
|
795
|
-
show() {
|
|
796
|
-
this.open = true;
|
|
459
|
+
if (this.productRefs.trim() || this.productIds.trim()) {
|
|
460
|
+
void this.autoFetch();
|
|
461
|
+
}
|
|
797
462
|
}
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
this.selectedOptions = {};
|
|
804
|
-
this.quantity = 1;
|
|
805
|
-
this.activeImageIndex = 0;
|
|
806
|
-
this.fetchError = "";
|
|
807
|
-
}
|
|
808
|
-
}, 400);
|
|
463
|
+
updated(changed) {
|
|
464
|
+
const propChanged = changed.has("productRefs") || changed.has("productIds") || changed.has("commerceKey") || changed.has("sponsorId");
|
|
465
|
+
if (propChanged && (this.productRefs.trim() || this.productIds.trim())) {
|
|
466
|
+
void this.autoFetch();
|
|
467
|
+
}
|
|
809
468
|
}
|
|
810
|
-
async
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
this.product = null;
|
|
469
|
+
async autoFetch() {
|
|
470
|
+
if (!Vio.isInitialized) {
|
|
471
|
+
this.fetchError = "Vio not initialized \u2014 call Vio.init({ apiKey }) first";
|
|
814
472
|
return;
|
|
815
473
|
}
|
|
816
474
|
this.isLoading = true;
|
|
817
475
|
this.fetchError = "";
|
|
818
476
|
try {
|
|
819
|
-
const
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
})
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
477
|
+
const refs = this.productRefs.trim() ? this.parseRefs(this.productRefs) : this.productIds.split(",").map((s) => parseInt(s.trim(), 10)).filter((n) => !Number.isNaN(n)).map((id) => ({ sponsorId: this.sponsorId, productId: id }));
|
|
478
|
+
if (refs.length === 0) {
|
|
479
|
+
this.fetched = [];
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
const bySponsor = /* @__PURE__ */ new Map();
|
|
483
|
+
for (const r of refs) {
|
|
484
|
+
const arr = bySponsor.get(r.sponsorId) ?? [];
|
|
485
|
+
arr.push(r.productId);
|
|
486
|
+
bySponsor.set(r.sponsorId, arr);
|
|
487
|
+
}
|
|
488
|
+
const fetched = await Promise.all(
|
|
489
|
+
Array.from(bySponsor.entries()).map(async ([sponsorId, ids]) => {
|
|
490
|
+
const commerce = this.commerceKey && this.productRefs.trim() === "" ? Vio.commerceWithKey(this.commerceKey) : Vio.commerceFor(sponsorId);
|
|
491
|
+
const products = await commerce.channel.product.getByIds({
|
|
492
|
+
product_ids: ids,
|
|
493
|
+
currency: this.currency,
|
|
494
|
+
image_size: "large"
|
|
495
|
+
});
|
|
496
|
+
return { sponsorId, products };
|
|
497
|
+
})
|
|
498
|
+
);
|
|
499
|
+
const lookup = /* @__PURE__ */ new Map();
|
|
500
|
+
for (const { sponsorId, products } of fetched) {
|
|
501
|
+
for (const p of products) lookup.set(`${sponsorId}:${p.id}`, p);
|
|
502
|
+
}
|
|
503
|
+
this.fetched = refs.map(({ sponsorId, productId }) => {
|
|
504
|
+
const product = lookup.get(`${sponsorId}:${productId}`);
|
|
505
|
+
return product ? { product, sponsorId } : null;
|
|
506
|
+
}).filter((x) => x !== null);
|
|
507
|
+
} catch (err) {
|
|
508
|
+
this.fetchError = err instanceof Error ? err.message : String(err);
|
|
843
509
|
} finally {
|
|
844
510
|
this.isLoading = false;
|
|
845
511
|
}
|
|
846
512
|
}
|
|
847
|
-
|
|
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;
|
|
513
|
+
parseRefs(raw) {
|
|
514
|
+
return raw.split(",").map((pair) => pair.trim()).filter((s) => s.length > 0).map((pair) => {
|
|
515
|
+
const [sidStr, pidStr] = pair.split(":");
|
|
516
|
+
const sid = parseInt((sidStr ?? "").trim(), 10);
|
|
517
|
+
const pid = parseInt((pidStr ?? "").trim(), 10);
|
|
518
|
+
if (Number.isNaN(sid) || Number.isNaN(pid)) return null;
|
|
519
|
+
return { sponsorId: sid, productId: pid };
|
|
520
|
+
}).filter((x) => x !== null);
|
|
908
521
|
}
|
|
909
|
-
|
|
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
|
-
}
|
|
522
|
+
scrollByDirection(direction) {
|
|
523
|
+
if (!this.scroller) return;
|
|
524
|
+
const item = this.scroller.querySelector(":scope > *");
|
|
525
|
+
const step = (item?.offsetWidth ?? 240) + 16;
|
|
526
|
+
this.scroller.scrollBy({ left: direction * step * 2, behavior: "smooth" });
|
|
930
527
|
}
|
|
931
|
-
|
|
932
|
-
|
|
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;
|
|
528
|
+
renderScrollerContent() {
|
|
529
|
+
if (this.isLoading && !this.fetched) {
|
|
530
|
+
return html`${Array.from({ length: 5 }).map(
|
|
531
|
+
() => html`
|
|
532
|
+
<div class="skeleton">
|
|
533
|
+
<div class="skeleton-image"></div>
|
|
534
|
+
<div class="skeleton-text"></div>
|
|
535
|
+
<div class="skeleton-text shorter"></div>
|
|
536
|
+
</div>
|
|
537
|
+
`
|
|
538
|
+
)}`;
|
|
952
539
|
}
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
540
|
+
if (this.fetched && this.fetched.length > 0) {
|
|
541
|
+
return html`${this.fetched.map(({ product: p, sponsorId }) => {
|
|
542
|
+
const img = primaryImageUrl(p) ?? "";
|
|
543
|
+
const retailer = sponsorNameFromBootstrap(sponsorId) ?? p.brand ?? "";
|
|
544
|
+
return html`
|
|
545
|
+
<vio-product
|
|
546
|
+
product-id=${String(p.id)}
|
|
547
|
+
sponsor-id=${String(sponsorId)}
|
|
548
|
+
brand=${p.brand ?? ""}
|
|
549
|
+
name=${p.title}
|
|
550
|
+
price=${displayPrice(p)}
|
|
551
|
+
retailer=${retailer}
|
|
552
|
+
image-url=${img}
|
|
553
|
+
?has-variants=${(p.variants?.length ?? 0) > 1}
|
|
554
|
+
></vio-product>
|
|
555
|
+
`;
|
|
556
|
+
})}`;
|
|
970
557
|
}
|
|
971
|
-
|
|
972
|
-
this.close();
|
|
558
|
+
return html`<slot></slot>`;
|
|
973
559
|
}
|
|
974
560
|
render() {
|
|
975
561
|
return html`
|
|
976
|
-
<div class="
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
562
|
+
<div class="header">
|
|
563
|
+
<div class="header-text">
|
|
564
|
+
${this.label ? html`<div class="label">${this.label}</div>` : ""}
|
|
565
|
+
<div class="heading-row">
|
|
566
|
+
${this.heading ? html`<h2 class="heading">${this.heading}</h2>` : ""}
|
|
567
|
+
${this.disclaimer ? html`<span class="disclaimer">(${this.disclaimer})</span>` : ""}
|
|
568
|
+
</div>
|
|
982
569
|
</div>
|
|
983
|
-
|
|
984
|
-
|
|
570
|
+
${this.seeAllHref ? html`<a class="see-all" href=${this.seeAllHref}>${this.seeAllLabel}</a>` : ""}
|
|
571
|
+
</div>
|
|
572
|
+
<div class="viewport">
|
|
573
|
+
<button
|
|
574
|
+
class="nav-btn prev"
|
|
575
|
+
@click=${() => this.scrollByDirection(-1)}
|
|
576
|
+
aria-label="Forrige"
|
|
577
|
+
>
|
|
578
|
+
‹
|
|
579
|
+
</button>
|
|
580
|
+
<div class="scroller">${this.renderScrollerContent()}</div>
|
|
581
|
+
<button
|
|
582
|
+
class="nav-btn next"
|
|
583
|
+
@click=${() => this.scrollByDirection(1)}
|
|
584
|
+
aria-label="Neste"
|
|
585
|
+
>
|
|
586
|
+
›
|
|
587
|
+
</button>
|
|
985
588
|
</div>
|
|
589
|
+
${this.fetchError && !this.fetched ? html`<div class="state error">${this.fetchError}</div>` : ""}
|
|
986
590
|
`;
|
|
987
591
|
}
|
|
988
|
-
renderBody() {
|
|
989
|
-
if (this.isLoading && !this.product) {
|
|
990
|
-
return html`
|
|
991
|
-
<div class="skeleton" role="status" aria-busy="true" aria-label="Laster produkt">
|
|
992
|
-
<div class="sk-image"></div>
|
|
993
|
-
<div class="sk-line sk-brand"></div>
|
|
994
|
-
<div class="sk-line sk-title"></div>
|
|
995
|
-
<div class="sk-line sk-title-2"></div>
|
|
996
|
-
<div class="sk-line sk-price"></div>
|
|
997
|
-
<div class="sk-button"></div>
|
|
998
|
-
</div>
|
|
999
|
-
`;
|
|
1000
|
-
}
|
|
1001
|
-
if (this.fetchError) {
|
|
1002
|
-
return html`<div class="error">Kunne ikke laste produkt: ${this.fetchError}</div>`;
|
|
1003
|
-
}
|
|
1004
|
-
if (!this.product) return html``;
|
|
1005
|
-
const p = this.product;
|
|
1006
|
-
const stock = this.stockLabel();
|
|
1007
|
-
const compare = this.compareAt;
|
|
1008
|
-
const showCompare = compare !== null && compare > this.unitPrice;
|
|
1009
|
-
const images = this.allImages();
|
|
1010
|
-
const activeImage = this.activeImage;
|
|
1011
|
-
return html`
|
|
1012
|
-
<div class="body">
|
|
1013
|
-
<div class="gallery">
|
|
1014
|
-
<div class="gallery-main">
|
|
1015
|
-
${activeImage?.url ? html`<img src=${activeImage.url} alt=${p.title} />` : ""}
|
|
1016
|
-
</div>
|
|
1017
|
-
${images.length > 1 ? html`
|
|
1018
|
-
<div class="gallery-thumbs">
|
|
1019
|
-
${images.map(
|
|
1020
|
-
(img, idx) => html`
|
|
1021
|
-
<button
|
|
1022
|
-
class="gallery-thumb ${idx === this.activeImageIndex ? "active" : ""}"
|
|
1023
|
-
@click=${() => this.activeImageIndex = idx}
|
|
1024
|
-
aria-label=${`Bilde ${idx + 1}`}
|
|
1025
|
-
>
|
|
1026
|
-
<img src=${img.url} alt="" />
|
|
1027
|
-
</button>
|
|
1028
|
-
`
|
|
1029
|
-
)}
|
|
1030
|
-
</div>
|
|
1031
|
-
` : ""}
|
|
1032
|
-
</div>
|
|
1033
|
-
|
|
1034
|
-
<div class="info">
|
|
1035
|
-
${p.brand ? html`<div class="brand">${p.brand}</div>` : ""}
|
|
1036
|
-
<h2 class="name">${p.title}</h2>
|
|
1037
|
-
|
|
1038
|
-
<div class="price-row">
|
|
1039
|
-
<span class="price">${formatPrice(this.unitPrice, p.price.currency_code)}</span>
|
|
1040
|
-
${showCompare ? html`<span class="compare-at">${formatPrice(
|
|
1041
|
-
compare,
|
|
1042
|
-
p.price.currency_code
|
|
1043
|
-
)}</span>` : ""}
|
|
1044
|
-
</div>
|
|
1045
|
-
|
|
1046
|
-
<div class="stock ${stock.cls}">${stock.text}</div>
|
|
1047
|
-
|
|
1048
|
-
${this.renderOptionPickers(p.options)}
|
|
1049
|
-
|
|
1050
|
-
<div class="qty-cta-row">
|
|
1051
|
-
<div class="qty-stepper">
|
|
1052
|
-
<button
|
|
1053
|
-
class="qty-btn"
|
|
1054
|
-
@click=${() => this.quantity = Math.max(1, this.quantity - 1)}
|
|
1055
|
-
aria-label="Reduser antall"
|
|
1056
|
-
>
|
|
1057
|
-
−
|
|
1058
|
-
</button>
|
|
1059
|
-
<span class="qty-value">${this.quantity}</span>
|
|
1060
|
-
<button
|
|
1061
|
-
class="qty-btn"
|
|
1062
|
-
@click=${() => this.quantity = Math.min(this.availableQuantity, this.quantity + 1)}
|
|
1063
|
-
aria-label="Øk antall"
|
|
1064
|
-
?disabled=${this.quantity >= this.availableQuantity}
|
|
1065
|
-
>
|
|
1066
|
-
+
|
|
1067
|
-
</button>
|
|
1068
|
-
</div>
|
|
1069
|
-
<button
|
|
1070
|
-
class="add-to-cart"
|
|
1071
|
-
@click=${this.addToCart}
|
|
1072
|
-
?disabled=${this.availableQuantity <= 0 || this.adding}
|
|
1073
|
-
>
|
|
1074
|
-
${this.adding ? "Legger til\u2026" : this.availableQuantity <= 0 ? "Utsolgt" : "Legg i handlekurv"}
|
|
1075
|
-
</button>
|
|
1076
|
-
</div>
|
|
1077
|
-
|
|
1078
|
-
${this.availableQuantity > 0 && this.applePayOk ? html`
|
|
1079
|
-
<button
|
|
1080
|
-
class="buy-applepay"
|
|
1081
|
-
@click=${this.buyWithApplePay}
|
|
1082
|
-
?disabled=${this.adding}
|
|
1083
|
-
aria-label="Kjøp nå med Apple Pay"
|
|
1084
|
-
>
|
|
1085
|
-
Kjøp nå med Pay
|
|
1086
|
-
</button>
|
|
1087
|
-
` : ""}
|
|
1088
|
-
${this.availableQuantity > 0 ? html`
|
|
1089
|
-
<button
|
|
1090
|
-
class="buy-klarna"
|
|
1091
|
-
@click=${this.buyWithKlarna}
|
|
1092
|
-
?disabled=${this.adding}
|
|
1093
|
-
>
|
|
1094
|
-
<span>Kjøp nå med</span>
|
|
1095
|
-
<img
|
|
1096
|
-
class="klarna-badge"
|
|
1097
|
-
src="https://x.klarnacdn.net/payment-method/assets/badges/generic/klarna.svg"
|
|
1098
|
-
alt="Klarna"
|
|
1099
|
-
/>
|
|
1100
|
-
</button>
|
|
1101
|
-
` : ""}
|
|
1102
|
-
|
|
1103
|
-
${p.description ? html`
|
|
1104
|
-
<div>
|
|
1105
|
-
<div class="description-label">Beskrivelse</div>
|
|
1106
|
-
<div class="description">${this.stripHtml(p.description)}</div>
|
|
1107
|
-
</div>
|
|
1108
|
-
` : ""}
|
|
1109
|
-
</div>
|
|
1110
|
-
</div>
|
|
1111
|
-
`;
|
|
1112
|
-
}
|
|
1113
|
-
renderOptionPickers(options) {
|
|
1114
|
-
if (options.length === 0) return "";
|
|
1115
|
-
return html`${options.map((opt) => {
|
|
1116
|
-
const values = normalizeValues(opt.values);
|
|
1117
|
-
if (values.length === 0) return "";
|
|
1118
|
-
return html`
|
|
1119
|
-
<div class="option-group">
|
|
1120
|
-
<div class="option-label">${opt.name}</div>
|
|
1121
|
-
<div class="option-values">
|
|
1122
|
-
${values.map(
|
|
1123
|
-
(val) => html`
|
|
1124
|
-
<button
|
|
1125
|
-
class="option-pill"
|
|
1126
|
-
@click=${() => this.selectOption(opt.name, val)}
|
|
1127
|
-
aria-pressed=${this.selectedOptions[opt.name] === val}
|
|
1128
|
-
>
|
|
1129
|
-
${val}
|
|
1130
|
-
</button>
|
|
1131
|
-
`
|
|
1132
|
-
)}
|
|
1133
|
-
</div>
|
|
1134
|
-
</div>
|
|
1135
|
-
`;
|
|
1136
|
-
})}`;
|
|
1137
|
-
}
|
|
1138
|
-
/** Best-effort HTML strip for description. */
|
|
1139
|
-
stripHtml(html6) {
|
|
1140
|
-
return html6.replace(/<br\s*\/?>/gi, "\n").replace(/<\/p>/gi, "\n\n").replace(/<[^>]+>/g, "").replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").trim();
|
|
1141
|
-
}
|
|
1142
592
|
};
|
|
1143
|
-
|
|
593
|
+
__decorateClass([
|
|
594
|
+
property({ type: String })
|
|
595
|
+
], VioProductCarousel.prototype, "label", 2);
|
|
596
|
+
__decorateClass([
|
|
597
|
+
property({ type: String })
|
|
598
|
+
], VioProductCarousel.prototype, "heading", 2);
|
|
599
|
+
__decorateClass([
|
|
600
|
+
property({ type: String })
|
|
601
|
+
], VioProductCarousel.prototype, "disclaimer", 2);
|
|
602
|
+
__decorateClass([
|
|
603
|
+
property({ type: String, attribute: "see-all-href" })
|
|
604
|
+
], VioProductCarousel.prototype, "seeAllHref", 2);
|
|
605
|
+
__decorateClass([
|
|
606
|
+
property({ type: String, attribute: "see-all-label" })
|
|
607
|
+
], VioProductCarousel.prototype, "seeAllLabel", 2);
|
|
608
|
+
__decorateClass([
|
|
609
|
+
property({ type: String, attribute: "product-ids" })
|
|
610
|
+
], VioProductCarousel.prototype, "productIds", 2);
|
|
611
|
+
__decorateClass([
|
|
612
|
+
property({ type: String, attribute: "commerce-key" })
|
|
613
|
+
], VioProductCarousel.prototype, "commerceKey", 2);
|
|
614
|
+
__decorateClass([
|
|
615
|
+
property({ type: Number, attribute: "sponsor-id" })
|
|
616
|
+
], VioProductCarousel.prototype, "sponsorId", 2);
|
|
617
|
+
__decorateClass([
|
|
618
|
+
property({ type: String, attribute: "product-refs" })
|
|
619
|
+
], VioProductCarousel.prototype, "productRefs", 2);
|
|
620
|
+
__decorateClass([
|
|
621
|
+
property({ type: String })
|
|
622
|
+
], VioProductCarousel.prototype, "currency", 2);
|
|
623
|
+
__decorateClass([
|
|
624
|
+
query(".scroller")
|
|
625
|
+
], VioProductCarousel.prototype, "scroller", 2);
|
|
626
|
+
__decorateClass([
|
|
627
|
+
state()
|
|
628
|
+
], VioProductCarousel.prototype, "fetched", 2);
|
|
629
|
+
__decorateClass([
|
|
630
|
+
state()
|
|
631
|
+
], VioProductCarousel.prototype, "isLoading", 2);
|
|
632
|
+
__decorateClass([
|
|
633
|
+
state()
|
|
634
|
+
], VioProductCarousel.prototype, "fetchError", 2);
|
|
635
|
+
function sponsorNameFromBootstrap(sponsorId) {
|
|
636
|
+
const boot = Vio.bootstrapCache;
|
|
637
|
+
if (!boot) return void 0;
|
|
638
|
+
if (boot.primarySponsor?.id === sponsorId) return boot.primarySponsor.name;
|
|
639
|
+
return boot.secondarySponsors?.find((s) => s.id === sponsorId)?.name;
|
|
640
|
+
}
|
|
641
|
+
var STRIPE_LOGO_SRC = "data:image/svg+xml;utf8," + encodeURIComponent(
|
|
642
|
+
'<svg width="360" height="150" viewBox="0 0 360 150" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M360 77.4001C360 51.8001 347.6 31.6001 323.9 31.6001C300.1 31.6001 285.7 51.8001 285.7 77.2001C285.7 107.3 302.7 122.5 327.1 122.5C339 122.5 348 119.8 354.8 116V96.0001C348 99.4001 340.2 101.5 330.3 101.5C320.6 101.5 312 98.1001 310.9 86.3001H359.8C359.8 85.0001 360 79.8001 360 77.4001ZM310.6 67.9001C310.6 56.6001 317.5 51.9001 323.8 51.9001C329.9 51.9001 336.4 56.6001 336.4 67.9001H310.6Z" fill="#533AFD"/><path fill-rule="evenodd" clip-rule="evenodd" d="M247.1 31.6001C237.3 31.6001 231 36.2001 227.5 39.4001L226.2 33.2001H204.2V149.8L229.2 144.5L229.3 116.2C232.9 118.8 238.2 122.5 247 122.5C264.9 122.5 281.2 108.1 281.2 76.4001C281.1 47.4001 264.6 31.6001 247.1 31.6001ZM241.1 100.5C235.2 100.5 231.7 98.4001 229.3 95.8001L229.2 58.7001C231.8 55.8001 235.4 53.8001 241.1 53.8001C250.2 53.8001 256.5 64.0001 256.5 77.1001C256.5 90.5001 250.3 100.5 241.1 100.5Z" fill="#533AFD"/><path fill-rule="evenodd" clip-rule="evenodd" d="M169.8 25.7L194.9 20.3V0L169.8 5.3V25.7Z" fill="#533AFD"/><path d="M194.9 33.3H169.8V120.8H194.9V33.3Z" fill="#533AFD"/><path fill-rule="evenodd" clip-rule="evenodd" d="M142.9 40.7L141.3 33.3H119.7V120.8H144.7V61.5C150.6 53.8 160.6 55.2 163.7 56.3V33.3C160.5 32.1 148.8 29.9 142.9 40.7Z" fill="#533AFD"/><path fill-rule="evenodd" clip-rule="evenodd" d="M92.8999 11.6001L68.4999 16.8001L68.3999 96.9001C68.3999 111.7 79.4999 122.6 94.2999 122.6C102.5 122.6 108.5 121.1 111.8 119.3V99.0001C108.6 100.3 92.7999 104.9 92.7999 90.1001V54.6001H111.8V33.3001H92.7999L92.8999 11.6001Z" fill="#533AFD"/><path fill-rule="evenodd" clip-rule="evenodd" d="M25.3 58.7001C25.3 54.8001 28.5 53.3001 33.8 53.3001C41.4 53.3001 51 55.6001 58.6 59.7001V36.2001C50.3 32.9001 42.1 31.6001 33.8 31.6001C13.5 31.6001 0 42.2001 0 59.9001C0 87.5001 38 83.1001 38 95.0001C38 99.6001 34 101.1 28.4 101.1C20.1 101.1 9.5 97.7001 1.1 93.1001V116.9C10.4 120.9 19.8 122.6 28.4 122.6C49.2 122.6 63.5 112.3 63.5 94.4001C63.4 64.6001 25.3 69.9001 25.3 58.7001Z" fill="#533AFD"/></svg>'
|
|
643
|
+
);
|
|
644
|
+
var VioProductDetail = class extends LitElement {
|
|
645
|
+
constructor() {
|
|
646
|
+
super(...arguments);
|
|
647
|
+
this.open = false;
|
|
648
|
+
this.productId = "";
|
|
649
|
+
this.sponsorId = 0;
|
|
650
|
+
this.autoOpen = true;
|
|
651
|
+
this.currency = "NOK";
|
|
652
|
+
this.product = null;
|
|
653
|
+
this.isLoading = false;
|
|
654
|
+
this.fetchError = "";
|
|
655
|
+
this.selectedOptions = {};
|
|
656
|
+
this.quantity = 1;
|
|
657
|
+
this.activeImageIndex = 0;
|
|
658
|
+
this.adding = false;
|
|
659
|
+
this.applePayOk = false;
|
|
660
|
+
this.availableMethods = null;
|
|
661
|
+
/** Race guard: only the latest fetch may write state (two quick taps on
|
|
662
|
+
* different cards would otherwise let the slow response win). */
|
|
663
|
+
this.fetchSeq = 0;
|
|
664
|
+
/** Memoised option maps per variant object (recomputed per product). */
|
|
665
|
+
this.optMapCache = /* @__PURE__ */ new WeakMap();
|
|
666
|
+
/** Pre-prepared Apple Pay handle — show() fires synchronously on tap. */
|
|
667
|
+
this.applePayHandle = null;
|
|
668
|
+
this.boundProductClick = (e) => {
|
|
669
|
+
if (!this.autoOpen) return;
|
|
670
|
+
const ev = e;
|
|
671
|
+
const productIdNum = parseInt(ev.detail.productId, 10);
|
|
672
|
+
if (Number.isNaN(productIdNum)) return;
|
|
673
|
+
const sponsorIdNum = parseInt(ev.detail.sponsorId, 10);
|
|
674
|
+
if (Number.isNaN(sponsorIdNum) || sponsorIdNum <= 0) return;
|
|
675
|
+
this.productId = ev.detail.productId;
|
|
676
|
+
this.sponsorId = sponsorIdNum;
|
|
677
|
+
this.currency = ev.detail.currency || getGlobalCurrency();
|
|
678
|
+
this.show();
|
|
679
|
+
void this.fetchProduct();
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
static {
|
|
683
|
+
this.styles = css`
|
|
1144
684
|
:host { display: contents; }
|
|
1145
685
|
|
|
1146
686
|
.backdrop {
|
|
@@ -1373,6 +913,13 @@ VioProductDetail.styles = css`
|
|
|
1373
913
|
background: var(--vio-color-text, #0a0a0a);
|
|
1374
914
|
color: var(--vio-color-text-on-primary, #fff);
|
|
1375
915
|
}
|
|
916
|
+
.option-pill.unavailable {
|
|
917
|
+
opacity: 0.4;
|
|
918
|
+
cursor: not-allowed;
|
|
919
|
+
text-decoration: line-through;
|
|
920
|
+
background: repeating-linear-gradient(-45deg, #fafafa, #fafafa 5px, #f0f0f0 5px, #f0f0f0 10px);
|
|
921
|
+
}
|
|
922
|
+
.option-pill.unavailable:hover { border-color: var(--vio-color-border, #e5e5e5); }
|
|
1376
923
|
|
|
1377
924
|
/* Qty + CTA row */
|
|
1378
925
|
.qty-cta-row {
|
|
@@ -1443,6 +990,35 @@ VioProductDetail.styles = css`
|
|
|
1443
990
|
.buy-klarna .klarna-badge { height: 22px; width: auto; display: block; }
|
|
1444
991
|
.buy-klarna:hover:not(:disabled) { background: #ffa0b9; }
|
|
1445
992
|
.buy-klarna:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
993
|
+
.buy-applepay .ap-logo { width: 20px; height: 20px; }
|
|
994
|
+
/* Vipps — brand orange buy button. */
|
|
995
|
+
.buy-vipps {
|
|
996
|
+
width: 100%;
|
|
997
|
+
margin-top: 10px;
|
|
998
|
+
display: flex;
|
|
999
|
+
align-items: center;
|
|
1000
|
+
justify-content: center;
|
|
1001
|
+
gap: 8px;
|
|
1002
|
+
background: #ff5b24;
|
|
1003
|
+
color: #fff;
|
|
1004
|
+
border: none;
|
|
1005
|
+
padding: 14px 24px;
|
|
1006
|
+
font-size: 12px;
|
|
1007
|
+
text-transform: uppercase;
|
|
1008
|
+
letter-spacing: var(--vio-tracking-label, 0.1em);
|
|
1009
|
+
font-weight: 700;
|
|
1010
|
+
cursor: pointer;
|
|
1011
|
+
font-family: inherit;
|
|
1012
|
+
transition: background 0.15s;
|
|
1013
|
+
}
|
|
1014
|
+
.buy-vipps .vipps-badge {
|
|
1015
|
+
font-size: 20px;
|
|
1016
|
+
font-weight: 800;
|
|
1017
|
+
letter-spacing: -0.02em;
|
|
1018
|
+
text-transform: none;
|
|
1019
|
+
}
|
|
1020
|
+
.buy-vipps:hover:not(:disabled) { background: #ec4f1c; }
|
|
1021
|
+
.buy-vipps:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
1446
1022
|
/* Apple Pay express button — black, per Apple's button guidelines. Only
|
|
1447
1023
|
shown in Safari (canApplePay). The glyph is the Apple logo (Safari). */
|
|
1448
1024
|
.buy-applepay {
|
|
@@ -1524,329 +1100,761 @@ VioProductDetail.styles = css`
|
|
|
1524
1100
|
font-size: 14px;
|
|
1525
1101
|
}
|
|
1526
1102
|
`;
|
|
1527
|
-
__decorateClass([
|
|
1528
|
-
property({ type: Boolean, reflect: true })
|
|
1529
|
-
], VioProductDetail.prototype, "open", 2);
|
|
1530
|
-
__decorateClass([
|
|
1531
|
-
property({ type: String, attribute: "product-id" })
|
|
1532
|
-
], VioProductDetail.prototype, "productId", 2);
|
|
1533
|
-
__decorateClass([
|
|
1534
|
-
property({ type: Number, attribute: "sponsor-id" })
|
|
1535
|
-
], VioProductDetail.prototype, "sponsorId", 2);
|
|
1536
|
-
__decorateClass([
|
|
1537
|
-
property({ type: Boolean, attribute: "auto-open" })
|
|
1538
|
-
], VioProductDetail.prototype, "autoOpen", 2);
|
|
1539
|
-
__decorateClass([
|
|
1540
|
-
property({ type: String })
|
|
1541
|
-
], VioProductDetail.prototype, "currency", 2);
|
|
1542
|
-
__decorateClass([
|
|
1543
|
-
state()
|
|
1544
|
-
], VioProductDetail.prototype, "product", 2);
|
|
1545
|
-
__decorateClass([
|
|
1546
|
-
state()
|
|
1547
|
-
], VioProductDetail.prototype, "isLoading", 2);
|
|
1548
|
-
__decorateClass([
|
|
1549
|
-
state()
|
|
1550
|
-
], VioProductDetail.prototype, "fetchError", 2);
|
|
1551
|
-
__decorateClass([
|
|
1552
|
-
state()
|
|
1553
|
-
], VioProductDetail.prototype, "selectedOptions", 2);
|
|
1554
|
-
__decorateClass([
|
|
1555
|
-
state()
|
|
1556
|
-
], VioProductDetail.prototype, "quantity", 2);
|
|
1557
|
-
__decorateClass([
|
|
1558
|
-
state()
|
|
1559
|
-
], VioProductDetail.prototype, "activeImageIndex", 2);
|
|
1560
|
-
__decorateClass([
|
|
1561
|
-
state()
|
|
1562
|
-
], VioProductDetail.prototype, "adding", 2);
|
|
1563
|
-
__decorateClass([
|
|
1564
|
-
state()
|
|
1565
|
-
], VioProductDetail.prototype, "applePayOk", 2);
|
|
1566
|
-
VioProductDetail = __decorateClass([
|
|
1567
|
-
customElement("vio-product-detail")
|
|
1568
|
-
], VioProductDetail);
|
|
1569
|
-
function normalizeValues(raw) {
|
|
1570
|
-
if (Array.isArray(raw)) {
|
|
1571
|
-
return raw.map((v) => String(v).trim()).filter((v) => v.length > 0);
|
|
1572
1103
|
}
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
return [];
|
|
1577
|
-
}
|
|
1578
|
-
var VioCart = class extends LitElement {
|
|
1579
|
-
constructor() {
|
|
1580
|
-
super(...arguments);
|
|
1581
|
-
this.open = false;
|
|
1582
|
-
this.heading = "Handlekurv";
|
|
1583
|
-
this.checkoutLabel = "Til kassen";
|
|
1584
|
-
this.emptyLabel = "Handlekurven er tom";
|
|
1585
|
-
this.subtotalLabel = "Sum";
|
|
1586
|
-
this.carts = /* @__PURE__ */ new Map();
|
|
1587
|
-
this.itemCount = 0;
|
|
1588
|
-
this.applePayAvailable = false;
|
|
1589
|
-
this.confirmedOrder = null;
|
|
1590
|
-
/** Pre-resolved Apple Pay handle; `show()` fires the sheet synchronously. */
|
|
1591
|
-
this.applePayHandle = null;
|
|
1592
|
-
/** Cart total Apple Pay was last prepared for (avoids redundant prep). */
|
|
1593
|
-
this.preparedTotal = -1;
|
|
1594
|
-
this.boundOnCartChange = (e) => {
|
|
1595
|
-
const detail = e.detail;
|
|
1596
|
-
this.carts = detail.cartsBySponsor;
|
|
1597
|
-
this.itemCount = detail.itemCount;
|
|
1598
|
-
};
|
|
1599
|
-
}
|
|
1600
|
-
connectedCallback() {
|
|
1601
|
-
super.connectedCallback();
|
|
1602
|
-
this.carts = Vio.cart.getAllCarts();
|
|
1603
|
-
this.itemCount = Vio.cart.itemCount;
|
|
1604
|
-
Vio.cart.addEventListener("change", this.boundOnCartChange);
|
|
1104
|
+
connectedCallback() {
|
|
1105
|
+
super.connectedCallback();
|
|
1106
|
+
document.addEventListener("vio:product-click", this.boundProductClick);
|
|
1605
1107
|
}
|
|
1606
1108
|
disconnectedCallback() {
|
|
1607
|
-
|
|
1109
|
+
document.removeEventListener("vio:product-click", this.boundProductClick);
|
|
1608
1110
|
super.disconnectedCallback();
|
|
1609
1111
|
}
|
|
1610
|
-
close() {
|
|
1611
|
-
this.open = false;
|
|
1612
|
-
}
|
|
1613
1112
|
show() {
|
|
1614
1113
|
this.open = true;
|
|
1615
1114
|
}
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
this.
|
|
1115
|
+
close() {
|
|
1116
|
+
this.open = false;
|
|
1117
|
+
setTimeout(() => {
|
|
1118
|
+
if (!this.open) {
|
|
1119
|
+
this.product = null;
|
|
1120
|
+
this.selectedOptions = {};
|
|
1121
|
+
this.quantity = 1;
|
|
1122
|
+
this.activeImageIndex = 0;
|
|
1123
|
+
this.fetchError = "";
|
|
1124
|
+
}
|
|
1125
|
+
}, 400);
|
|
1126
|
+
}
|
|
1127
|
+
async fetchProduct() {
|
|
1128
|
+
const productIdNum = parseInt(this.productId, 10);
|
|
1129
|
+
if (Number.isNaN(productIdNum) || !this.sponsorId) {
|
|
1130
|
+
this.product = null;
|
|
1131
|
+
return;
|
|
1620
1132
|
}
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1133
|
+
const seq = ++this.fetchSeq;
|
|
1134
|
+
this.isLoading = true;
|
|
1135
|
+
this.fetchError = "";
|
|
1136
|
+
let products = null;
|
|
1137
|
+
let lastError = null;
|
|
1138
|
+
for (let attempt = 0; attempt < 4; attempt++) {
|
|
1139
|
+
try {
|
|
1140
|
+
const activeCurrency = getGlobalCurrency() || this.currency;
|
|
1141
|
+
this.currency = activeCurrency;
|
|
1142
|
+
const commerce = Vio.commerceFor(this.sponsorId);
|
|
1143
|
+
products = await commerce.channel.product.getByIds({
|
|
1144
|
+
product_ids: [productIdNum],
|
|
1145
|
+
currency: activeCurrency,
|
|
1146
|
+
image_size: "large"
|
|
1147
|
+
});
|
|
1148
|
+
lastError = null;
|
|
1149
|
+
break;
|
|
1150
|
+
} catch (err) {
|
|
1151
|
+
lastError = err;
|
|
1152
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
1626
1153
|
}
|
|
1627
1154
|
}
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
this.
|
|
1633
|
-
this.applePayAvailable = false;
|
|
1155
|
+
if (seq !== this.fetchSeq) return;
|
|
1156
|
+
if (lastError) {
|
|
1157
|
+
this.fetchError = lastError instanceof Error ? lastError.message : String(lastError);
|
|
1158
|
+
console.warn("[VioDetail] fetch failed after retries \u2192", this.fetchError);
|
|
1159
|
+
this.isLoading = false;
|
|
1634
1160
|
return;
|
|
1635
1161
|
}
|
|
1162
|
+
this.optMapCache = /* @__PURE__ */ new WeakMap();
|
|
1163
|
+
this.product = products?.[0] ?? null;
|
|
1164
|
+
if (!this.product) {
|
|
1165
|
+
this.fetchError = "Fant ikke produktet.";
|
|
1166
|
+
}
|
|
1167
|
+
if (this.product) {
|
|
1168
|
+
const initial = {};
|
|
1169
|
+
for (const opt of this.product.options ?? []) {
|
|
1170
|
+
const values = normalizeValues(opt.values);
|
|
1171
|
+
if (values.length > 0) initial[opt.name] = values[0];
|
|
1172
|
+
}
|
|
1173
|
+
this.selectedOptions = initial;
|
|
1174
|
+
void Vio.checkout.prepareApplePayFor(this.unitPrice * this.quantity, this.currency).then((handle) => {
|
|
1175
|
+
this.applePayHandle = handle;
|
|
1176
|
+
this.applePayOk = handle.available;
|
|
1177
|
+
}).catch(() => {
|
|
1178
|
+
this.applePayHandle = null;
|
|
1179
|
+
this.applePayOk = false;
|
|
1180
|
+
});
|
|
1181
|
+
void this.loadAvailablePaymentMethods();
|
|
1182
|
+
}
|
|
1183
|
+
this.isLoading = false;
|
|
1184
|
+
}
|
|
1185
|
+
/** Which buy buttons to render — backend-configured per sponsor. */
|
|
1186
|
+
async loadAvailablePaymentMethods() {
|
|
1187
|
+
const spId = this.sponsorId || globalThis.__VIO_SPONSOR_ID__;
|
|
1188
|
+
if (!spId) return;
|
|
1636
1189
|
try {
|
|
1637
|
-
const
|
|
1638
|
-
if (
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1190
|
+
const methods = await Vio.checkout.getAvailablePaymentMethods(spId);
|
|
1191
|
+
if (Array.isArray(methods)) {
|
|
1192
|
+
this.availableMethods = methods.map((m) => m.name);
|
|
1193
|
+
} else {
|
|
1194
|
+
this.availableMethods = null;
|
|
1195
|
+
}
|
|
1196
|
+
} catch (err) {
|
|
1197
|
+
if (typeof console !== "undefined") {
|
|
1198
|
+
console.warn("[VioProductDetail] Failed to load payment methods:", err);
|
|
1199
|
+
}
|
|
1200
|
+
this.availableMethods = null;
|
|
1644
1201
|
}
|
|
1645
1202
|
}
|
|
1646
|
-
/**
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1203
|
+
/** Backend names arrive as e.g. "Apple Pay" — compare letters only. */
|
|
1204
|
+
methodEnabled(...names) {
|
|
1205
|
+
if (this.availableMethods === null) return true;
|
|
1206
|
+
const norm = (s) => s.toLowerCase().replace(/[^a-z]/g, "");
|
|
1207
|
+
return this.availableMethods.some((m) => names.some((n) => norm(m) === norm(n)));
|
|
1208
|
+
}
|
|
1209
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
1210
|
+
/**
|
|
1211
|
+
* Build an option-name → value map for a variant, from every shape the feeds
|
|
1212
|
+
* ship: option1/2/3 columns, options/selected_options/variant arrays, plain
|
|
1213
|
+
* object maps, and finally the title (value scan first, position fallback).
|
|
1214
|
+
*/
|
|
1215
|
+
getVariantOptionMap(v, productOptions) {
|
|
1216
|
+
if (v && typeof v === "object") {
|
|
1217
|
+
const cached = this.optMapCache.get(v);
|
|
1218
|
+
if (cached) return cached;
|
|
1219
|
+
}
|
|
1220
|
+
const map = {};
|
|
1221
|
+
if (!v) return { map, titleParts: [] };
|
|
1222
|
+
if (v.option1 != null && productOptions?.[0]) {
|
|
1223
|
+
map[productOptions[0].name.toLowerCase()] = String(v.option1).trim().toLowerCase();
|
|
1224
|
+
}
|
|
1225
|
+
if (v.option2 != null && productOptions?.[1]) {
|
|
1226
|
+
map[productOptions[1].name.toLowerCase()] = String(v.option2).trim().toLowerCase();
|
|
1227
|
+
}
|
|
1228
|
+
if (v.option3 != null && productOptions?.[2]) {
|
|
1229
|
+
map[productOptions[2].name.toLowerCase()] = String(v.option3).trim().toLowerCase();
|
|
1230
|
+
}
|
|
1231
|
+
const optArray = Array.isArray(v.options) ? v.options : Array.isArray(v.selected_options) ? v.selected_options : Array.isArray(v.variant) ? v.variant : null;
|
|
1232
|
+
if (optArray) {
|
|
1233
|
+
for (const item of optArray) {
|
|
1234
|
+
const key = (item.name || item.option || "").trim().toLowerCase();
|
|
1235
|
+
const val = (item.value || "").trim().toLowerCase();
|
|
1236
|
+
if (key && val) map[key] = val;
|
|
1237
|
+
}
|
|
1238
|
+
} else if (v.options && typeof v.options === "object") {
|
|
1239
|
+
for (const [k, val] of Object.entries(v.options)) {
|
|
1240
|
+
map[k.trim().toLowerCase()] = String(val).trim().toLowerCase();
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
const title = String(v.title ?? "").trim();
|
|
1244
|
+
const titleLower = title.toLowerCase();
|
|
1245
|
+
if (Array.isArray(productOptions)) {
|
|
1246
|
+
for (const opt of productOptions) {
|
|
1247
|
+
const key = opt.name.toLowerCase();
|
|
1248
|
+
if (!map[key]) {
|
|
1249
|
+
const rawValues = normalizeValues(opt.values).filter(
|
|
1250
|
+
(val) => !productOptions.some(
|
|
1251
|
+
(other) => other !== opt && normalizeValues(other.values).some(
|
|
1252
|
+
(ov) => ov.trim().toLowerCase() === val.trim().toLowerCase()
|
|
1253
|
+
)
|
|
1254
|
+
)
|
|
1255
|
+
);
|
|
1256
|
+
const sortedVals = [...rawValues].sort((a, b) => b.length - a.length);
|
|
1257
|
+
for (const valStr of sortedVals) {
|
|
1258
|
+
const valLower = valStr.trim().toLowerCase();
|
|
1259
|
+
const escaped = valLower.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1260
|
+
const regex = new RegExp(`(?:^|[/|\\-,;\\s])${escaped}(?:$|[/|\\-,;\\s])`, "i");
|
|
1261
|
+
if (regex.test(titleLower) || titleLower === valLower) {
|
|
1262
|
+
map[key] = valLower;
|
|
1263
|
+
break;
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
const titleParts = title.split(/\s*[/|,]\s*|\s+-\s+/).map((s) => s.trim()).filter(Boolean);
|
|
1270
|
+
if (Array.isArray(productOptions)) {
|
|
1271
|
+
productOptions.forEach((opt, idx) => {
|
|
1272
|
+
const key = opt.name.toLowerCase();
|
|
1273
|
+
if (!map[key] && titleParts[idx] != null) {
|
|
1274
|
+
map[key] = titleParts[idx].toLowerCase();
|
|
1275
|
+
}
|
|
1276
|
+
});
|
|
1277
|
+
}
|
|
1278
|
+
const result = { map, titleParts };
|
|
1279
|
+
if (v && typeof v === "object") this.optMapCache.set(v, result);
|
|
1280
|
+
return result;
|
|
1281
|
+
}
|
|
1282
|
+
/** Option values a variant represents (map values ∪ title parts). */
|
|
1283
|
+
variantOptionValues(v) {
|
|
1284
|
+
const { map, titleParts } = this.getVariantOptionMap(v, this.product?.options);
|
|
1285
|
+
const set = new Set(titleParts.map((s) => s.toLowerCase()));
|
|
1286
|
+
for (const val of Object.values(map)) set.add(val);
|
|
1287
|
+
return Array.from(set);
|
|
1288
|
+
}
|
|
1289
|
+
/** Does a variant part represent this option value? Exact match, plus a
|
|
1290
|
+
* whole-token match ONLY for numeric values ("30" matches "30 ml") — word
|
|
1291
|
+
* subsets of text values are not matches ("Large" must NOT match
|
|
1292
|
+
* "Extra Large"). */
|
|
1293
|
+
partMatchesValue(part, value) {
|
|
1294
|
+
if (!part || !value) return false;
|
|
1295
|
+
const p = String(part).trim().toLowerCase();
|
|
1296
|
+
const v = String(value).trim().toLowerCase();
|
|
1297
|
+
if (p === v) return true;
|
|
1298
|
+
if (!/^[\d.,]+$/.test(v)) return false;
|
|
1299
|
+
return p.split(/\s+/).includes(v);
|
|
1300
|
+
}
|
|
1301
|
+
/** Does a variant satisfy every currently-selected option value? */
|
|
1302
|
+
variantMatchesSelection(v, selection) {
|
|
1303
|
+
if (!v || !selection) return false;
|
|
1304
|
+
const entries = Object.entries(selection);
|
|
1305
|
+
if (entries.length === 0) return true;
|
|
1306
|
+
const { map, titleParts } = this.getVariantOptionMap(v, this.product?.options);
|
|
1307
|
+
const titlePartsLower = titleParts.map((t) => t.toLowerCase());
|
|
1308
|
+
return entries.every(([optName, val]) => {
|
|
1309
|
+
const key = optName.trim().toLowerCase();
|
|
1310
|
+
const targetVal = String(val).trim().toLowerCase();
|
|
1311
|
+
if (map[key]) {
|
|
1312
|
+
return this.partMatchesValue(map[key], targetVal);
|
|
1313
|
+
}
|
|
1314
|
+
return titlePartsLower.some((part) => this.partMatchesValue(part, targetVal));
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
/** Sellable quantity for a variant, across the stock field zoo. Boolean
|
|
1318
|
+
* availability flags win; a numeric field wins next; `null` = the feed does
|
|
1319
|
+
* not report stock for this variant (callers fall back to product stock —
|
|
1320
|
+
* never assume "in stock" from absent data). */
|
|
1321
|
+
getVariantQty(v) {
|
|
1322
|
+
if (!v) return 0;
|
|
1323
|
+
if (v.available === false || v.in_stock === false || v.is_available === false) return 0;
|
|
1324
|
+
if (v.available === true || v.in_stock === true || v.is_available === true) return 999;
|
|
1325
|
+
const rawQty = v.quantity ?? v.inventory_quantity ?? v.stock ?? v.available_quantity ?? v.stock_quantity;
|
|
1326
|
+
if (rawQty !== null && rawQty !== void 0 && rawQty !== "") {
|
|
1327
|
+
const parsed = Number(rawQty);
|
|
1328
|
+
if (!Number.isNaN(parsed)) return parsed;
|
|
1329
|
+
}
|
|
1330
|
+
return null;
|
|
1331
|
+
}
|
|
1332
|
+
/** Variant stock with the product-level fallback applied (0 = not sellable). */
|
|
1333
|
+
variantSellableQty(v) {
|
|
1334
|
+
const q = this.getVariantQty(v);
|
|
1335
|
+
if (q !== null) return q;
|
|
1336
|
+
return this.product?.quantity ?? 0;
|
|
1337
|
+
}
|
|
1338
|
+
/** Resolve the active variant: full in-stock match → full match → best score
|
|
1339
|
+
* (2 per mapped option match, 1 per title match, +0.5 if in stock). */
|
|
1340
|
+
selectedVariant() {
|
|
1341
|
+
if (!this.product || !Array.isArray(this.product.variants) || this.product.variants.length === 0) {
|
|
1342
|
+
return null;
|
|
1343
|
+
}
|
|
1344
|
+
const sel = this.selectedOptions;
|
|
1345
|
+
const entries = Object.entries(sel);
|
|
1346
|
+
if (entries.length === 0) return this.product.variants[0] ?? null;
|
|
1347
|
+
const fullInStock = this.product.variants.find(
|
|
1348
|
+
(v) => this.variantMatchesSelection(v, sel) && this.variantSellableQty(v) > 0
|
|
1349
|
+
);
|
|
1350
|
+
if (fullInStock) return fullInStock;
|
|
1351
|
+
const full = this.product.variants.find((v) => this.variantMatchesSelection(v, sel));
|
|
1352
|
+
if (full) return full;
|
|
1353
|
+
let bestVariant = null;
|
|
1354
|
+
let maxScore = -1;
|
|
1355
|
+
for (const v of this.product.variants) {
|
|
1356
|
+
const { map, titleParts } = this.getVariantOptionMap(v, this.product?.options);
|
|
1357
|
+
const titlePartsLower = titleParts.map((t) => t.toLowerCase());
|
|
1358
|
+
let score = 0;
|
|
1359
|
+
for (const [optName, val] of entries) {
|
|
1360
|
+
const key = optName.trim().toLowerCase();
|
|
1361
|
+
const targetVal = String(val).trim().toLowerCase();
|
|
1362
|
+
if (map[key] && this.partMatchesValue(map[key], targetVal)) {
|
|
1363
|
+
score += 2;
|
|
1364
|
+
} else if (titlePartsLower.some((part) => this.partMatchesValue(part, targetVal))) {
|
|
1365
|
+
score += 1;
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
if (this.variantSellableQty(v) > 0) score += 0.5;
|
|
1369
|
+
if (score > maxScore) {
|
|
1370
|
+
maxScore = score;
|
|
1371
|
+
bestVariant = v;
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
return bestVariant ?? this.product.variants[0] ?? null;
|
|
1375
|
+
}
|
|
1376
|
+
/** True if some in-stock variant carries this option value. */
|
|
1377
|
+
isValueAvailable(optionName, value) {
|
|
1378
|
+
if (!this.product || !Array.isArray(this.product.variants) || this.product.variants.length === 0) {
|
|
1379
|
+
return true;
|
|
1380
|
+
}
|
|
1381
|
+
const valLower = String(value).trim().toLowerCase();
|
|
1382
|
+
const key = optionName.trim().toLowerCase();
|
|
1383
|
+
return this.product.variants.some((v) => {
|
|
1384
|
+
const { map, titleParts } = this.getVariantOptionMap(v, this.product?.options);
|
|
1385
|
+
if (this.variantSellableQty(v) <= 0) return false;
|
|
1386
|
+
if (map[key]) return this.partMatchesValue(map[key], valLower);
|
|
1387
|
+
return titleParts.some((part) => this.partMatchesValue(part.toLowerCase(), valLower));
|
|
1388
|
+
});
|
|
1389
|
+
}
|
|
1390
|
+
get unitPrice() {
|
|
1391
|
+
const v = this.selectedVariant();
|
|
1392
|
+
const price = v?.price ?? this.product?.price;
|
|
1393
|
+
return price?.amount_incl_taxes ?? price?.amount ?? 0;
|
|
1394
|
+
}
|
|
1395
|
+
get compareAt() {
|
|
1396
|
+
const v = this.selectedVariant();
|
|
1397
|
+
const price = v?.price ?? this.product?.price;
|
|
1398
|
+
return price?.compare_at_incl_taxes ?? price?.compare_at ?? null;
|
|
1399
|
+
}
|
|
1400
|
+
get availableQuantity() {
|
|
1401
|
+
const v = this.selectedVariant();
|
|
1402
|
+
if (v) return this.variantSellableQty(v);
|
|
1403
|
+
return this.product?.quantity ?? 0;
|
|
1404
|
+
}
|
|
1405
|
+
stockLabel() {
|
|
1406
|
+
const qty = this.availableQuantity;
|
|
1407
|
+
if (qty <= 0) return { text: "Utsolgt", cls: "out-of-stock" };
|
|
1408
|
+
if (qty <= 3) return { text: `F\xE5 igjen \u2014 ${qty} stk`, cls: "low-stock" };
|
|
1409
|
+
return { text: "P\xE5 lager", cls: "in-stock" };
|
|
1410
|
+
}
|
|
1411
|
+
/** Pick an option value. If the exact combination doesn't exist, jump to an
|
|
1412
|
+
* in-stock variant that carries the value and adopt its full option map
|
|
1413
|
+
* (keeps the selection always resolvable to a real variant). */
|
|
1414
|
+
selectOption(optionName, value) {
|
|
1415
|
+
const newSelection = { ...this.selectedOptions, [optionName]: value };
|
|
1416
|
+
const exactInStock = this.product?.variants?.find(
|
|
1417
|
+
(v) => this.variantMatchesSelection(v, newSelection) && this.variantSellableQty(v) > 0
|
|
1418
|
+
);
|
|
1419
|
+
if (exactInStock) {
|
|
1420
|
+
this.selectedOptions = newSelection;
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1423
|
+
const exactAnyStock = this.product?.variants?.find(
|
|
1424
|
+
(v) => this.variantMatchesSelection(v, newSelection)
|
|
1425
|
+
);
|
|
1426
|
+
if (exactAnyStock) {
|
|
1427
|
+
this.selectedOptions = newSelection;
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1430
|
+
const valLower = String(value).trim().toLowerCase();
|
|
1431
|
+
const key = optionName.trim().toLowerCase();
|
|
1432
|
+
const anyInStockWithVal = this.product?.variants?.find((v) => {
|
|
1433
|
+
const { map, titleParts } = this.getVariantOptionMap(v, this.product?.options);
|
|
1434
|
+
if (this.variantSellableQty(v) <= 0) return false;
|
|
1435
|
+
if (map[key]) return this.partMatchesValue(map[key], valLower);
|
|
1436
|
+
return titleParts.some((p) => this.partMatchesValue(p.toLowerCase(), valLower));
|
|
1437
|
+
});
|
|
1438
|
+
if (anyInStockWithVal) {
|
|
1439
|
+
const { map } = this.getVariantOptionMap(anyInStockWithVal, this.product?.options);
|
|
1440
|
+
const updated = { ...newSelection };
|
|
1441
|
+
for (const opt of this.product?.options ?? []) {
|
|
1442
|
+
const k = opt.name.toLowerCase();
|
|
1443
|
+
if (map[k]) {
|
|
1444
|
+
const optValues = normalizeValues(opt.values);
|
|
1445
|
+
const origCase = optValues.find((val) => val.toLowerCase() === map[k]) || map[k];
|
|
1446
|
+
updated[opt.name] = origCase;
|
|
1447
|
+
}
|
|
1656
1448
|
}
|
|
1449
|
+
this.selectedOptions = updated;
|
|
1450
|
+
} else {
|
|
1451
|
+
this.selectedOptions = newSelection;
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
/* eslint-enable @typescript-eslint/no-explicit-any */
|
|
1455
|
+
allImages() {
|
|
1456
|
+
if (!this.product) return [];
|
|
1457
|
+
const variantImages = this.selectedVariant()?.images ?? [];
|
|
1458
|
+
if (variantImages.length > 0) return variantImages;
|
|
1459
|
+
return this.product.images;
|
|
1460
|
+
}
|
|
1461
|
+
get activeImage() {
|
|
1462
|
+
return this.allImages()[this.activeImageIndex];
|
|
1463
|
+
}
|
|
1464
|
+
/** Add the current product + selected variant + qty to the cart. Returns the variant id used. */
|
|
1465
|
+
addCurrentToCart() {
|
|
1466
|
+
if (!this.product) return void 0;
|
|
1467
|
+
const variant = this.selectedVariant();
|
|
1468
|
+
const sponsor = Vio.bootstrapCache?.primarySponsor;
|
|
1469
|
+
const secondaries = Vio.bootstrapCache?.secondarySponsors ?? [];
|
|
1470
|
+
const sponsorName = sponsor?.id === this.sponsorId ? sponsor.name : secondaries.find((s) => s.id === this.sponsorId)?.name;
|
|
1471
|
+
Vio.cart.addProduct({
|
|
1472
|
+
product: this.product,
|
|
1473
|
+
sponsorId: this.sponsorId,
|
|
1474
|
+
sponsorName,
|
|
1475
|
+
quantity: this.quantity,
|
|
1476
|
+
variantId: variant?.id
|
|
1477
|
+
});
|
|
1478
|
+
return variant?.id;
|
|
1479
|
+
}
|
|
1480
|
+
async addToCart() {
|
|
1481
|
+
if (!this.product || this.availableQuantity <= 0 || this.adding) return;
|
|
1482
|
+
this.adding = true;
|
|
1483
|
+
try {
|
|
1484
|
+
const variantId = this.addCurrentToCart();
|
|
1657
1485
|
this.dispatchEvent(
|
|
1658
|
-
new CustomEvent("vio:
|
|
1486
|
+
new CustomEvent("vio:added-to-cart", {
|
|
1659
1487
|
bubbles: true,
|
|
1660
1488
|
composed: true,
|
|
1661
|
-
detail: {
|
|
1489
|
+
detail: {
|
|
1490
|
+
productId: this.product.id,
|
|
1491
|
+
sponsorId: this.sponsorId,
|
|
1492
|
+
quantity: this.quantity,
|
|
1493
|
+
variantId
|
|
1494
|
+
}
|
|
1662
1495
|
})
|
|
1663
1496
|
);
|
|
1664
|
-
|
|
1497
|
+
this.close();
|
|
1498
|
+
} finally {
|
|
1499
|
+
this.adding = false;
|
|
1500
|
+
}
|
|
1665
1501
|
}
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1502
|
+
/**
|
|
1503
|
+
* Express buy: add to cart, then open the checkout overlay with Klarna
|
|
1504
|
+
* preselected. The host app handles `vio:checkout-open` (same event the cart
|
|
1505
|
+
* uses) — the `paymentMethod` hint tells it to preselect Klarna so its widget
|
|
1506
|
+
* loads immediately. Reuses the working Klarna Payments flow (no express SDK).
|
|
1507
|
+
*/
|
|
1508
|
+
async buyWithKlarna() {
|
|
1509
|
+
if (!this.product || this.availableQuantity <= 0 || this.adding) return;
|
|
1510
|
+
this.adding = true;
|
|
1511
|
+
try {
|
|
1512
|
+
this.addCurrentToCart();
|
|
1513
|
+
this.close();
|
|
1514
|
+
this.dispatchEvent(
|
|
1515
|
+
new CustomEvent("vio:checkout-open", {
|
|
1516
|
+
bubbles: true,
|
|
1517
|
+
composed: true,
|
|
1518
|
+
detail: { sponsorId: this.sponsorId, paymentMethod: "klarna", express: true }
|
|
1519
|
+
})
|
|
1520
|
+
);
|
|
1521
|
+
} finally {
|
|
1522
|
+
this.adding = false;
|
|
1678
1523
|
}
|
|
1679
|
-
return { items, total: this.totalAcrossSponsors, currency: this.primaryCurrency };
|
|
1680
1524
|
}
|
|
1681
|
-
|
|
1682
|
-
|
|
1525
|
+
/** Buy with Vipps — opens the checkout with Vipps preselected. */
|
|
1526
|
+
buyWithVipps() {
|
|
1527
|
+
if (!this.product || this.availableQuantity <= 0 || this.adding) return;
|
|
1528
|
+
this.addCurrentToCart();
|
|
1683
1529
|
this.close();
|
|
1684
|
-
}
|
|
1685
|
-
/** Klarna express — opens the side-drawer Klarna checkout for the whole cart,
|
|
1686
|
-
* skipping the address form (leveringsadresse) and method picker
|
|
1687
|
-
* (betalingsmetode). The host's checkout-open handler closes this cart and
|
|
1688
|
-
* opens <vio-checkout> in express mode with Klarna preselected. */
|
|
1689
|
-
onKlarnaExpress() {
|
|
1690
|
-
if (this.itemCount === 0) return;
|
|
1691
|
-
const firstSponsorId = [...this.carts.keys()][0];
|
|
1692
|
-
if (firstSponsorId === void 0) return;
|
|
1693
1530
|
this.dispatchEvent(
|
|
1694
1531
|
new CustomEvent("vio:checkout-open", {
|
|
1695
1532
|
bubbles: true,
|
|
1696
1533
|
composed: true,
|
|
1697
|
-
detail: { sponsorId:
|
|
1534
|
+
detail: { sponsorId: this.sponsorId, paymentMethod: "vipps", express: false }
|
|
1698
1535
|
})
|
|
1699
1536
|
);
|
|
1700
1537
|
}
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
Vio.cart.updateQuantity(item.id, item.sponsorId, item.quantity + 1);
|
|
1714
|
-
}
|
|
1715
|
-
onDecrement(item) {
|
|
1716
|
-
Vio.cart.updateQuantity(item.id, item.sponsorId, item.quantity - 1);
|
|
1538
|
+
/** Buy with Stripe — opens the checkout with Stripe preselected. */
|
|
1539
|
+
buyWithStripe() {
|
|
1540
|
+
if (!this.product || this.availableQuantity <= 0 || this.adding) return;
|
|
1541
|
+
this.addCurrentToCart();
|
|
1542
|
+
this.close();
|
|
1543
|
+
this.dispatchEvent(
|
|
1544
|
+
new CustomEvent("vio:checkout-open", {
|
|
1545
|
+
bubbles: true,
|
|
1546
|
+
composed: true,
|
|
1547
|
+
detail: { sponsorId: this.sponsorId, paymentMethod: "stripe", express: false }
|
|
1548
|
+
})
|
|
1549
|
+
);
|
|
1717
1550
|
}
|
|
1718
|
-
|
|
1719
|
-
|
|
1551
|
+
/**
|
|
1552
|
+
* Express buy with Apple Pay. Runs synchronously inside the click so the
|
|
1553
|
+
* Apple Pay sheet keeps the user gesture: add to cart → open checkout (sets
|
|
1554
|
+
* state) → launch the sheet. On authorize, `payment-complete` drives the
|
|
1555
|
+
* confirmation drawer (same path as the in-checkout Apple Pay button).
|
|
1556
|
+
*/
|
|
1557
|
+
buyWithApplePay() {
|
|
1558
|
+
if (!this.product || this.availableQuantity <= 0 || !this.applePayHandle) return;
|
|
1559
|
+
this.addCurrentToCart();
|
|
1560
|
+
try {
|
|
1561
|
+
Vio.checkout.open(this.sponsorId);
|
|
1562
|
+
} catch (err) {
|
|
1563
|
+
if (typeof console !== "undefined") {
|
|
1564
|
+
console.warn("[VioProductDetail] checkout open failed:", err);
|
|
1565
|
+
}
|
|
1566
|
+
return;
|
|
1567
|
+
}
|
|
1568
|
+
void this.applePayHandle.show();
|
|
1569
|
+
this.close();
|
|
1720
1570
|
}
|
|
1721
1571
|
render() {
|
|
1722
|
-
const isEmpty = this.itemCount === 0;
|
|
1723
|
-
const sponsorCarts = [...this.carts.values()].filter((c) => c.items.length > 0);
|
|
1724
1572
|
return html`
|
|
1725
1573
|
<div class="backdrop" @click=${this.close}></div>
|
|
1726
|
-
<
|
|
1574
|
+
<div class="modal" role="dialog" aria-label="Produkt">
|
|
1727
1575
|
<div class="handle" @click=${this.close} aria-hidden="true"></div>
|
|
1728
|
-
<div class="
|
|
1729
|
-
<
|
|
1576
|
+
<div class="topbar">
|
|
1577
|
+
<span class="topbar-brand">${this.product?.brand ?? ""}</span>
|
|
1730
1578
|
<button class="close" @click=${this.close} aria-label="Lukk">×</button>
|
|
1731
1579
|
</div>
|
|
1732
1580
|
|
|
1733
|
-
${this.
|
|
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
|
-
</button>
|
|
1773
|
-
</span>
|
|
1774
|
-
<span class="item-price">
|
|
1775
|
-
${formatPrice(
|
|
1776
|
-
item.unitPrice * item.quantity,
|
|
1777
|
-
item.currency
|
|
1778
|
-
)}
|
|
1779
|
-
</span>
|
|
1780
|
-
</div>
|
|
1781
|
-
</div>
|
|
1782
|
-
</div>
|
|
1783
|
-
`
|
|
1784
|
-
)}
|
|
1785
|
-
</div>
|
|
1786
|
-
`
|
|
1787
|
-
)}
|
|
1788
|
-
</div>
|
|
1789
|
-
<div class="footer">
|
|
1790
|
-
<div class="subtotal-row">
|
|
1791
|
-
<span class="subtotal-label">${this.subtotalLabel}</span>
|
|
1792
|
-
<span class="subtotal-value">
|
|
1793
|
-
${formatPrice(this.totalAcrossSponsors, this.primaryCurrency)}
|
|
1794
|
-
</span>
|
|
1795
|
-
</div>
|
|
1796
|
-
${this.applePayAvailable ? html`
|
|
1581
|
+
${this.renderBody()}
|
|
1582
|
+
</div>
|
|
1583
|
+
`;
|
|
1584
|
+
}
|
|
1585
|
+
renderBody() {
|
|
1586
|
+
if (this.isLoading && !this.product) {
|
|
1587
|
+
return html`
|
|
1588
|
+
<div class="skeleton" role="status" aria-busy="true" aria-label="Laster produkt">
|
|
1589
|
+
<div class="sk-image"></div>
|
|
1590
|
+
<div class="sk-line sk-brand"></div>
|
|
1591
|
+
<div class="sk-line sk-title"></div>
|
|
1592
|
+
<div class="sk-line sk-title-2"></div>
|
|
1593
|
+
<div class="sk-line sk-price"></div>
|
|
1594
|
+
<div class="sk-button"></div>
|
|
1595
|
+
</div>
|
|
1596
|
+
`;
|
|
1597
|
+
}
|
|
1598
|
+
if (this.fetchError) {
|
|
1599
|
+
return html`<div class="error">Kunne ikke laste produkt: ${this.fetchError}</div>`;
|
|
1600
|
+
}
|
|
1601
|
+
if (!this.product) return html``;
|
|
1602
|
+
const p = this.product;
|
|
1603
|
+
const stock = this.stockLabel();
|
|
1604
|
+
const compare = this.compareAt;
|
|
1605
|
+
const showCompare = compare !== null && compare > this.unitPrice;
|
|
1606
|
+
const images = this.allImages();
|
|
1607
|
+
const activeImage = this.activeImage;
|
|
1608
|
+
return html`
|
|
1609
|
+
<div class="body">
|
|
1610
|
+
<div class="gallery">
|
|
1611
|
+
<div class="gallery-main">
|
|
1612
|
+
${activeImage?.url ? html`<img src=${activeImage.url} alt=${p.title} />` : ""}
|
|
1613
|
+
</div>
|
|
1614
|
+
${images.length > 1 ? html`
|
|
1615
|
+
<div class="gallery-thumbs">
|
|
1616
|
+
${images.map(
|
|
1617
|
+
(img, idx) => html`
|
|
1797
1618
|
<button
|
|
1798
|
-
class="
|
|
1799
|
-
@click=${this.
|
|
1800
|
-
aria-label
|
|
1619
|
+
class="gallery-thumb ${idx === this.activeImageIndex ? "active" : ""}"
|
|
1620
|
+
@click=${() => this.activeImageIndex = idx}
|
|
1621
|
+
aria-label=${`Bilde ${idx + 1}`}
|
|
1801
1622
|
>
|
|
1802
|
-
<
|
|
1623
|
+
<img src=${img.url} alt="" />
|
|
1803
1624
|
</button>
|
|
1804
|
-
`
|
|
1625
|
+
`
|
|
1626
|
+
)}
|
|
1627
|
+
</div>
|
|
1628
|
+
` : ""}
|
|
1629
|
+
</div>
|
|
1630
|
+
|
|
1631
|
+
<div class="info">
|
|
1632
|
+
${p.brand ? html`<div class="brand">${p.brand}</div>` : ""}
|
|
1633
|
+
<h2 class="name">${p.title}</h2>
|
|
1634
|
+
|
|
1635
|
+
<div class="price-row">
|
|
1636
|
+
<span class="price">${formatPrice(this.unitPrice, p.price.currency_code)}</span>
|
|
1637
|
+
${showCompare ? html`<span class="compare-at">${formatPrice(
|
|
1638
|
+
compare,
|
|
1639
|
+
p.price.currency_code
|
|
1640
|
+
)}</span>` : ""}
|
|
1641
|
+
</div>
|
|
1642
|
+
|
|
1643
|
+
<div class="stock ${stock.cls}">${stock.text}</div>
|
|
1644
|
+
|
|
1645
|
+
${this.renderOptionPickers(p.options)}
|
|
1646
|
+
|
|
1647
|
+
<div class="qty-cta-row">
|
|
1648
|
+
<div class="qty-stepper">
|
|
1649
|
+
<button
|
|
1650
|
+
class="qty-btn"
|
|
1651
|
+
@click=${() => this.quantity = Math.max(1, this.quantity - 1)}
|
|
1652
|
+
aria-label="Reduser antall"
|
|
1653
|
+
>
|
|
1654
|
+
−
|
|
1655
|
+
</button>
|
|
1656
|
+
<span class="qty-value">${this.quantity}</span>
|
|
1657
|
+
<button
|
|
1658
|
+
class="qty-btn"
|
|
1659
|
+
@click=${() => this.quantity = Math.min(this.availableQuantity, this.quantity + 1)}
|
|
1660
|
+
aria-label="Øk antall"
|
|
1661
|
+
?disabled=${this.quantity >= this.availableQuantity}
|
|
1662
|
+
>
|
|
1663
|
+
+
|
|
1664
|
+
</button>
|
|
1665
|
+
</div>
|
|
1666
|
+
<button
|
|
1667
|
+
class="add-to-cart"
|
|
1668
|
+
@click=${this.addToCart}
|
|
1669
|
+
?disabled=${this.availableQuantity <= 0 || this.adding}
|
|
1670
|
+
>
|
|
1671
|
+
${this.adding ? "Legger til\u2026" : this.availableQuantity <= 0 ? "Utsolgt" : "Legg i handlekurv"}
|
|
1672
|
+
</button>
|
|
1673
|
+
</div>
|
|
1674
|
+
|
|
1675
|
+
${this.availableQuantity > 0 && this.applePayOk && this.methodEnabled("apple-pay", "applepay") ? html`
|
|
1676
|
+
<button
|
|
1677
|
+
class="buy-applepay"
|
|
1678
|
+
@click=${this.buyWithApplePay}
|
|
1679
|
+
?disabled=${this.adding}
|
|
1680
|
+
aria-label="Kjøp nå med Apple Pay"
|
|
1681
|
+
>
|
|
1682
|
+
<span>Kjøp nå med</span>
|
|
1683
|
+
<svg class="ap-logo" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M17.05 20.28c-.98.95-2.05.8-3.08.35-1.09-.46-2.09-.48-3.24 0-1.44.62-2.2.44-3.06-.35C2.79 15.25 3.51 7.59 9.05 7.31c1.35.07 2.29.74 3.08.8 1.18-.24 2.31-.93 3.57-.84 1.51.12 2.65.72 3.4 1.8-3.12 1.87-2.38 5.98.48 7.13-.57 1.5-1.31 2.99-2.54 4.09l.01-.01zM12.03 7.25c-.15-2.23 1.66-4.07 3.74-4.25.29 2.58-2.34 4.5-3.74 4.25z"/></svg>
|
|
1684
|
+
<span>Apple Pay</span>
|
|
1685
|
+
</button>
|
|
1686
|
+
` : ""}
|
|
1687
|
+
${this.availableQuantity > 0 && this.methodEnabled("stripe") ? html`
|
|
1688
|
+
<button
|
|
1689
|
+
class="buy-stripe"
|
|
1690
|
+
@click=${this.buyWithStripe}
|
|
1691
|
+
?disabled=${this.adding}
|
|
1692
|
+
aria-label="Kjøp nå med Stripe"
|
|
1693
|
+
style="width: 100%; margin-top: 10px; display: flex; align-items: center; justify-content: center; gap: 6px; background: #635bff; color: #fff; border: none; padding: 14px 24px; font-size: 15px; font-weight: 600; cursor: pointer; font-family: inherit;"
|
|
1694
|
+
>
|
|
1695
|
+
<span>Kjøp nå med</span>
|
|
1696
|
+
<img
|
|
1697
|
+
src=${STRIPE_LOGO_SRC}
|
|
1698
|
+
style="height: 20px; display: block; filter: brightness(0) invert(1);"
|
|
1699
|
+
alt="Stripe"
|
|
1700
|
+
/>
|
|
1701
|
+
</button>
|
|
1702
|
+
` : ""}
|
|
1703
|
+
${this.availableQuantity > 0 && this.methodEnabled("klarna") ? html`
|
|
1805
1704
|
<button
|
|
1806
|
-
class="klarna
|
|
1807
|
-
@click=${this.
|
|
1808
|
-
|
|
1705
|
+
class="buy-klarna"
|
|
1706
|
+
@click=${this.buyWithKlarna}
|
|
1707
|
+
?disabled=${this.adding}
|
|
1809
1708
|
>
|
|
1810
|
-
Kjøp med
|
|
1709
|
+
<span>Kjøp nå med</span>
|
|
1811
1710
|
<img
|
|
1812
|
-
class="klarna-
|
|
1711
|
+
class="klarna-badge"
|
|
1813
1712
|
src="https://x.klarnacdn.net/payment-method/assets/badges/generic/klarna.svg"
|
|
1814
1713
|
alt="Klarna"
|
|
1815
1714
|
/>
|
|
1816
1715
|
</button>
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1716
|
+
` : ""}
|
|
1717
|
+
|
|
1718
|
+
${this.availableQuantity > 0 && this.methodEnabled("vipps") ? html`
|
|
1719
|
+
<button
|
|
1720
|
+
class="buy-vipps"
|
|
1721
|
+
@click=${this.buyWithVipps}
|
|
1722
|
+
?disabled=${this.adding}
|
|
1723
|
+
aria-label="Kjøp nå med Vipps"
|
|
1724
|
+
>
|
|
1725
|
+
<span>Kjøp nå med</span>
|
|
1726
|
+
<span class="vipps-badge">vipps</span>
|
|
1727
|
+
</button>
|
|
1728
|
+
` : ""}
|
|
1729
|
+
|
|
1730
|
+
${p.description ? html`
|
|
1731
|
+
<div>
|
|
1732
|
+
<div class="description-label">Beskrivelse</div>
|
|
1733
|
+
<div class="description">${this.stripHtml(p.description)}</div>
|
|
1734
|
+
</div>
|
|
1735
|
+
` : ""}
|
|
1736
|
+
</div>
|
|
1737
|
+
</div>
|
|
1820
1738
|
`;
|
|
1821
1739
|
}
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
<
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1740
|
+
renderOptionPickers(options) {
|
|
1741
|
+
if (options.length === 0) return "";
|
|
1742
|
+
return html`${options.map((opt) => {
|
|
1743
|
+
const values = normalizeValues(opt.values);
|
|
1744
|
+
if (values.length === 0) return "";
|
|
1745
|
+
return html`
|
|
1746
|
+
<div class="option-group">
|
|
1747
|
+
<div class="option-label">${opt.name}</div>
|
|
1748
|
+
<div class="option-values">
|
|
1749
|
+
${values.map((val) => {
|
|
1750
|
+
const available = this.isValueAvailable(opt.name, val);
|
|
1751
|
+
const currentVal = this.selectedOptions[opt.name] ?? "";
|
|
1752
|
+
const isSelected = String(currentVal).trim().toLowerCase() === String(val).trim().toLowerCase();
|
|
1753
|
+
return html`
|
|
1754
|
+
<button
|
|
1755
|
+
class="option-pill ${available ? "" : "unavailable"}"
|
|
1756
|
+
@click=${() => this.selectOption(opt.name, val)}
|
|
1757
|
+
aria-pressed=${isSelected}
|
|
1758
|
+
?disabled=${!available}
|
|
1759
|
+
title=${available ? "" : "Utsolgt"}
|
|
1760
|
+
>
|
|
1761
|
+
${val}
|
|
1762
|
+
</button>
|
|
1763
|
+
`;
|
|
1764
|
+
})}
|
|
1842
1765
|
</div>
|
|
1843
1766
|
</div>
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1767
|
+
`;
|
|
1768
|
+
})}`;
|
|
1769
|
+
}
|
|
1770
|
+
/** Best-effort HTML strip for description. */
|
|
1771
|
+
stripHtml(html6) {
|
|
1772
|
+
return html6.replace(/<br\s*\/?>/gi, "\n").replace(/<\/p>/gi, "\n\n").replace(/<[^>]+>/g, "").replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").trim();
|
|
1847
1773
|
}
|
|
1848
1774
|
};
|
|
1849
|
-
|
|
1775
|
+
__decorateClass([
|
|
1776
|
+
property({ type: Boolean, reflect: true })
|
|
1777
|
+
], VioProductDetail.prototype, "open", 2);
|
|
1778
|
+
__decorateClass([
|
|
1779
|
+
property({ type: String, attribute: "product-id" })
|
|
1780
|
+
], VioProductDetail.prototype, "productId", 2);
|
|
1781
|
+
__decorateClass([
|
|
1782
|
+
property({ type: Number, attribute: "sponsor-id" })
|
|
1783
|
+
], VioProductDetail.prototype, "sponsorId", 2);
|
|
1784
|
+
__decorateClass([
|
|
1785
|
+
property({ type: Boolean, attribute: "auto-open" })
|
|
1786
|
+
], VioProductDetail.prototype, "autoOpen", 2);
|
|
1787
|
+
__decorateClass([
|
|
1788
|
+
property({ type: String })
|
|
1789
|
+
], VioProductDetail.prototype, "currency", 2);
|
|
1790
|
+
__decorateClass([
|
|
1791
|
+
state()
|
|
1792
|
+
], VioProductDetail.prototype, "product", 2);
|
|
1793
|
+
__decorateClass([
|
|
1794
|
+
state()
|
|
1795
|
+
], VioProductDetail.prototype, "isLoading", 2);
|
|
1796
|
+
__decorateClass([
|
|
1797
|
+
state()
|
|
1798
|
+
], VioProductDetail.prototype, "fetchError", 2);
|
|
1799
|
+
__decorateClass([
|
|
1800
|
+
state()
|
|
1801
|
+
], VioProductDetail.prototype, "selectedOptions", 2);
|
|
1802
|
+
__decorateClass([
|
|
1803
|
+
state()
|
|
1804
|
+
], VioProductDetail.prototype, "quantity", 2);
|
|
1805
|
+
__decorateClass([
|
|
1806
|
+
state()
|
|
1807
|
+
], VioProductDetail.prototype, "activeImageIndex", 2);
|
|
1808
|
+
__decorateClass([
|
|
1809
|
+
state()
|
|
1810
|
+
], VioProductDetail.prototype, "adding", 2);
|
|
1811
|
+
__decorateClass([
|
|
1812
|
+
state()
|
|
1813
|
+
], VioProductDetail.prototype, "applePayOk", 2);
|
|
1814
|
+
__decorateClass([
|
|
1815
|
+
state()
|
|
1816
|
+
], VioProductDetail.prototype, "availableMethods", 2);
|
|
1817
|
+
function normalizeValues(raw) {
|
|
1818
|
+
if (Array.isArray(raw)) {
|
|
1819
|
+
return raw.map((v) => String(v).trim()).filter((v) => v.length > 0);
|
|
1820
|
+
}
|
|
1821
|
+
if (typeof raw === "string") {
|
|
1822
|
+
return raw.split(",").map((v) => v.trim()).filter((v) => v.length > 0);
|
|
1823
|
+
}
|
|
1824
|
+
return [];
|
|
1825
|
+
}
|
|
1826
|
+
var STRIPE_LOGO_SRC2 = "data:image/svg+xml;utf8," + encodeURIComponent(
|
|
1827
|
+
'<svg width="360" height="150" viewBox="0 0 360 150" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M360 77.4001C360 51.8001 347.6 31.6001 323.9 31.6001C300.1 31.6001 285.7 51.8001 285.7 77.2001C285.7 107.3 302.7 122.5 327.1 122.5C339 122.5 348 119.8 354.8 116V96.0001C348 99.4001 340.2 101.5 330.3 101.5C320.6 101.5 312 98.1001 310.9 86.3001H359.8C359.8 85.0001 360 79.8001 360 77.4001ZM310.6 67.9001C310.6 56.6001 317.5 51.9001 323.8 51.9001C329.9 51.9001 336.4 56.6001 336.4 67.9001H310.6Z" fill="#533AFD"/><path fill-rule="evenodd" clip-rule="evenodd" d="M247.1 31.6001C237.3 31.6001 231 36.2001 227.5 39.4001L226.2 33.2001H204.2V149.8L229.2 144.5L229.3 116.2C232.9 118.8 238.2 122.5 247 122.5C264.9 122.5 281.2 108.1 281.2 76.4001C281.1 47.4001 264.6 31.6001 247.1 31.6001ZM241.1 100.5C235.2 100.5 231.7 98.4001 229.3 95.8001L229.2 58.7001C231.8 55.8001 235.4 53.8001 241.1 53.8001C250.2 53.8001 256.5 64.0001 256.5 77.1001C256.5 90.5001 250.3 100.5 241.1 100.5Z" fill="#533AFD"/><path fill-rule="evenodd" clip-rule="evenodd" d="M169.8 25.7L194.9 20.3V0L169.8 5.3V25.7Z" fill="#533AFD"/><path d="M194.9 33.3H169.8V120.8H194.9V33.3Z" fill="#533AFD"/><path fill-rule="evenodd" clip-rule="evenodd" d="M142.9 40.7L141.3 33.3H119.7V120.8H144.7V61.5C150.6 53.8 160.6 55.2 163.7 56.3V33.3C160.5 32.1 148.8 29.9 142.9 40.7Z" fill="#533AFD"/><path fill-rule="evenodd" clip-rule="evenodd" d="M92.8999 11.6001L68.4999 16.8001L68.3999 96.9001C68.3999 111.7 79.4999 122.6 94.2999 122.6C102.5 122.6 108.5 121.1 111.8 119.3V99.0001C108.6 100.3 92.7999 104.9 92.7999 90.1001V54.6001H111.8V33.3001H92.7999L92.8999 11.6001Z" fill="#533AFD"/><path fill-rule="evenodd" clip-rule="evenodd" d="M25.3 58.7001C25.3 54.8001 28.5 53.3001 33.8 53.3001C41.4 53.3001 51 55.6001 58.6 59.7001V36.2001C50.3 32.9001 42.1 31.6001 33.8 31.6001C13.5 31.6001 0 42.2001 0 59.9001C0 87.5001 38 83.1001 38 95.0001C38 99.6001 34 101.1 28.4 101.1C20.1 101.1 9.5 97.7001 1.1 93.1001V116.9C10.4 120.9 19.8 122.6 28.4 122.6C49.2 122.6 63.5 112.3 63.5 94.4001C63.4 64.6001 25.3 69.9001 25.3 58.7001Z" fill="#533AFD"/></svg>'
|
|
1828
|
+
);
|
|
1829
|
+
var VioCart = class extends LitElement {
|
|
1830
|
+
constructor() {
|
|
1831
|
+
super(...arguments);
|
|
1832
|
+
this.open = false;
|
|
1833
|
+
this.heading = "Handlekurv";
|
|
1834
|
+
this.emptyLabel = "Handlekurven er tom";
|
|
1835
|
+
this.subtotalLabel = "Sum";
|
|
1836
|
+
this.carts = /* @__PURE__ */ new Map();
|
|
1837
|
+
this.itemCount = 0;
|
|
1838
|
+
this.applePayAvailable = false;
|
|
1839
|
+
this.availableMethods = null;
|
|
1840
|
+
this.confirmedOrder = null;
|
|
1841
|
+
/** Pre-resolved Apple Pay handle; `show()` fires the sheet synchronously. */
|
|
1842
|
+
this.applePayHandle = null;
|
|
1843
|
+
/** Cart total Apple Pay was last prepared for (avoids redundant prep). */
|
|
1844
|
+
this.preparedTotal = -1;
|
|
1845
|
+
/** In-flight guard for loadAvailablePaymentMethods. */
|
|
1846
|
+
this.loadingPaymentMethods = false;
|
|
1847
|
+
this.boundOnCartChange = (e) => {
|
|
1848
|
+
const detail = e.detail;
|
|
1849
|
+
this.carts = detail.cartsBySponsor;
|
|
1850
|
+
this.itemCount = detail.itemCount;
|
|
1851
|
+
if (this.availableMethods === null) {
|
|
1852
|
+
void this.loadAvailablePaymentMethods();
|
|
1853
|
+
}
|
|
1854
|
+
};
|
|
1855
|
+
}
|
|
1856
|
+
static {
|
|
1857
|
+
this.styles = css`
|
|
1850
1858
|
:host { display: contents; }
|
|
1851
1859
|
|
|
1852
1860
|
.backdrop {
|
|
@@ -2029,27 +2037,7 @@ VioCart.styles = css`
|
|
|
2029
2037
|
.subtotal-label { color: var(--vio-color-text-secondary, #666); }
|
|
2030
2038
|
.subtotal-value { font-weight: 700; font-size: 16px; }
|
|
2031
2039
|
|
|
2032
|
-
.
|
|
2033
|
-
width: 100%;
|
|
2034
|
-
padding: 18px;
|
|
2035
|
-
background: var(--vio-color-text, #0a0a0a);
|
|
2036
|
-
color: var(--vio-color-text-on-primary, #fff);
|
|
2037
|
-
border: none;
|
|
2038
|
-
font-size: 12px;
|
|
2039
|
-
text-transform: uppercase;
|
|
2040
|
-
letter-spacing: var(--vio-tracking-label, 0.1em);
|
|
2041
|
-
cursor: pointer;
|
|
2042
|
-
font-weight: 600;
|
|
2043
|
-
font-family: var(--vio-font-sans, -apple-system, sans-serif);
|
|
2044
|
-
transition: background 0.15s;
|
|
2045
|
-
}
|
|
2046
|
-
.checkout-btn:hover:not(:disabled) { background: #222; }
|
|
2047
|
-
.checkout-btn:disabled {
|
|
2048
|
-
background: var(--vio-color-text-tertiary, #999);
|
|
2049
|
-
cursor: not-allowed;
|
|
2050
|
-
}
|
|
2051
|
-
|
|
2052
|
-
/* Apple Pay express — pay the whole cart directly, above "Til kassen". */
|
|
2040
|
+
/* Apple Pay express — pay the whole cart directly. */
|
|
2053
2041
|
.applepay-btn {
|
|
2054
2042
|
width: 100%;
|
|
2055
2043
|
padding: 15px;
|
|
@@ -2070,7 +2058,7 @@ VioCart.styles = css`
|
|
|
2070
2058
|
transition: opacity 0.15s;
|
|
2071
2059
|
}
|
|
2072
2060
|
.applepay-btn:hover { opacity: 0.86; }
|
|
2073
|
-
.applepay-btn .ap-logo {
|
|
2061
|
+
.applepay-btn .ap-logo { width: 20px; height: 20px; }
|
|
2074
2062
|
/* Klarna express — pink brand button, same express row as Apple Pay. */
|
|
2075
2063
|
.klarna-btn {
|
|
2076
2064
|
width: 100%;
|
|
@@ -2092,23 +2080,50 @@ VioCart.styles = css`
|
|
|
2092
2080
|
}
|
|
2093
2081
|
.klarna-btn:hover { background: #ffa1ba; }
|
|
2094
2082
|
.klarna-btn .klarna-logo { height: 15px; width: auto; display: inline-block; }
|
|
2095
|
-
|
|
2083
|
+
/* Vipps — brand orange button, same express row. */
|
|
2084
|
+
.vipps-btn {
|
|
2085
|
+
width: 100%;
|
|
2086
|
+
padding: 15px;
|
|
2087
|
+
margin-bottom: 10px;
|
|
2088
|
+
background: #ff5b24;
|
|
2089
|
+
color: #fff;
|
|
2090
|
+
border: none;
|
|
2091
|
+
border-radius: 8px;
|
|
2092
|
+
cursor: pointer;
|
|
2096
2093
|
display: flex;
|
|
2097
2094
|
align-items: center;
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
font-size: 11px;
|
|
2102
|
-
text-transform: uppercase;
|
|
2103
|
-
letter-spacing: 0.1em;
|
|
2095
|
+
justify-content: center;
|
|
2096
|
+
font-family: var(--vio-font-sans, -apple-system, sans-serif);
|
|
2097
|
+
transition: background 0.15s;
|
|
2104
2098
|
}
|
|
2105
|
-
.
|
|
2106
|
-
.
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2099
|
+
.vipps-btn:hover { background: #ec4f1c; }
|
|
2100
|
+
.vipps-btn .vipps-logo {
|
|
2101
|
+
font-size: 20px;
|
|
2102
|
+
font-weight: 800;
|
|
2103
|
+
letter-spacing: -0.02em;
|
|
2104
|
+
line-height: 1;
|
|
2105
|
+
}
|
|
2106
|
+
/* Stripe — brand blurple button, same express row. */
|
|
2107
|
+
.stripe-btn {
|
|
2108
|
+
width: 100%;
|
|
2109
|
+
padding: 15px;
|
|
2110
|
+
margin-bottom: 10px;
|
|
2111
|
+
background: #635bff;
|
|
2112
|
+
color: #fff;
|
|
2113
|
+
border: none;
|
|
2114
|
+
border-radius: 8px;
|
|
2115
|
+
font-size: 16px;
|
|
2116
|
+
font-weight: 600;
|
|
2117
|
+
cursor: pointer;
|
|
2118
|
+
display: flex;
|
|
2119
|
+
align-items: center;
|
|
2120
|
+
justify-content: center;
|
|
2121
|
+
gap: 6px;
|
|
2122
|
+
font-family: var(--vio-font-sans, -apple-system, sans-serif);
|
|
2123
|
+
transition: background 0.15s;
|
|
2111
2124
|
}
|
|
2125
|
+
.stripe-btn:hover { background: #5248ff; }
|
|
2126
|
+
.stripe-btn .stripe-logo { height: 20px; width: auto; display: inline-block; filter: brightness(0) invert(1); }
|
|
2112
2127
|
|
|
2113
2128
|
/* In-drawer confirmation after Apple Pay (nothing else is shown). */
|
|
2114
2129
|
.confirm {
|
|
@@ -2177,601 +2192,454 @@ VioCart.styles = css`
|
|
|
2177
2192
|
}
|
|
2178
2193
|
.confirm-close:hover { background: #222; }
|
|
2179
2194
|
`;
|
|
2180
|
-
__decorateClass([
|
|
2181
|
-
property({ type: Boolean, reflect: true })
|
|
2182
|
-
], VioCart.prototype, "open", 2);
|
|
2183
|
-
__decorateClass([
|
|
2184
|
-
property({ type: String })
|
|
2185
|
-
], VioCart.prototype, "heading", 2);
|
|
2186
|
-
__decorateClass([
|
|
2187
|
-
property({ type: String, attribute: "checkout-label" })
|
|
2188
|
-
], VioCart.prototype, "checkoutLabel", 2);
|
|
2189
|
-
__decorateClass([
|
|
2190
|
-
property({ type: String, attribute: "empty-label" })
|
|
2191
|
-
], VioCart.prototype, "emptyLabel", 2);
|
|
2192
|
-
__decorateClass([
|
|
2193
|
-
property({ type: String, attribute: "subtotal-label" })
|
|
2194
|
-
], VioCart.prototype, "subtotalLabel", 2);
|
|
2195
|
-
__decorateClass([
|
|
2196
|
-
state()
|
|
2197
|
-
], VioCart.prototype, "carts", 2);
|
|
2198
|
-
__decorateClass([
|
|
2199
|
-
state()
|
|
2200
|
-
], VioCart.prototype, "itemCount", 2);
|
|
2201
|
-
__decorateClass([
|
|
2202
|
-
state()
|
|
2203
|
-
], VioCart.prototype, "applePayAvailable", 2);
|
|
2204
|
-
__decorateClass([
|
|
2205
|
-
state()
|
|
2206
|
-
], VioCart.prototype, "confirmedOrder", 2);
|
|
2207
|
-
VioCart = __decorateClass([
|
|
2208
|
-
customElement("vio-cart")
|
|
2209
|
-
], VioCart);
|
|
2210
|
-
var VioCheckout = class extends LitElement {
|
|
2211
|
-
constructor() {
|
|
2212
|
-
super(...arguments);
|
|
2213
|
-
this.open = false;
|
|
2214
|
-
this.heading = "Kasse";
|
|
2215
|
-
this.shippingLabel = "Beregnes ved kasse";
|
|
2216
|
-
this.express = false;
|
|
2217
|
-
this.checkoutState = null;
|
|
2218
|
-
this.items = [];
|
|
2219
|
-
this.applePayAvailable = false;
|
|
2220
|
-
this.applePayInProgress = false;
|
|
2221
|
-
this.klarnaAvailable = false;
|
|
2222
|
-
this.orderConfirmed = false;
|
|
2223
|
-
this.confirmedMethod = null;
|
|
2224
|
-
this.paymentError = null;
|
|
2225
|
-
this.confirmedOrder = null;
|
|
2226
|
-
/** Active Klarna Payments widget handle + the subtotal it was mounted for. */
|
|
2227
|
-
this.klarnaHandle = null;
|
|
2228
|
-
this.klarnaMountedAmount = null;
|
|
2229
|
-
this.klarnaMountedShipping = null;
|
|
2230
|
-
this.klarnaMounting = false;
|
|
2231
|
-
this.klarnaCategories = [];
|
|
2232
|
-
this.klarnaSelectedCat = "";
|
|
2233
|
-
this.klarnaAuthorizing = false;
|
|
2234
|
-
this.selectedShipping = KLARNA_SHIPPING_OPTIONS.find((o) => o.preselected)?.id ?? KLARNA_SHIPPING_OPTIONS[0]?.id ?? "";
|
|
2235
|
-
this.form = {
|
|
2236
|
-
firstName: "",
|
|
2237
|
-
lastName: "",
|
|
2238
|
-
email: "",
|
|
2239
|
-
address: "",
|
|
2240
|
-
postalCode: "",
|
|
2241
|
-
city: ""
|
|
2242
|
-
};
|
|
2243
|
-
this.boundOnCheckoutChange = (e) => {
|
|
2244
|
-
const detail = e.detail;
|
|
2245
|
-
this.checkoutState = detail.state;
|
|
2246
|
-
this.items = Vio.checkout.items;
|
|
2247
|
-
if (this.checkoutState) {
|
|
2248
|
-
this.refreshApplePay();
|
|
2249
|
-
this.refreshKlarna();
|
|
2250
|
-
} else {
|
|
2251
|
-
this.applePayAvailable = false;
|
|
2252
|
-
this.klarnaAvailable = false;
|
|
2253
|
-
this.orderConfirmed = false;
|
|
2254
|
-
this.confirmedMethod = null;
|
|
2255
|
-
this.confirmedOrder = null;
|
|
2256
|
-
this.paymentError = null;
|
|
2257
|
-
this.express = false;
|
|
2258
|
-
this.unmountKlarna();
|
|
2259
|
-
}
|
|
2260
|
-
};
|
|
2261
|
-
this.boundOnPaymentComplete = (e) => {
|
|
2262
|
-
const detail = e.detail;
|
|
2263
|
-
if (detail.method !== "klarna" && detail.method !== "apple-pay") return;
|
|
2264
|
-
const sponsorId = detail.state?.sponsorId ?? this.checkoutState?.sponsorId;
|
|
2265
|
-
if (sponsorId == null) return;
|
|
2266
|
-
this.confirmOrder(detail.method, sponsorId, detail.result);
|
|
2267
|
-
};
|
|
2268
|
-
this.boundOnPaymentError = (e) => {
|
|
2269
|
-
const detail = e.detail;
|
|
2270
|
-
this.paymentError = detail.method === "klarna" ? `Klarna-betalingen kunne ikke fullf\xF8res: ${detail.error}` : detail.error;
|
|
2271
|
-
this.open = true;
|
|
2272
|
-
};
|
|
2273
2195
|
}
|
|
2274
2196
|
connectedCallback() {
|
|
2275
2197
|
super.connectedCallback();
|
|
2276
|
-
this.
|
|
2277
|
-
this.
|
|
2278
|
-
Vio.
|
|
2279
|
-
|
|
2280
|
-
Vio.checkout.addEventListener("payment-error", this.boundOnPaymentError);
|
|
2281
|
-
if (this.checkoutState) {
|
|
2282
|
-
void this.refreshApplePay();
|
|
2283
|
-
void this.refreshKlarna();
|
|
2284
|
-
}
|
|
2285
|
-
}
|
|
2286
|
-
disconnectedCallback() {
|
|
2287
|
-
Vio.checkout.removeEventListener("change", this.boundOnCheckoutChange);
|
|
2288
|
-
Vio.checkout.removeEventListener("payment-complete", this.boundOnPaymentComplete);
|
|
2289
|
-
Vio.checkout.removeEventListener("payment-error", this.boundOnPaymentError);
|
|
2290
|
-
this.unmountKlarna();
|
|
2291
|
-
super.disconnectedCallback();
|
|
2198
|
+
this.carts = Vio.cart.getAllCarts();
|
|
2199
|
+
this.itemCount = Vio.cart.itemCount;
|
|
2200
|
+
Vio.cart.addEventListener("change", this.boundOnCartChange);
|
|
2201
|
+
void this.loadAvailablePaymentMethods();
|
|
2292
2202
|
}
|
|
2293
|
-
|
|
2294
|
-
|
|
2203
|
+
disconnectedCallback() {
|
|
2204
|
+
Vio.cart.removeEventListener("change", this.boundOnCartChange);
|
|
2205
|
+
super.disconnectedCallback();
|
|
2295
2206
|
}
|
|
2296
2207
|
close() {
|
|
2297
2208
|
this.open = false;
|
|
2298
|
-
this.express = false;
|
|
2299
|
-
Vio.checkout.close();
|
|
2300
2209
|
}
|
|
2301
2210
|
show() {
|
|
2302
2211
|
this.open = true;
|
|
2212
|
+
void this.loadAvailablePaymentMethods();
|
|
2303
2213
|
}
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
const
|
|
2310
|
-
if (
|
|
2311
|
-
|
|
2312
|
-
this.klarnaAvailable = available;
|
|
2313
|
-
}
|
|
2314
|
-
}
|
|
2315
|
-
/** Mount (or re-mount on amount change) the Klarna Payments widget. */
|
|
2316
|
-
async mountKlarnaIfNeeded() {
|
|
2317
|
-
if (!this.klarnaAvailable || !this.open || !this.checkoutState) return;
|
|
2318
|
-
if (this.checkoutState.paymentMethod !== "klarna" && !this.express) return;
|
|
2319
|
-
if (this.klarnaMounting) return;
|
|
2320
|
-
const amount = this.checkoutState.subtotal;
|
|
2321
|
-
if (this.klarnaHandle && this.klarnaMountedAmount === amount && this.klarnaMountedShipping === this.selectedShipping) {
|
|
2214
|
+
/** Which payment buttons to render — backend-configured per sponsor.
|
|
2215
|
+
* Falls back to showing everything if the query fails or none is set. */
|
|
2216
|
+
async loadAvailablePaymentMethods() {
|
|
2217
|
+
if (this.loadingPaymentMethods) return;
|
|
2218
|
+
this.loadingPaymentMethods = true;
|
|
2219
|
+
const firstSponsorId = [...this.carts.keys()][0] ?? globalThis.__VIO_SPONSOR_ID__;
|
|
2220
|
+
if (firstSponsorId === void 0) {
|
|
2221
|
+
this.loadingPaymentMethods = false;
|
|
2322
2222
|
return;
|
|
2323
2223
|
}
|
|
2324
|
-
const container = this.renderRoot?.querySelector(
|
|
2325
|
-
"#vio-klarna-payments-container"
|
|
2326
|
-
);
|
|
2327
|
-
if (!container) return;
|
|
2328
|
-
this.klarnaMounting = true;
|
|
2329
|
-
this.unmountKlarna();
|
|
2330
|
-
container.innerHTML = "";
|
|
2331
2224
|
try {
|
|
2332
|
-
const
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
}
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
this.klarnaMountedShipping = this.selectedShipping;
|
|
2339
|
-
this.klarnaCategories = handle.categories;
|
|
2340
|
-
this.klarnaSelectedCat = handle.selected;
|
|
2225
|
+
const methods = await Vio.checkout.getAvailablePaymentMethods(firstSponsorId);
|
|
2226
|
+
if (Array.isArray(methods)) {
|
|
2227
|
+
this.availableMethods = methods.map((m) => m.name);
|
|
2228
|
+
} else {
|
|
2229
|
+
this.availableMethods = null;
|
|
2230
|
+
}
|
|
2341
2231
|
} catch (err) {
|
|
2342
2232
|
if (typeof console !== "undefined") {
|
|
2343
|
-
console.warn("[
|
|
2233
|
+
console.warn("[VioCart] Failed to load payment methods:", err);
|
|
2344
2234
|
}
|
|
2345
|
-
this.
|
|
2235
|
+
this.availableMethods = null;
|
|
2346
2236
|
} finally {
|
|
2347
|
-
this.
|
|
2348
|
-
}
|
|
2349
|
-
}
|
|
2350
|
-
unmountKlarna() {
|
|
2351
|
-
if (this.klarnaHandle) {
|
|
2352
|
-
this.klarnaHandle.unmount();
|
|
2353
|
-
this.klarnaHandle = null;
|
|
2237
|
+
this.loadingPaymentMethods = false;
|
|
2354
2238
|
}
|
|
2355
|
-
this.klarnaMountedAmount = null;
|
|
2356
|
-
this.klarnaMountedShipping = null;
|
|
2357
|
-
this.klarnaCategories = [];
|
|
2358
|
-
this.klarnaSelectedCat = "";
|
|
2359
|
-
this.klarnaAuthorizing = false;
|
|
2360
2239
|
}
|
|
2361
|
-
/**
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
onSelectShipping(id) {
|
|
2367
|
-
if (this.selectedShipping === id || this.klarnaMounting) return;
|
|
2368
|
-
this.selectedShipping = id;
|
|
2240
|
+
/** Backend names arrive as e.g. "Apple Pay" — compare letters only. */
|
|
2241
|
+
methodEnabled(name) {
|
|
2242
|
+
if (this.availableMethods === null) return true;
|
|
2243
|
+
const norm = (s) => s.toLowerCase().replace(/[^a-z]/g, "");
|
|
2244
|
+
return this.availableMethods.some((m) => norm(m) === norm(name));
|
|
2369
2245
|
}
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
if (
|
|
2378
|
-
|
|
2246
|
+
updated(changed) {
|
|
2247
|
+
if (changed.has("open") && this.open) {
|
|
2248
|
+
this.confirmedOrder = null;
|
|
2249
|
+
this.preparedTotal = -1;
|
|
2250
|
+
}
|
|
2251
|
+
if (this.open && this.itemCount > 0) {
|
|
2252
|
+
const total = this.totalAcrossSponsors;
|
|
2253
|
+
if (total !== this.preparedTotal) {
|
|
2254
|
+
this.preparedTotal = total;
|
|
2255
|
+
void this.refreshApplePay(total);
|
|
2379
2256
|
}
|
|
2380
2257
|
}
|
|
2381
2258
|
}
|
|
2382
|
-
/**
|
|
2383
|
-
async
|
|
2384
|
-
if (
|
|
2385
|
-
|
|
2386
|
-
|
|
2259
|
+
/** Pre-resolve the Apple Pay sheet for the current cart total. */
|
|
2260
|
+
async refreshApplePay(total) {
|
|
2261
|
+
if (total <= 0) {
|
|
2262
|
+
this.applePayHandle = null;
|
|
2263
|
+
this.applePayAvailable = false;
|
|
2264
|
+
return;
|
|
2265
|
+
}
|
|
2387
2266
|
try {
|
|
2388
|
-
await
|
|
2389
|
-
|
|
2390
|
-
this.
|
|
2267
|
+
const handle = await Vio.checkout.prepareApplePayExpress(total, this.primaryCurrency);
|
|
2268
|
+
if (this.preparedTotal !== total) return;
|
|
2269
|
+
this.applePayHandle = handle;
|
|
2270
|
+
this.applePayAvailable = handle.available;
|
|
2271
|
+
} catch {
|
|
2272
|
+
this.applePayHandle = null;
|
|
2273
|
+
this.applePayAvailable = false;
|
|
2391
2274
|
}
|
|
2392
2275
|
}
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2276
|
+
/** Apple Pay express — pay the whole cart, then show the confirmation. */
|
|
2277
|
+
onApplePay() {
|
|
2278
|
+
const handle = this.applePayHandle;
|
|
2279
|
+
if (!handle || !handle.available || this.itemCount === 0) return;
|
|
2280
|
+
const snapshot = this.buildConfirmation();
|
|
2281
|
+
void handle.show().then((result) => {
|
|
2282
|
+
if (!result) return;
|
|
2283
|
+
this.confirmedOrder = snapshot;
|
|
2284
|
+
for (const sponsorId of [...this.carts.keys()]) {
|
|
2285
|
+
Vio.cart.clearSponsorCart(sponsorId);
|
|
2286
|
+
}
|
|
2287
|
+
this.dispatchEvent(
|
|
2288
|
+
new CustomEvent("vio:payment-success", {
|
|
2289
|
+
bubbles: true,
|
|
2290
|
+
composed: true,
|
|
2291
|
+
detail: { method: "apple-pay", result }
|
|
2292
|
+
})
|
|
2293
|
+
);
|
|
2294
|
+
});
|
|
2396
2295
|
}
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
this.
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2296
|
+
buildConfirmation() {
|
|
2297
|
+
const items = [];
|
|
2298
|
+
for (const cart of this.carts.values()) {
|
|
2299
|
+
for (const it of cart.items) {
|
|
2300
|
+
items.push({
|
|
2301
|
+
brand: it.brand,
|
|
2302
|
+
name: it.name,
|
|
2303
|
+
quantity: it.quantity,
|
|
2304
|
+
unitPrice: it.unitPrice,
|
|
2305
|
+
currency: it.currency
|
|
2306
|
+
});
|
|
2405
2307
|
}
|
|
2406
|
-
} finally {
|
|
2407
|
-
this.applePayInProgress = false;
|
|
2408
2308
|
}
|
|
2309
|
+
return { items, total: this.totalAcrossSponsors, currency: this.primaryCurrency };
|
|
2409
2310
|
}
|
|
2410
|
-
|
|
2411
|
-
this.
|
|
2412
|
-
|
|
2413
|
-
void this.onApplePay();
|
|
2414
|
-
return;
|
|
2415
|
-
}
|
|
2416
|
-
Vio.checkout.selectPaymentMethod(method);
|
|
2311
|
+
closeConfirmation() {
|
|
2312
|
+
this.confirmedOrder = null;
|
|
2313
|
+
this.close();
|
|
2417
2314
|
}
|
|
2418
|
-
/**
|
|
2419
|
-
*
|
|
2420
|
-
*
|
|
2421
|
-
*
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2315
|
+
/** Klarna express — opens the side-drawer Klarna checkout for the whole cart,
|
|
2316
|
+
* skipping the address form (leveringsadresse) and method picker
|
|
2317
|
+
* (betalingsmetode). The host's checkout-open handler closes this cart and
|
|
2318
|
+
* opens <vio-checkout> in express mode with Klarna preselected. */
|
|
2319
|
+
onKlarnaExpress() {
|
|
2320
|
+
if (this.itemCount === 0) return;
|
|
2321
|
+
const firstSponsorId = [...this.carts.keys()][0];
|
|
2322
|
+
if (firstSponsorId === void 0) return;
|
|
2323
|
+
this.dispatchEvent(
|
|
2324
|
+
new CustomEvent("vio:checkout-open", {
|
|
2325
|
+
bubbles: true,
|
|
2326
|
+
composed: true,
|
|
2327
|
+
detail: { sponsorId: firstSponsorId, paymentMethod: "klarna", express: true }
|
|
2328
|
+
})
|
|
2329
|
+
);
|
|
2429
2330
|
}
|
|
2430
|
-
/**
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
*/
|
|
2436
|
-
confirmOrder(method, sponsorId, result) {
|
|
2437
|
-
const r = result ?? {};
|
|
2438
|
-
this.confirmedOrder = {
|
|
2439
|
-
items: [...this.items],
|
|
2440
|
-
currency: this.checkoutState?.currency ?? "",
|
|
2441
|
-
total: typeof r.chargedTotal === "number" ? r.chargedTotal : this.checkoutState?.subtotal ?? 0,
|
|
2442
|
-
orderId: r.order?.orderId
|
|
2443
|
-
};
|
|
2444
|
-
this.confirmedMethod = method;
|
|
2445
|
-
this.orderConfirmed = true;
|
|
2446
|
-
this.open = true;
|
|
2331
|
+
/** Vipps — opens the checkout with Vipps preselected. */
|
|
2332
|
+
onVipps() {
|
|
2333
|
+
if (this.itemCount === 0) return;
|
|
2334
|
+
const firstSponsorId = [...this.carts.keys()][0];
|
|
2335
|
+
if (firstSponsorId === void 0) return;
|
|
2447
2336
|
this.dispatchEvent(
|
|
2448
|
-
new CustomEvent("vio:
|
|
2337
|
+
new CustomEvent("vio:checkout-open", {
|
|
2449
2338
|
bubbles: true,
|
|
2450
2339
|
composed: true,
|
|
2451
|
-
detail: {
|
|
2340
|
+
detail: { sponsorId: firstSponsorId, paymentMethod: "vipps", express: false }
|
|
2452
2341
|
})
|
|
2453
2342
|
);
|
|
2454
|
-
Vio.cart.clearSponsorCart(sponsorId);
|
|
2455
2343
|
}
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2344
|
+
/** Stripe — opens the checkout with Stripe preselected. */
|
|
2345
|
+
onStripe() {
|
|
2346
|
+
if (this.itemCount === 0) return;
|
|
2347
|
+
const firstSponsorId = [...this.carts.keys()][0];
|
|
2348
|
+
if (firstSponsorId === void 0) return;
|
|
2349
|
+
this.dispatchEvent(
|
|
2350
|
+
new CustomEvent("vio:checkout-open", {
|
|
2351
|
+
bubbles: true,
|
|
2352
|
+
composed: true,
|
|
2353
|
+
detail: { sponsorId: firstSponsorId, paymentMethod: "stripe", express: false }
|
|
2354
|
+
})
|
|
2355
|
+
);
|
|
2356
|
+
}
|
|
2357
|
+
get totalAcrossSponsors() {
|
|
2358
|
+
let sum = 0;
|
|
2359
|
+
for (const cart of this.carts.values()) {
|
|
2360
|
+
for (const item of cart.items) sum += item.unitPrice * item.quantity;
|
|
2468
2361
|
}
|
|
2362
|
+
return sum;
|
|
2469
2363
|
}
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
return
|
|
2364
|
+
get primaryCurrency() {
|
|
2365
|
+
for (const cart of this.carts.values()) return cart.currency;
|
|
2366
|
+
return "NOK";
|
|
2473
2367
|
}
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
if (!s) return "";
|
|
2477
|
-
return formatPrice(s.subtotal, s.currency);
|
|
2368
|
+
onIncrement(item) {
|
|
2369
|
+
Vio.cart.updateQuantity(item.id, item.sponsorId, item.quantity + 1);
|
|
2478
2370
|
}
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
return this.express ? (this.shippingOption?.price ?? 0) / 100 : 0;
|
|
2371
|
+
onDecrement(item) {
|
|
2372
|
+
Vio.cart.updateQuantity(item.id, item.sponsorId, item.quantity - 1);
|
|
2482
2373
|
}
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
const s = this.checkoutState;
|
|
2486
|
-
if (!s) return "";
|
|
2487
|
-
return formatPrice(s.subtotal + this.shippingMajor(), s.currency);
|
|
2374
|
+
onRemove(item) {
|
|
2375
|
+
Vio.cart.removeItem(item.id, item.sponsorId);
|
|
2488
2376
|
}
|
|
2489
2377
|
render() {
|
|
2378
|
+
const isEmpty = this.itemCount === 0;
|
|
2379
|
+
const sponsorCarts = [...this.carts.values()].filter((c) => c.items.length > 0);
|
|
2490
2380
|
return html`
|
|
2491
2381
|
<div class="backdrop" @click=${this.close}></div>
|
|
2492
|
-
<
|
|
2493
|
-
class="modal ${this.express ? "as-side" : this.orderConfirmed ? "as-confirm" : ""}"
|
|
2494
|
-
role="dialog"
|
|
2495
|
-
aria-label=${this.heading}
|
|
2496
|
-
>
|
|
2382
|
+
<aside class="drawer" role="dialog" aria-label=${this.heading}>
|
|
2497
2383
|
<div class="handle" @click=${this.close} aria-hidden="true"></div>
|
|
2498
|
-
<div class="
|
|
2499
|
-
<h2 class="heading">${this.
|
|
2384
|
+
<div class="header">
|
|
2385
|
+
<h2 class="heading">${this.confirmedOrder ? "Takk!" : this.heading}</h2>
|
|
2500
2386
|
<button class="close" @click=${this.close} aria-label="Lukk">×</button>
|
|
2501
2387
|
</div>
|
|
2502
2388
|
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2389
|
+
${this.confirmedOrder ? this.renderConfirmation() : isEmpty ? html`<div class="empty">${this.emptyLabel}</div>` : html`
|
|
2390
|
+
<div class="body">
|
|
2391
|
+
${sponsorCarts.map(
|
|
2392
|
+
(cart) => html`
|
|
2393
|
+
<div class="sponsor-group">
|
|
2394
|
+
${cart.sponsorName ? html`<div class="sponsor-label">${cart.sponsorName}</div>` : ""}
|
|
2395
|
+
${cart.items.map(
|
|
2396
|
+
(item) => html`
|
|
2397
|
+
<div class="item">
|
|
2398
|
+
<div class="item-image">
|
|
2399
|
+
${item.imageUrl ? html`<img src=${item.imageUrl} alt=${item.name} />` : ""}
|
|
2400
|
+
</div>
|
|
2401
|
+
<div>
|
|
2402
|
+
<div class="item-brand">${item.brand}</div>
|
|
2403
|
+
<div class="item-name">${item.name}</div>
|
|
2404
|
+
<div class="item-row">
|
|
2405
|
+
<span class="item-qty">
|
|
2406
|
+
<button
|
|
2407
|
+
class="qty-btn"
|
|
2408
|
+
@click=${() => this.onDecrement(item)}
|
|
2409
|
+
aria-label="Reduser"
|
|
2410
|
+
>
|
|
2411
|
+
−
|
|
2412
|
+
</button>
|
|
2413
|
+
${item.quantity}
|
|
2414
|
+
<button
|
|
2415
|
+
class="qty-btn"
|
|
2416
|
+
@click=${() => this.onIncrement(item)}
|
|
2417
|
+
aria-label="Øk"
|
|
2418
|
+
>
|
|
2419
|
+
+
|
|
2420
|
+
</button>
|
|
2421
|
+
<button
|
|
2422
|
+
class="qty-btn"
|
|
2423
|
+
@click=${() => this.onRemove(item)}
|
|
2424
|
+
aria-label="Fjern"
|
|
2425
|
+
style="margin-left: 8px;"
|
|
2426
|
+
>
|
|
2427
|
+
×
|
|
2428
|
+
</button>
|
|
2429
|
+
</span>
|
|
2430
|
+
<span class="item-price">
|
|
2431
|
+
${formatPrice(
|
|
2432
|
+
item.unitPrice * item.quantity,
|
|
2433
|
+
item.currency
|
|
2434
|
+
)}
|
|
2435
|
+
</span>
|
|
2436
|
+
</div>
|
|
2437
|
+
</div>
|
|
2438
|
+
</div>
|
|
2439
|
+
`
|
|
2440
|
+
)}
|
|
2526
2441
|
</div>
|
|
2527
2442
|
`
|
|
2528
2443
|
)}
|
|
2529
|
-
|
|
2530
|
-
|
|
2444
|
+
</div>
|
|
2445
|
+
<div class="footer">
|
|
2446
|
+
<div class="subtotal-row">
|
|
2447
|
+
<span class="subtotal-label">${this.subtotalLabel}</span>
|
|
2448
|
+
<span class="subtotal-value">
|
|
2449
|
+
${formatPrice(this.totalAcrossSponsors, this.primaryCurrency)}
|
|
2450
|
+
</span>
|
|
2531
2451
|
</div>
|
|
2452
|
+
${this.methodEnabled("apple-pay") && this.applePayAvailable ? html`
|
|
2453
|
+
<button
|
|
2454
|
+
class="applepay-btn"
|
|
2455
|
+
@click=${this.onApplePay}
|
|
2456
|
+
aria-label="Betal med Apple Pay"
|
|
2457
|
+
>
|
|
2458
|
+
<svg class="ap-logo" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M17.05 20.28c-.98.95-2.05.8-3.08.35-1.09-.46-2.09-.48-3.24 0-1.44.62-2.2.44-3.06-.35C2.79 15.25 3.51 7.59 9.05 7.31c1.35.07 2.29.74 3.08.8 1.18-.24 2.31-.93 3.57-.84 1.51.12 2.65.72 3.4 1.8-3.12 1.87-2.38 5.98.48 7.13-.57 1.5-1.31 2.99-2.54 4.09l.01-.01zM12.03 7.25c-.15-2.23 1.66-4.07 3.74-4.25.29 2.58-2.34 4.5-3.74 4.25z"/></svg>
|
|
2459
|
+
Apple Pay
|
|
2460
|
+
</button>
|
|
2461
|
+
` : ""}
|
|
2462
|
+
${this.methodEnabled("stripe") ? html`
|
|
2463
|
+
<button
|
|
2464
|
+
class="stripe-btn"
|
|
2465
|
+
@click=${this.onStripe}
|
|
2466
|
+
aria-label="Betal med Stripe"
|
|
2467
|
+
>
|
|
2468
|
+
<img class="stripe-logo" src=${STRIPE_LOGO_SRC2} alt="Stripe" />
|
|
2469
|
+
</button>
|
|
2470
|
+
` : ""}
|
|
2471
|
+
${this.methodEnabled("klarna") ? html`
|
|
2472
|
+
<button
|
|
2473
|
+
class="klarna-btn"
|
|
2474
|
+
@click=${this.onKlarnaExpress}
|
|
2475
|
+
aria-label="Betal med Klarna"
|
|
2476
|
+
>
|
|
2477
|
+
Kjøp med
|
|
2478
|
+
<img
|
|
2479
|
+
class="klarna-logo"
|
|
2480
|
+
src="https://x.klarnacdn.net/payment-method/assets/badges/generic/klarna.svg"
|
|
2481
|
+
alt="Klarna"
|
|
2482
|
+
/>
|
|
2483
|
+
</button>
|
|
2484
|
+
` : ""}
|
|
2485
|
+
${this.methodEnabled("vipps") ? html`
|
|
2486
|
+
<button
|
|
2487
|
+
class="vipps-btn"
|
|
2488
|
+
@click=${this.onVipps}
|
|
2489
|
+
aria-label="Betal med Vipps"
|
|
2490
|
+
>
|
|
2491
|
+
<span class="vipps-logo">vipps</span>
|
|
2492
|
+
</button>
|
|
2493
|
+
` : ""}
|
|
2532
2494
|
</div>
|
|
2533
|
-
`
|
|
2534
|
-
|
|
2535
|
-
</section>
|
|
2495
|
+
`}
|
|
2496
|
+
</aside>
|
|
2536
2497
|
`;
|
|
2537
2498
|
}
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2499
|
+
renderConfirmation() {
|
|
2500
|
+
const o = this.confirmedOrder;
|
|
2501
|
+
if (!o) return html``;
|
|
2541
2502
|
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
|
-
)}
|
|
2503
|
+
<div class="confirm">
|
|
2504
|
+
<div class="confirm-check" aria-hidden="true">✓</div>
|
|
2505
|
+
<h3>Takk for handelen!</h3>
|
|
2506
|
+
<p>Betaling med Apple Pay er bekreftet.</p>
|
|
2507
|
+
<div class="confirm-summary">
|
|
2508
|
+
${o.items.map(
|
|
2509
|
+
(it) => html`
|
|
2510
|
+
<div class="order-row">
|
|
2511
|
+
<span>${it.brand ? `${it.brand} ` : ""}${it.name} ×${it.quantity}</span>
|
|
2512
|
+
<span>${formatPrice(it.unitPrice * it.quantity, it.currency)}</span>
|
|
2561
2513
|
</div>
|
|
2562
|
-
`
|
|
2563
|
-
${this.klarnaCategories.length > 1 ? html`
|
|
2564
|
-
<div class="klarna-cats">
|
|
2565
|
-
${this.klarnaCategories.map(
|
|
2566
|
-
(c) => html`
|
|
2567
|
-
<button
|
|
2568
|
-
class="klarna-cat ${this.klarnaSelectedCat === c.identifier ? "active" : ""}"
|
|
2569
|
-
@click=${() => void this.onKlarnaCategory(c.identifier)}
|
|
2570
|
-
>
|
|
2571
|
-
${c.name}
|
|
2572
|
-
</button>
|
|
2573
|
-
`
|
|
2514
|
+
`
|
|
2574
2515
|
)}
|
|
2575
|
-
|
|
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>
|
|
2516
|
+
<div class="order-row total">
|
|
2517
|
+
<span>Totalt</span>
|
|
2518
|
+
<span>${formatPrice(o.total, o.currency)}</span>
|
|
2519
|
+
</div>
|
|
2520
|
+
</div>
|
|
2521
|
+
<button class="confirm-close" @click=${this.closeConfirmation}>Lukk</button>
|
|
2770
2522
|
</div>
|
|
2771
2523
|
`;
|
|
2772
2524
|
}
|
|
2773
2525
|
};
|
|
2774
|
-
|
|
2526
|
+
__decorateClass([
|
|
2527
|
+
property({ type: Boolean, reflect: true })
|
|
2528
|
+
], VioCart.prototype, "open", 2);
|
|
2529
|
+
__decorateClass([
|
|
2530
|
+
property({ type: String })
|
|
2531
|
+
], VioCart.prototype, "heading", 2);
|
|
2532
|
+
__decorateClass([
|
|
2533
|
+
property({ type: String, attribute: "empty-label" })
|
|
2534
|
+
], VioCart.prototype, "emptyLabel", 2);
|
|
2535
|
+
__decorateClass([
|
|
2536
|
+
property({ type: String, attribute: "subtotal-label" })
|
|
2537
|
+
], VioCart.prototype, "subtotalLabel", 2);
|
|
2538
|
+
__decorateClass([
|
|
2539
|
+
state()
|
|
2540
|
+
], VioCart.prototype, "carts", 2);
|
|
2541
|
+
__decorateClass([
|
|
2542
|
+
state()
|
|
2543
|
+
], VioCart.prototype, "itemCount", 2);
|
|
2544
|
+
__decorateClass([
|
|
2545
|
+
state()
|
|
2546
|
+
], VioCart.prototype, "applePayAvailable", 2);
|
|
2547
|
+
__decorateClass([
|
|
2548
|
+
state()
|
|
2549
|
+
], VioCart.prototype, "availableMethods", 2);
|
|
2550
|
+
__decorateClass([
|
|
2551
|
+
state()
|
|
2552
|
+
], VioCart.prototype, "confirmedOrder", 2);
|
|
2553
|
+
var STRIPE_LOGO_SRC3 = "data:image/svg+xml;utf8," + encodeURIComponent(
|
|
2554
|
+
'<svg width="360" height="150" viewBox="0 0 360 150" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M360 77.4001C360 51.8001 347.6 31.6001 323.9 31.6001C300.1 31.6001 285.7 51.8001 285.7 77.2001C285.7 107.3 302.7 122.5 327.1 122.5C339 122.5 348 119.8 354.8 116V96.0001C348 99.4001 340.2 101.5 330.3 101.5C320.6 101.5 312 98.1001 310.9 86.3001H359.8C359.8 85.0001 360 79.8001 360 77.4001ZM310.6 67.9001C310.6 56.6001 317.5 51.9001 323.8 51.9001C329.9 51.9001 336.4 56.6001 336.4 67.9001H310.6Z" fill="#533AFD"/><path fill-rule="evenodd" clip-rule="evenodd" d="M247.1 31.6001C237.3 31.6001 231 36.2001 227.5 39.4001L226.2 33.2001H204.2V149.8L229.2 144.5L229.3 116.2C232.9 118.8 238.2 122.5 247 122.5C264.9 122.5 281.2 108.1 281.2 76.4001C281.1 47.4001 264.6 31.6001 247.1 31.6001ZM241.1 100.5C235.2 100.5 231.7 98.4001 229.3 95.8001L229.2 58.7001C231.8 55.8001 235.4 53.8001 241.1 53.8001C250.2 53.8001 256.5 64.0001 256.5 77.1001C256.5 90.5001 250.3 100.5 241.1 100.5Z" fill="#533AFD"/><path fill-rule="evenodd" clip-rule="evenodd" d="M169.8 25.7L194.9 20.3V0L169.8 5.3V25.7Z" fill="#533AFD"/><path d="M194.9 33.3H169.8V120.8H194.9V33.3Z" fill="#533AFD"/><path fill-rule="evenodd" clip-rule="evenodd" d="M142.9 40.7L141.3 33.3H119.7V120.8H144.7V61.5C150.6 53.8 160.6 55.2 163.7 56.3V33.3C160.5 32.1 148.8 29.9 142.9 40.7Z" fill="#533AFD"/><path fill-rule="evenodd" clip-rule="evenodd" d="M92.8999 11.6001L68.4999 16.8001L68.3999 96.9001C68.3999 111.7 79.4999 122.6 94.2999 122.6C102.5 122.6 108.5 121.1 111.8 119.3V99.0001C108.6 100.3 92.7999 104.9 92.7999 90.1001V54.6001H111.8V33.3001H92.7999L92.8999 11.6001Z" fill="#533AFD"/><path fill-rule="evenodd" clip-rule="evenodd" d="M25.3 58.7001C25.3 54.8001 28.5 53.3001 33.8 53.3001C41.4 53.3001 51 55.6001 58.6 59.7001V36.2001C50.3 32.9001 42.1 31.6001 33.8 31.6001C13.5 31.6001 0 42.2001 0 59.9001C0 87.5001 38 83.1001 38 95.0001C38 99.6001 34 101.1 28.4 101.1C20.1 101.1 9.5 97.7001 1.1 93.1001V116.9C10.4 120.9 19.8 122.6 28.4 122.6C49.2 122.6 63.5 112.3 63.5 94.4001C63.4 64.6001 25.3 69.9001 25.3 58.7001Z" fill="#533AFD"/></svg>'
|
|
2555
|
+
);
|
|
2556
|
+
var _VioCheckout = class _VioCheckout extends LitElement {
|
|
2557
|
+
constructor() {
|
|
2558
|
+
super(...arguments);
|
|
2559
|
+
this.open = false;
|
|
2560
|
+
this.heading = "Kasse";
|
|
2561
|
+
this.shippingLabel = "Beregnes ved kasse";
|
|
2562
|
+
this.express = false;
|
|
2563
|
+
this.checkoutState = null;
|
|
2564
|
+
this.items = [];
|
|
2565
|
+
this.applePayAvailable = false;
|
|
2566
|
+
this.applePayInProgress = false;
|
|
2567
|
+
this.klarnaAvailable = false;
|
|
2568
|
+
this.orderConfirmed = false;
|
|
2569
|
+
this.confirmedMethod = null;
|
|
2570
|
+
this.paymentError = null;
|
|
2571
|
+
this.paymentNotice = null;
|
|
2572
|
+
this.confirmedOrder = null;
|
|
2573
|
+
/** Active Klarna Payments widget handle + the subtotal it was mounted for. */
|
|
2574
|
+
this.klarnaHandle = null;
|
|
2575
|
+
this.klarnaMountedAmount = null;
|
|
2576
|
+
this.klarnaMountedShipping = null;
|
|
2577
|
+
this.klarnaMounting = false;
|
|
2578
|
+
this.klarnaCategories = [];
|
|
2579
|
+
this.klarnaSelectedCat = "";
|
|
2580
|
+
this.klarnaAuthorizing = false;
|
|
2581
|
+
this.selectedShipping = KLARNA_SHIPPING_OPTIONS.find((o) => o.preselected)?.id ?? KLARNA_SHIPPING_OPTIONS[0]?.id ?? "";
|
|
2582
|
+
this.availableShippingsList = [];
|
|
2583
|
+
this.availableMethods = null;
|
|
2584
|
+
this.stripeLoading = false;
|
|
2585
|
+
this.vippsLoading = false;
|
|
2586
|
+
this.form = {
|
|
2587
|
+
firstName: "",
|
|
2588
|
+
lastName: "",
|
|
2589
|
+
email: "",
|
|
2590
|
+
address: "",
|
|
2591
|
+
postalCode: "",
|
|
2592
|
+
city: ""
|
|
2593
|
+
};
|
|
2594
|
+
/** In-flight guard for loadAvailablePaymentMethods. */
|
|
2595
|
+
this.loadingPaymentMethods = false;
|
|
2596
|
+
/** In-flight + attempted guards for loadAvailableShippings (change events
|
|
2597
|
+
* fire per keystroke — without these, typing an address hammers the API). */
|
|
2598
|
+
this.loadingShippings = false;
|
|
2599
|
+
this.shippingsAttempted = false;
|
|
2600
|
+
this.boundOnCheckoutChange = (e) => {
|
|
2601
|
+
const detail = e.detail;
|
|
2602
|
+
const prevSponsorId = this.checkoutState?.sponsorId;
|
|
2603
|
+
const sponsorChanged = !prevSponsorId || prevSponsorId !== detail.state?.sponsorId;
|
|
2604
|
+
this.checkoutState = detail.state;
|
|
2605
|
+
this.items = Vio.checkout.items;
|
|
2606
|
+
if (this.checkoutState) {
|
|
2607
|
+
if (sponsorChanged || this.availableMethods === null) {
|
|
2608
|
+
void this.refreshApplePay();
|
|
2609
|
+
void this.refreshKlarna();
|
|
2610
|
+
void this.loadAvailablePaymentMethods();
|
|
2611
|
+
void this.loadAvailableShippings();
|
|
2612
|
+
} else if (!this.shippingsAttempted) {
|
|
2613
|
+
void this.loadAvailableShippings();
|
|
2614
|
+
}
|
|
2615
|
+
} else {
|
|
2616
|
+
this.applePayAvailable = false;
|
|
2617
|
+
this.klarnaAvailable = false;
|
|
2618
|
+
this.orderConfirmed = false;
|
|
2619
|
+
this.confirmedMethod = null;
|
|
2620
|
+
this.confirmedOrder = null;
|
|
2621
|
+
this.paymentError = null;
|
|
2622
|
+
this.paymentNotice = null;
|
|
2623
|
+
this.express = false;
|
|
2624
|
+
this.availableMethods = null;
|
|
2625
|
+
this.unmountKlarna();
|
|
2626
|
+
}
|
|
2627
|
+
};
|
|
2628
|
+
this.boundOnPaymentComplete = (e) => {
|
|
2629
|
+
const detail = e.detail;
|
|
2630
|
+
if (detail.method !== "klarna" && detail.method !== "apple-pay") return;
|
|
2631
|
+
const sponsorId = detail.state?.sponsorId ?? this.checkoutState?.sponsorId;
|
|
2632
|
+
if (sponsorId == null) return;
|
|
2633
|
+
this.confirmOrder(detail.method, sponsorId, detail.result);
|
|
2634
|
+
};
|
|
2635
|
+
this.boundOnPaymentError = (e) => {
|
|
2636
|
+
const detail = e.detail;
|
|
2637
|
+
this.paymentError = detail.method === "klarna" ? `Klarna-betalingen kunne ikke fullf\xF8res: ${detail.error}` : detail.error;
|
|
2638
|
+
this.open = true;
|
|
2639
|
+
};
|
|
2640
|
+
}
|
|
2641
|
+
static {
|
|
2642
|
+
this.styles = css`
|
|
2775
2643
|
:host { display: contents; }
|
|
2776
2644
|
.backdrop {
|
|
2777
2645
|
position: fixed;
|
|
@@ -2834,6 +2702,21 @@ VioCheckout.styles = css`
|
|
|
2834
2702
|
box-shadow: -8px 0 24px rgba(0, 0, 0, 0.1);
|
|
2835
2703
|
}
|
|
2836
2704
|
:host([open]) .modal.as-side { transform: translateX(0); }
|
|
2705
|
+
|
|
2706
|
+
/* Desktop: the checkout ALWAYS lives as a right-side panel (like the cart,
|
|
2707
|
+
product detail and express drawer) — never a full-width overlay. Only the
|
|
2708
|
+
compact confirmation card stays centered. */
|
|
2709
|
+
@media (min-width: 601px) {
|
|
2710
|
+
.modal:not(.as-confirm) {
|
|
2711
|
+
inset: 0 0 0 auto;
|
|
2712
|
+
width: min(440px, 100%);
|
|
2713
|
+
max-width: 440px;
|
|
2714
|
+
transform: translateX(100%);
|
|
2715
|
+
border-radius: 0;
|
|
2716
|
+
box-shadow: -8px 0 24px rgba(0, 0, 0, 0.1);
|
|
2717
|
+
}
|
|
2718
|
+
:host([open]) .modal:not(.as-confirm) { transform: translateX(0); }
|
|
2719
|
+
}
|
|
2837
2720
|
@media (max-width: 600px) {
|
|
2838
2721
|
/* On phones, keep it a bottom sheet for reachability. */
|
|
2839
2722
|
.modal.as-side {
|
|
@@ -3014,8 +2897,9 @@ VioCheckout.styles = css`
|
|
|
3014
2897
|
background: var(--vio-color-surface-hover, #fafafa);
|
|
3015
2898
|
}
|
|
3016
2899
|
.payment-btn:disabled {
|
|
3017
|
-
cursor:
|
|
3018
|
-
opacity: 0.
|
|
2900
|
+
cursor: not-allowed;
|
|
2901
|
+
opacity: 0.45;
|
|
2902
|
+
pointer-events: none;
|
|
3019
2903
|
}
|
|
3020
2904
|
.payment-btn.primary {
|
|
3021
2905
|
background: var(--vio-color-text, #0a0a0a);
|
|
@@ -3099,9 +2983,27 @@ VioCheckout.styles = css`
|
|
|
3099
2983
|
color: var(--vio-color-accent, #c14a3b);
|
|
3100
2984
|
text-align: center;
|
|
3101
2985
|
}
|
|
2986
|
+
.payment-notice {
|
|
2987
|
+
grid-column: 1 / -1;
|
|
2988
|
+
padding: 12px 16px;
|
|
2989
|
+
background: #f4f4f2;
|
|
2990
|
+
border: 1px solid var(--vio-color-border, #ddd);
|
|
2991
|
+
border-radius: 8px;
|
|
2992
|
+
font-size: 13px;
|
|
2993
|
+
color: var(--vio-color-text, #1a1a1a);
|
|
2994
|
+
text-align: center;
|
|
2995
|
+
}
|
|
3102
2996
|
/* "Betal" CTA — sits full-width below the method grid once a method is
|
|
3103
2997
|
picked. Reuses .payment-btn.primary (its grid-column spans the row). */
|
|
3104
|
-
.complete-cta {
|
|
2998
|
+
.complete-cta {
|
|
2999
|
+
width: 100% !important;
|
|
3000
|
+
box-sizing: border-box !important;
|
|
3001
|
+
margin-top: 8px;
|
|
3002
|
+
display: flex !important;
|
|
3003
|
+
align-items: center !important;
|
|
3004
|
+
justify-content: center !important;
|
|
3005
|
+
text-align: center !important;
|
|
3006
|
+
}
|
|
3105
3007
|
|
|
3106
3008
|
/* Order confirmation — compact, shown inside the drawer. */
|
|
3107
3009
|
.confirmation {
|
|
@@ -3169,116 +3071,1152 @@ VioCheckout.styles = css`
|
|
|
3169
3071
|
font-family: inherit;
|
|
3170
3072
|
margin-top: 16px;
|
|
3171
3073
|
}
|
|
3172
|
-
.confirm-close:hover { background: #222; }
|
|
3074
|
+
.confirm-close:hover { background: #222; }
|
|
3075
|
+
|
|
3076
|
+
.order-summary {
|
|
3077
|
+
background: var(--vio-color-surface-muted, #f2f2f2);
|
|
3078
|
+
padding: var(--vio-space-lg, 24px);
|
|
3079
|
+
margin-top: var(--vio-space-xl, 32px);
|
|
3080
|
+
}
|
|
3081
|
+
.order-line {
|
|
3082
|
+
display: grid;
|
|
3083
|
+
grid-template-columns: 56px 1fr auto;
|
|
3084
|
+
gap: 12px;
|
|
3085
|
+
padding: 8px 0;
|
|
3086
|
+
align-items: center;
|
|
3087
|
+
}
|
|
3088
|
+
.order-line-img {
|
|
3089
|
+
aspect-ratio: 1;
|
|
3090
|
+
background: #fff;
|
|
3091
|
+
overflow: hidden;
|
|
3092
|
+
}
|
|
3093
|
+
.order-line-img img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
|
3094
|
+
.order-line-meta { font-size: 13px; }
|
|
3095
|
+
.order-line-meta b { display: block; }
|
|
3096
|
+
.order-line-meta span {
|
|
3097
|
+
color: var(--vio-color-text-secondary, #666);
|
|
3098
|
+
font-size: 11px;
|
|
3099
|
+
text-transform: uppercase;
|
|
3100
|
+
letter-spacing: var(--vio-tracking-label, 0.1em);
|
|
3101
|
+
}
|
|
3102
|
+
.order-line-price { font-weight: 700; font-size: 14px; }
|
|
3103
|
+
.order-row {
|
|
3104
|
+
display: flex;
|
|
3105
|
+
justify-content: space-between;
|
|
3106
|
+
padding: 8px 0;
|
|
3107
|
+
font-size: 14px;
|
|
3108
|
+
}
|
|
3109
|
+
.order-row.total {
|
|
3110
|
+
border-top: 1px solid var(--vio-color-border-default, #ccc);
|
|
3111
|
+
margin-top: var(--vio-space-sm, 8px);
|
|
3112
|
+
padding-top: var(--vio-space-md, 16px);
|
|
3113
|
+
font-weight: 700;
|
|
3114
|
+
font-size: 16px;
|
|
3115
|
+
}
|
|
3116
|
+
|
|
3117
|
+
@media (max-width: 600px) {
|
|
3118
|
+
.form-row { grid-template-columns: 1fr; gap: 0; }
|
|
3119
|
+
.payment-grid { grid-template-columns: 1fr; }
|
|
3120
|
+
.body { padding: 32px var(--vio-space-lg, 24px) 64px; }
|
|
3121
|
+
}
|
|
3122
|
+
`;
|
|
3123
|
+
}
|
|
3124
|
+
connectedCallback() {
|
|
3125
|
+
super.connectedCallback();
|
|
3126
|
+
this.checkoutState = Vio.checkout.currentState;
|
|
3127
|
+
this.items = Vio.checkout.items;
|
|
3128
|
+
Vio.checkout.addEventListener("change", this.boundOnCheckoutChange);
|
|
3129
|
+
Vio.checkout.addEventListener("payment-complete", this.boundOnPaymentComplete);
|
|
3130
|
+
Vio.checkout.addEventListener("payment-error", this.boundOnPaymentError);
|
|
3131
|
+
if (this.checkoutState) {
|
|
3132
|
+
void this.refreshApplePay();
|
|
3133
|
+
void this.refreshKlarna();
|
|
3134
|
+
void this.loadAvailablePaymentMethods();
|
|
3135
|
+
}
|
|
3136
|
+
void Vio.checkout.resumeKlarnaReturn();
|
|
3137
|
+
void this.checkReturnPaymentStatus();
|
|
3138
|
+
}
|
|
3139
|
+
static {
|
|
3140
|
+
/**
|
|
3141
|
+
* Detect the redirect return from a hosted payment page (Stripe / Vipps /
|
|
3142
|
+
* Klarna): the return URL carries vio_payment/vio_method/vio_sponsor. On
|
|
3143
|
+
* success: clear that sponsor's cart, show the confirmation drawer, emit
|
|
3144
|
+
* vio:payment-success (analytics/host hooks) and scrub the URL params.
|
|
3145
|
+
*/
|
|
3146
|
+
/** Backend checkout statuses that mean the payment went through. */
|
|
3147
|
+
this.PAID_STATUSES = /* @__PURE__ */ new Set([
|
|
3148
|
+
"paid",
|
|
3149
|
+
"completed",
|
|
3150
|
+
"complete",
|
|
3151
|
+
"success",
|
|
3152
|
+
"succeeded",
|
|
3153
|
+
"confirmed",
|
|
3154
|
+
"captured"
|
|
3155
|
+
]);
|
|
3156
|
+
}
|
|
3157
|
+
static {
|
|
3158
|
+
/** Statuses that mean the payment definitively did NOT go through. */
|
|
3159
|
+
this.FAILED_STATUSES = /* @__PURE__ */ new Set([
|
|
3160
|
+
"failed",
|
|
3161
|
+
"cancelled",
|
|
3162
|
+
"canceled",
|
|
3163
|
+
"rejected",
|
|
3164
|
+
"declined",
|
|
3165
|
+
"expired",
|
|
3166
|
+
"aborted",
|
|
3167
|
+
"terminated",
|
|
3168
|
+
"error",
|
|
3169
|
+
"voided",
|
|
3170
|
+
"unpaid"
|
|
3171
|
+
]);
|
|
3172
|
+
}
|
|
3173
|
+
static {
|
|
3174
|
+
/** Waits between GetCheckout attempts — PSP webhooks (Vipps especially) can
|
|
3175
|
+
* land seconds after the shopper is redirected back, so a single immediate
|
|
3176
|
+
* check misreads a successful charge as unpaid. ~20s total. */
|
|
3177
|
+
this.RETURN_VERIFY_DELAYS_MS = [0, 2e3, 4e3, 6e3, 8e3];
|
|
3178
|
+
}
|
|
3179
|
+
static {
|
|
3180
|
+
/** Vipps polling cadence — up to 2 minutes. Its webhook can lag well behind
|
|
3181
|
+
* the redirect; this window was verified against a live checkout. */
|
|
3182
|
+
this.VIPPS_STATUS_MAX_ATTEMPTS = 24;
|
|
3183
|
+
}
|
|
3184
|
+
static {
|
|
3185
|
+
this.VIPPS_STATUS_DELAY_MS = 5e3;
|
|
3186
|
+
}
|
|
3187
|
+
/**
|
|
3188
|
+
* Vipps redirects to our return URL the same way on cancel as on success —
|
|
3189
|
+
* the URL alone can't tell them apart, and the generic checkout status can
|
|
3190
|
+
* lag behind Vipps' own webhook. Poll Vipps' own state directly instead:
|
|
3191
|
+
* AUTHORIZED confirms, CREATED means still processing (keep polling),
|
|
3192
|
+
* anything else (including an unrecognized state) is treated as failed —
|
|
3193
|
+
* fail-closed by default rather than guessing.
|
|
3194
|
+
*/
|
|
3195
|
+
async pollVippsStatus(checkoutId, sponsorId) {
|
|
3196
|
+
let sawStatus = false;
|
|
3197
|
+
for (let attempt = 0; attempt < _VioCheckout.VIPPS_STATUS_MAX_ATTEMPTS; attempt++) {
|
|
3198
|
+
if (attempt > 0) {
|
|
3199
|
+
await new Promise((r) => setTimeout(r, _VioCheckout.VIPPS_STATUS_DELAY_MS));
|
|
3200
|
+
}
|
|
3201
|
+
try {
|
|
3202
|
+
const vippsStatus = await Vio.checkout.getVippsStatus(checkoutId, sponsorId || void 0);
|
|
3203
|
+
const state6 = String(vippsStatus?.state ?? "").toUpperCase();
|
|
3204
|
+
sawStatus = true;
|
|
3205
|
+
if (state6 === "AUTHORIZED") return { outcome: "paid", sawStatus };
|
|
3206
|
+
if (state6 === "CREATED") continue;
|
|
3207
|
+
return { outcome: "failed", sawStatus };
|
|
3208
|
+
} catch (err) {
|
|
3209
|
+
if (typeof console !== "undefined") {
|
|
3210
|
+
console.warn("[VioCheckout] GetVippsStatus attempt failed:", err);
|
|
3211
|
+
}
|
|
3212
|
+
}
|
|
3213
|
+
}
|
|
3214
|
+
return { outcome: "pending", sawStatus };
|
|
3215
|
+
}
|
|
3216
|
+
/** Shared outcome handler for both the generic (GetCheckout) and the
|
|
3217
|
+
* Vipps-specific (GetVippsStatus) return-verification paths. */
|
|
3218
|
+
applyReturnOutcome(outcome, sawStatus, vioMethod, sponsorId, checkoutId) {
|
|
3219
|
+
if (outcome === "paid") {
|
|
3220
|
+
if (sponsorId > 0) Vio.cart.clearSponsorCart(sponsorId);
|
|
3221
|
+
this.confirmedOrder = {
|
|
3222
|
+
items: [],
|
|
3223
|
+
currency: getGlobalCurrency(),
|
|
3224
|
+
total: 0,
|
|
3225
|
+
orderId: checkoutId || void 0
|
|
3226
|
+
};
|
|
3227
|
+
this.confirmedMethod = vioMethod;
|
|
3228
|
+
this.orderConfirmed = true;
|
|
3229
|
+
this.open = true;
|
|
3230
|
+
this.dispatchEvent(
|
|
3231
|
+
new CustomEvent("vio:payment-success", {
|
|
3232
|
+
bubbles: true,
|
|
3233
|
+
composed: true,
|
|
3234
|
+
detail: { method: vioMethod, sponsorId, orderId: checkoutId }
|
|
3235
|
+
})
|
|
3236
|
+
);
|
|
3237
|
+
} else if (outcome === "failed") {
|
|
3238
|
+
this.paymentError = "Betalingen ble avbrutt eller feilet. Vennligst pr\xF8v igjen.";
|
|
3239
|
+
this.open = true;
|
|
3240
|
+
} else if (sawStatus) {
|
|
3241
|
+
this.paymentNotice = "Betalingen behandles fortsatt. Du f\xE5r ordrebekreftelse p\xE5 e-post n\xE5r den er gjennomf\xF8rt. Handlekurven er uendret.";
|
|
3242
|
+
this.open = true;
|
|
3243
|
+
} else {
|
|
3244
|
+
this.paymentError = "Vi kunne ikke bekrefte betalingen. Handlekurven er uendret \u2014 pr\xF8v igjen, eller kontakt support hvis du ble belastet.";
|
|
3245
|
+
this.open = true;
|
|
3246
|
+
}
|
|
3247
|
+
}
|
|
3248
|
+
async checkReturnPaymentStatus() {
|
|
3249
|
+
if (typeof window === "undefined" || !window.location || !window.location.search) return;
|
|
3250
|
+
try {
|
|
3251
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
3252
|
+
const vioPayment = urlParams.get("vio_payment");
|
|
3253
|
+
const vioMethod = urlParams.get("vio_method") || "stripe";
|
|
3254
|
+
const sponsorId = Number(urlParams.get("vio_sponsor") || 0);
|
|
3255
|
+
const checkoutId = urlParams.get("checkout_id") || "";
|
|
3256
|
+
if (!vioPayment && !checkoutId) return;
|
|
3257
|
+
if (vioMethod === "klarna" && typeof sessionStorage !== "undefined" && sessionStorage.getItem("vio.klarna.pending.v1")) {
|
|
3258
|
+
this.cleanReturnQueryParams(["vio_payment", "vio_method", "vio_sponsor", "checkout_id"]);
|
|
3259
|
+
return;
|
|
3260
|
+
}
|
|
3261
|
+
if (vioMethod === "vipps" && checkoutId) {
|
|
3262
|
+
if (!this.checkoutState) {
|
|
3263
|
+
const spIdToUse = sponsorId || [...Vio.cart.getAllCarts().keys()][0] || 0;
|
|
3264
|
+
if (spIdToUse) {
|
|
3265
|
+
try {
|
|
3266
|
+
Vio.checkout.open(spIdToUse);
|
|
3267
|
+
Vio.checkout.selectPaymentMethod("vipps");
|
|
3268
|
+
} catch {
|
|
3269
|
+
}
|
|
3270
|
+
}
|
|
3271
|
+
}
|
|
3272
|
+
this.open = true;
|
|
3273
|
+
this.paymentNotice = "Bekrefter betalingen med Vipps\u2026";
|
|
3274
|
+
const { outcome, sawStatus } = await this.pollVippsStatus(checkoutId, sponsorId);
|
|
3275
|
+
this.paymentNotice = null;
|
|
3276
|
+
this.applyReturnOutcome(outcome, sawStatus, vioMethod, sponsorId, checkoutId);
|
|
3277
|
+
this.cleanReturnQueryParams(["vio_payment", "vio_method", "vio_sponsor", "checkout_id"]);
|
|
3278
|
+
} else if (vioPayment === "success") {
|
|
3279
|
+
let outcome = "pending";
|
|
3280
|
+
let sawStatus = false;
|
|
3281
|
+
if (checkoutId) {
|
|
3282
|
+
this.open = true;
|
|
3283
|
+
this.paymentNotice = "Bekrefter betalingen\u2026";
|
|
3284
|
+
for (const delayMs of _VioCheckout.RETURN_VERIFY_DELAYS_MS) {
|
|
3285
|
+
if (delayMs > 0) await new Promise((r) => setTimeout(r, delayMs));
|
|
3286
|
+
try {
|
|
3287
|
+
const checkout = await Vio.checkout.getCheckout(checkoutId, sponsorId || void 0);
|
|
3288
|
+
const status = String(checkout?.status ?? "").toLowerCase();
|
|
3289
|
+
sawStatus = true;
|
|
3290
|
+
if (_VioCheckout.FAILED_STATUSES.has(status)) {
|
|
3291
|
+
outcome = "failed";
|
|
3292
|
+
break;
|
|
3293
|
+
}
|
|
3294
|
+
if (_VioCheckout.PAID_STATUSES.has(status) || Boolean(checkout?.origin_payment_id)) {
|
|
3295
|
+
outcome = "paid";
|
|
3296
|
+
break;
|
|
3297
|
+
}
|
|
3298
|
+
} catch (err) {
|
|
3299
|
+
if (typeof console !== "undefined") {
|
|
3300
|
+
console.warn("[VioCheckout] return verification attempt failed:", err);
|
|
3301
|
+
}
|
|
3302
|
+
}
|
|
3303
|
+
}
|
|
3304
|
+
this.paymentNotice = null;
|
|
3305
|
+
}
|
|
3306
|
+
this.applyReturnOutcome(outcome, sawStatus, vioMethod, sponsorId, checkoutId);
|
|
3307
|
+
this.cleanReturnQueryParams(["vio_payment", "vio_method", "vio_sponsor", "checkout_id"]);
|
|
3308
|
+
} else if (vioPayment === "cancel" || vioPayment === "error") {
|
|
3309
|
+
this.paymentError = "Betalingen ble avbrutt eller feilet. Vennligst pr\xF8v igjen.";
|
|
3310
|
+
this.open = true;
|
|
3311
|
+
this.cleanReturnQueryParams(["vio_payment", "vio_method", "vio_sponsor", "checkout_id"]);
|
|
3312
|
+
}
|
|
3313
|
+
} catch (err) {
|
|
3314
|
+
if (typeof console !== "undefined") {
|
|
3315
|
+
console.warn("[VioCheckout] checkReturnPaymentStatus failed:", err);
|
|
3316
|
+
}
|
|
3317
|
+
}
|
|
3318
|
+
}
|
|
3319
|
+
/** Remove our payment-return params from the URL without a reload. */
|
|
3320
|
+
cleanReturnQueryParams(keys) {
|
|
3321
|
+
try {
|
|
3322
|
+
if (typeof window === "undefined" || !window.history || !window.location) return;
|
|
3323
|
+
const url = new URL(window.location.href);
|
|
3324
|
+
let changed = false;
|
|
3325
|
+
keys.forEach((k) => {
|
|
3326
|
+
if (url.searchParams.has(k)) {
|
|
3327
|
+
url.searchParams.delete(k);
|
|
3328
|
+
changed = true;
|
|
3329
|
+
}
|
|
3330
|
+
});
|
|
3331
|
+
if (changed) {
|
|
3332
|
+
const newSearch = url.searchParams.toString();
|
|
3333
|
+
const newUrl = url.pathname + (newSearch ? "?" + newSearch : "") + url.hash;
|
|
3334
|
+
window.history.replaceState({}, document.title, newUrl);
|
|
3335
|
+
}
|
|
3336
|
+
} catch {
|
|
3337
|
+
}
|
|
3338
|
+
}
|
|
3339
|
+
disconnectedCallback() {
|
|
3340
|
+
Vio.checkout.removeEventListener("change", this.boundOnCheckoutChange);
|
|
3341
|
+
Vio.checkout.removeEventListener("payment-complete", this.boundOnPaymentComplete);
|
|
3342
|
+
Vio.checkout.removeEventListener("payment-error", this.boundOnPaymentError);
|
|
3343
|
+
this.unmountKlarna();
|
|
3344
|
+
super.disconnectedCallback();
|
|
3345
|
+
}
|
|
3346
|
+
updated(changed) {
|
|
3347
|
+
if (changed.has("open") && this.open && this.availableShippingsList.length === 0) {
|
|
3348
|
+
void this.loadAvailableShippings();
|
|
3349
|
+
}
|
|
3350
|
+
void this.mountKlarnaIfNeeded();
|
|
3351
|
+
}
|
|
3352
|
+
close() {
|
|
3353
|
+
this.open = false;
|
|
3354
|
+
this.express = false;
|
|
3355
|
+
Vio.checkout.close();
|
|
3356
|
+
}
|
|
3357
|
+
show() {
|
|
3358
|
+
this.open = true;
|
|
3359
|
+
if (this.availableShippingsList.length === 0) {
|
|
3360
|
+
void this.loadAvailableShippings();
|
|
3361
|
+
}
|
|
3362
|
+
}
|
|
3363
|
+
/** Fetch the real per-supplier shippings and preselect + persist the first. */
|
|
3364
|
+
async loadAvailableShippings() {
|
|
3365
|
+
const spId = this.checkoutState?.sponsorId;
|
|
3366
|
+
if (!spId) return;
|
|
3367
|
+
if (this.loadingShippings) return;
|
|
3368
|
+
this.loadingShippings = true;
|
|
3369
|
+
this.shippingsAttempted = true;
|
|
3370
|
+
try {
|
|
3371
|
+
const shippings = await Vio.checkout.fetchAvailableShippings(spId);
|
|
3372
|
+
if (Array.isArray(shippings) && shippings.length > 0) {
|
|
3373
|
+
this.availableShippingsList = shippings;
|
|
3374
|
+
if (!this.selectedShipping || !shippings.some((s) => s.id === this.selectedShipping)) {
|
|
3375
|
+
const firstOpt = shippings[0];
|
|
3376
|
+
this.selectedShipping = firstOpt.id;
|
|
3377
|
+
if (firstOpt.supplierId) {
|
|
3378
|
+
void Vio.checkout.updateShippingsBySupplier(
|
|
3379
|
+
[{ shipping_id: firstOpt.id, supplier_id: Number(firstOpt.supplierId) }],
|
|
3380
|
+
spId
|
|
3381
|
+
);
|
|
3382
|
+
}
|
|
3383
|
+
}
|
|
3384
|
+
}
|
|
3385
|
+
} catch (err) {
|
|
3386
|
+
if (typeof console !== "undefined") {
|
|
3387
|
+
console.warn("[VioCheckout] loadAvailableShippings failed:", err);
|
|
3388
|
+
}
|
|
3389
|
+
} finally {
|
|
3390
|
+
this.loadingShippings = false;
|
|
3391
|
+
}
|
|
3392
|
+
}
|
|
3393
|
+
/** Whether all mandatory Leveringsadresse fields are filled. */
|
|
3394
|
+
get isAddressValid() {
|
|
3395
|
+
const f = this.form;
|
|
3396
|
+
if (!f) return false;
|
|
3397
|
+
return Boolean(
|
|
3398
|
+
f.firstName && String(f.firstName).trim().length > 0 && f.lastName && String(f.lastName).trim().length > 0 && f.email && String(f.email).trim().length > 0 && String(f.email).includes("@") && f.address && String(f.address).trim().length > 0 && f.postalCode && String(f.postalCode).trim().length > 0 && f.city && String(f.city).trim().length > 0
|
|
3399
|
+
);
|
|
3400
|
+
}
|
|
3401
|
+
/** Whether a valid shipping option is selected (when shippings exist). */
|
|
3402
|
+
get hasSelectedShipping() {
|
|
3403
|
+
if (this.availableShippingsList.length === 0) return true;
|
|
3404
|
+
return Boolean(
|
|
3405
|
+
this.selectedShipping && this.shippingList.some((s) => s.id === this.selectedShipping)
|
|
3406
|
+
);
|
|
3407
|
+
}
|
|
3408
|
+
/** Which payment buttons to render — backend-configured per sponsor. */
|
|
3409
|
+
async loadAvailablePaymentMethods() {
|
|
3410
|
+
if (!this.checkoutState) return;
|
|
3411
|
+
if (this.loadingPaymentMethods) return;
|
|
3412
|
+
this.loadingPaymentMethods = true;
|
|
3413
|
+
try {
|
|
3414
|
+
const methods = await Vio.checkout.getAvailablePaymentMethods(
|
|
3415
|
+
this.checkoutState.sponsorId
|
|
3416
|
+
);
|
|
3417
|
+
if (Array.isArray(methods)) {
|
|
3418
|
+
this.availableMethods = methods.map((m) => m.name);
|
|
3419
|
+
} else {
|
|
3420
|
+
this.availableMethods = null;
|
|
3421
|
+
}
|
|
3422
|
+
} catch (err) {
|
|
3423
|
+
if (typeof console !== "undefined") {
|
|
3424
|
+
console.warn("[VioCheckout] Failed to load payment methods:", err);
|
|
3425
|
+
}
|
|
3426
|
+
this.availableMethods = null;
|
|
3427
|
+
} finally {
|
|
3428
|
+
this.loadingPaymentMethods = false;
|
|
3429
|
+
}
|
|
3430
|
+
}
|
|
3431
|
+
/** Backend names arrive as e.g. "Apple Pay" — compare letters only. */
|
|
3432
|
+
methodEnabled(name) {
|
|
3433
|
+
if (this.availableMethods === null) return true;
|
|
3434
|
+
const norm = (s) => s.toLowerCase().replace(/[^a-z]/g, "");
|
|
3435
|
+
return this.availableMethods.some((m) => norm(m) === norm(name));
|
|
3436
|
+
}
|
|
3437
|
+
async refreshApplePay() {
|
|
3438
|
+
Vio.checkout.clearApplePayCache();
|
|
3439
|
+
this.applePayAvailable = await Vio.checkout.isApplePayAvailable("NO");
|
|
3440
|
+
}
|
|
3441
|
+
async refreshKlarna() {
|
|
3442
|
+
const available = await Vio.checkout.klarnaPaymentsAvailable();
|
|
3443
|
+
if (available !== this.klarnaAvailable) {
|
|
3444
|
+
if (!available) this.unmountKlarna();
|
|
3445
|
+
this.klarnaAvailable = available;
|
|
3446
|
+
}
|
|
3447
|
+
}
|
|
3448
|
+
/** Mount (or re-mount on amount change) the Klarna Payments widget. */
|
|
3449
|
+
async mountKlarnaIfNeeded() {
|
|
3450
|
+
if (!this.klarnaAvailable || !this.open || !this.checkoutState) return;
|
|
3451
|
+
if (this.checkoutState.paymentMethod !== "klarna" && !this.express) return;
|
|
3452
|
+
if (this.klarnaMounting) return;
|
|
3453
|
+
const amount = this.checkoutState.subtotal;
|
|
3454
|
+
if (this.klarnaHandle && this.klarnaMountedAmount === amount && this.klarnaMountedShipping === this.selectedShipping) {
|
|
3455
|
+
return;
|
|
3456
|
+
}
|
|
3457
|
+
const container = this.renderRoot?.querySelector(
|
|
3458
|
+
"#vio-klarna-payments-container"
|
|
3459
|
+
);
|
|
3460
|
+
if (!container) return;
|
|
3461
|
+
this.klarnaMounting = true;
|
|
3462
|
+
this.unmountKlarna();
|
|
3463
|
+
container.innerHTML = "";
|
|
3464
|
+
try {
|
|
3465
|
+
const handle = await Vio.checkout.mountKlarnaPayments(container, {
|
|
3466
|
+
withShipping: this.express,
|
|
3467
|
+
shippingId: this.selectedShipping
|
|
3468
|
+
});
|
|
3469
|
+
this.klarnaHandle = handle;
|
|
3470
|
+
this.klarnaMountedAmount = amount;
|
|
3471
|
+
this.klarnaMountedShipping = this.selectedShipping;
|
|
3472
|
+
this.klarnaCategories = handle.categories;
|
|
3473
|
+
this.klarnaSelectedCat = handle.selected;
|
|
3474
|
+
if (Array.isArray(handle.shippings) && handle.shippings.length > 0) {
|
|
3475
|
+
this.availableShippingsList = handle.shippings;
|
|
3476
|
+
if (!this.selectedShipping || !handle.shippings.some((s) => s.id === this.selectedShipping)) {
|
|
3477
|
+
this.selectedShipping = handle.shippings[0].id;
|
|
3478
|
+
}
|
|
3479
|
+
}
|
|
3480
|
+
} catch (err) {
|
|
3481
|
+
if (typeof console !== "undefined") {
|
|
3482
|
+
console.warn("[VioCheckout] Klarna Payments mount failed:", err);
|
|
3483
|
+
}
|
|
3484
|
+
this.paymentError = `Kunne ikke laste Klarna: ${err instanceof Error ? err.message : String(err)}`;
|
|
3485
|
+
} finally {
|
|
3486
|
+
this.klarnaMounting = false;
|
|
3487
|
+
}
|
|
3488
|
+
}
|
|
3489
|
+
unmountKlarna() {
|
|
3490
|
+
if (this.klarnaHandle) {
|
|
3491
|
+
this.klarnaHandle.unmount();
|
|
3492
|
+
this.klarnaHandle = null;
|
|
3493
|
+
}
|
|
3494
|
+
this.klarnaMountedAmount = null;
|
|
3495
|
+
this.klarnaMountedShipping = null;
|
|
3496
|
+
this.klarnaCategories = [];
|
|
3497
|
+
this.klarnaSelectedCat = "";
|
|
3498
|
+
this.klarnaAuthorizing = false;
|
|
3499
|
+
}
|
|
3500
|
+
/** Shippings in effect: backend per-supplier when loaded, static otherwise. */
|
|
3501
|
+
get shippingList() {
|
|
3502
|
+
return this.availableShippingsList.length > 0 ? this.availableShippingsList : KLARNA_SHIPPING_OPTIONS;
|
|
3503
|
+
}
|
|
3504
|
+
/** Selected shipping option (express flow). */
|
|
3505
|
+
get shippingOption() {
|
|
3506
|
+
const list = this.shippingList;
|
|
3507
|
+
return list.find((o) => o.id === this.selectedShipping) ?? list[0] ?? null;
|
|
3508
|
+
}
|
|
3509
|
+
/** Pick a shipping option → re-mounts the Klarna widget with the new total.
|
|
3510
|
+
* Also persists the choice on the backend cart (per supplier). */
|
|
3511
|
+
async onSelectShipping(id) {
|
|
3512
|
+
if (this.selectedShipping === id || this.klarnaMounting) return;
|
|
3513
|
+
this.selectedShipping = id;
|
|
3514
|
+
const selectedOpt = this.shippingList.find((s) => s.id === id);
|
|
3515
|
+
if (selectedOpt?.supplierId) {
|
|
3516
|
+
try {
|
|
3517
|
+
await Vio.checkout.updateShippingsBySupplier(
|
|
3518
|
+
[{ shipping_id: selectedOpt.id, supplier_id: Number(selectedOpt.supplierId) }],
|
|
3519
|
+
this.checkoutState?.sponsorId
|
|
3520
|
+
);
|
|
3521
|
+
} catch (err) {
|
|
3522
|
+
if (typeof console !== "undefined") {
|
|
3523
|
+
console.warn("[VioCheckout] updateShippingsBySupplier failed:", err);
|
|
3524
|
+
}
|
|
3525
|
+
}
|
|
3526
|
+
}
|
|
3527
|
+
}
|
|
3528
|
+
/** Switch the Klarna widget to a different payment_method_category. */
|
|
3529
|
+
async onKlarnaCategory(identifier) {
|
|
3530
|
+
if (!this.klarnaHandle || this.klarnaSelectedCat === identifier) return;
|
|
3531
|
+
this.klarnaSelectedCat = identifier;
|
|
3532
|
+
try {
|
|
3533
|
+
await this.klarnaHandle.load(identifier);
|
|
3534
|
+
} catch (err) {
|
|
3535
|
+
if (typeof console !== "undefined") {
|
|
3536
|
+
console.warn("[VioCheckout] Klarna category switch failed:", err);
|
|
3537
|
+
}
|
|
3538
|
+
}
|
|
3539
|
+
}
|
|
3540
|
+
/** Authorize the selected Klarna method → backend creates the order. */
|
|
3541
|
+
async onKlarnaAuthorize() {
|
|
3542
|
+
if (!this.klarnaHandle || this.klarnaAuthorizing) return;
|
|
3543
|
+
this.paymentError = null;
|
|
3544
|
+
this.paymentNotice = null;
|
|
3545
|
+
this.klarnaAuthorizing = true;
|
|
3546
|
+
try {
|
|
3547
|
+
await this.klarnaHandle.authorize();
|
|
3548
|
+
} finally {
|
|
3549
|
+
this.klarnaAuthorizing = false;
|
|
3550
|
+
}
|
|
3551
|
+
}
|
|
3552
|
+
onFieldChange(field, value) {
|
|
3553
|
+
this.form = { ...this.form, [field]: value };
|
|
3554
|
+
Vio.checkout.setAddress(this.form);
|
|
3555
|
+
}
|
|
3556
|
+
async onApplePay() {
|
|
3557
|
+
if (this.applePayInProgress) return;
|
|
3558
|
+
this.applePayInProgress = true;
|
|
3559
|
+
try {
|
|
3560
|
+
await Vio.checkout.startApplePay({ country: "NO", label: "Vio" });
|
|
3561
|
+
} catch (err) {
|
|
3562
|
+
if (typeof console !== "undefined") {
|
|
3563
|
+
console.warn("[VioCheckout] Apple Pay flow ended:", err);
|
|
3564
|
+
}
|
|
3565
|
+
} finally {
|
|
3566
|
+
this.applePayInProgress = false;
|
|
3567
|
+
}
|
|
3568
|
+
}
|
|
3569
|
+
async onPay(method) {
|
|
3570
|
+
this.paymentError = null;
|
|
3571
|
+
this.paymentNotice = null;
|
|
3572
|
+
if (method === "apple-pay") {
|
|
3573
|
+
void this.onApplePay();
|
|
3574
|
+
return;
|
|
3575
|
+
}
|
|
3576
|
+
if (method === "vipps") {
|
|
3577
|
+
const email = String(this.form.email ?? "").trim();
|
|
3578
|
+
if (!email || !email.includes("@")) {
|
|
3579
|
+
this.paymentError = "Vennligst fyll inn e-postadressen din.";
|
|
3580
|
+
return;
|
|
3581
|
+
}
|
|
3582
|
+
} else {
|
|
3583
|
+
if (!this.isAddressValid) {
|
|
3584
|
+
this.paymentError = "Vennligst fyll ut alle feltene i leveringsadresse.";
|
|
3585
|
+
return;
|
|
3586
|
+
}
|
|
3587
|
+
if (!this.hasSelectedShipping) {
|
|
3588
|
+
this.paymentError = "Vennligst velg en fraktmetode.";
|
|
3589
|
+
return;
|
|
3590
|
+
}
|
|
3591
|
+
}
|
|
3592
|
+
Vio.checkout.selectPaymentMethod(method);
|
|
3593
|
+
if (method === "stripe") {
|
|
3594
|
+
const selectedOpt = this.shippingList.find((s) => s.id === this.selectedShipping);
|
|
3595
|
+
if (selectedOpt?.supplierId) {
|
|
3596
|
+
try {
|
|
3597
|
+
await Vio.checkout.updateShippingsBySupplier(
|
|
3598
|
+
[{ shipping_id: selectedOpt.id, supplier_id: Number(selectedOpt.supplierId) }],
|
|
3599
|
+
this.checkoutState?.sponsorId
|
|
3600
|
+
);
|
|
3601
|
+
} catch (err) {
|
|
3602
|
+
if (typeof console !== "undefined") {
|
|
3603
|
+
console.warn("[VioCheckout] updateShippingsBySupplier on stripe pay failed:", err);
|
|
3604
|
+
}
|
|
3605
|
+
}
|
|
3606
|
+
}
|
|
3607
|
+
this.stripeLoading = true;
|
|
3608
|
+
void Vio.checkout.startStripePayment(this.checkoutState?.sponsorId, this.form).catch((err) => {
|
|
3609
|
+
this.paymentError = `Kunne ikke starte Stripe-betaling: ${err instanceof Error ? err.message : String(err)}`;
|
|
3610
|
+
}).finally(() => {
|
|
3611
|
+
this.stripeLoading = false;
|
|
3612
|
+
});
|
|
3613
|
+
}
|
|
3614
|
+
if (method === "vipps") {
|
|
3615
|
+
this.vippsLoading = true;
|
|
3616
|
+
void Vio.checkout.startVippsPayment(this.checkoutState?.sponsorId, this.form).catch((err) => {
|
|
3617
|
+
this.paymentError = `Kunne ikke starte Vipps-betaling: ${err instanceof Error ? err.message : String(err)}`;
|
|
3618
|
+
}).finally(() => {
|
|
3619
|
+
this.vippsLoading = false;
|
|
3620
|
+
});
|
|
3621
|
+
}
|
|
3622
|
+
}
|
|
3623
|
+
/**
|
|
3624
|
+
* Complete the order for the selected method. Apple Pay and Klarna Express
|
|
3625
|
+
* finish inside their own sheet/popup; Stripe and Vipps redirect to their
|
|
3626
|
+
* hosted pages (via onPay). Anything else confirms state-only.
|
|
3627
|
+
*/
|
|
3628
|
+
onCompleteOrder() {
|
|
3629
|
+
const s = this.checkoutState;
|
|
3630
|
+
if (!s || !s.paymentMethod) return;
|
|
3631
|
+
if (s.paymentMethod !== "vipps" && !this.isAddressValid) {
|
|
3632
|
+
this.paymentError = "Vennligst fyll ut alle feltene i leveringsadresse.";
|
|
3633
|
+
return;
|
|
3634
|
+
}
|
|
3635
|
+
if (s.paymentMethod === "stripe" || s.paymentMethod === "vipps") {
|
|
3636
|
+
void this.onPay(s.paymentMethod);
|
|
3637
|
+
return;
|
|
3638
|
+
}
|
|
3639
|
+
if (s.paymentMethod !== "klarna" && s.paymentMethod !== "apple-pay") {
|
|
3640
|
+
this.paymentError = "Betalingsmetoden er ikke tilgjengelig enn\xE5.";
|
|
3641
|
+
return;
|
|
3642
|
+
}
|
|
3643
|
+
this.confirmOrder(s.paymentMethod, s.sponsorId);
|
|
3644
|
+
}
|
|
3645
|
+
/**
|
|
3646
|
+
* Shared order confirmation — reached from both the plain "Betal" CTA
|
|
3647
|
+
* (Vipps/Card/Klarna) and the Klarna Express authorize callback. Shows
|
|
3648
|
+
* the confirmation screen, emits `vio:payment-success`, and clears the
|
|
3649
|
+
* paid sponsor's cart so the badge + drawer reflect the purchase.
|
|
3650
|
+
*/
|
|
3651
|
+
confirmOrder(method, sponsorId, result) {
|
|
3652
|
+
const r = result ?? {};
|
|
3653
|
+
this.confirmedOrder = {
|
|
3654
|
+
items: [...this.items],
|
|
3655
|
+
currency: this.checkoutState?.currency ?? "",
|
|
3656
|
+
total: typeof r.chargedTotal === "number" ? r.chargedTotal : this.checkoutState?.subtotal ?? 0,
|
|
3657
|
+
orderId: r.order?.orderId
|
|
3658
|
+
};
|
|
3659
|
+
this.confirmedMethod = method;
|
|
3660
|
+
this.orderConfirmed = true;
|
|
3661
|
+
this.open = true;
|
|
3662
|
+
this.dispatchEvent(
|
|
3663
|
+
new CustomEvent("vio:payment-success", {
|
|
3664
|
+
bubbles: true,
|
|
3665
|
+
composed: true,
|
|
3666
|
+
detail: { method, sponsorId, result }
|
|
3667
|
+
})
|
|
3668
|
+
);
|
|
3669
|
+
Vio.cart.clearSponsorCart(sponsorId);
|
|
3670
|
+
}
|
|
3671
|
+
methodLabel(method) {
|
|
3672
|
+
switch (method) {
|
|
3673
|
+
case "apple-pay":
|
|
3674
|
+
return "Apple Pay";
|
|
3675
|
+
case "klarna":
|
|
3676
|
+
return "Klarna";
|
|
3677
|
+
case "vipps":
|
|
3678
|
+
return "Vipps";
|
|
3679
|
+
case "stripe":
|
|
3680
|
+
return "Stripe";
|
|
3681
|
+
case "card":
|
|
3682
|
+
return "Kort";
|
|
3683
|
+
default:
|
|
3684
|
+
return "";
|
|
3685
|
+
}
|
|
3686
|
+
}
|
|
3687
|
+
/** True for methods that complete via a separate sheet/widget, not the CTA. */
|
|
3688
|
+
isExpressMethod(method) {
|
|
3689
|
+
return method === "apple-pay" || method === "klarna";
|
|
3690
|
+
}
|
|
3691
|
+
orderTotal() {
|
|
3692
|
+
const s = this.checkoutState;
|
|
3693
|
+
if (!s) return "";
|
|
3694
|
+
return formatPrice(s.subtotal, s.currency);
|
|
3695
|
+
}
|
|
3696
|
+
/** Shipping cost (major units) whenever a shipping option is in effect —
|
|
3697
|
+
* express always has one; the normal flow has one once the backend
|
|
3698
|
+
* shippings loaded (the choice IS persisted on the cart and charged). */
|
|
3699
|
+
shippingMajor() {
|
|
3700
|
+
if (!this.express && this.availableShippingsList.length === 0) return 0;
|
|
3701
|
+
const o = this.shippingOption;
|
|
3702
|
+
if (!o) return 0;
|
|
3703
|
+
return o.priceMajor ?? o.price / 100;
|
|
3704
|
+
}
|
|
3705
|
+
/** Total to charge — items + shipping (express). */
|
|
3706
|
+
payTotalLabel() {
|
|
3707
|
+
const s = this.checkoutState;
|
|
3708
|
+
if (!s) return "";
|
|
3709
|
+
return formatPrice(s.subtotal + this.shippingMajor(), s.currency);
|
|
3710
|
+
}
|
|
3711
|
+
render() {
|
|
3712
|
+
return html`
|
|
3713
|
+
<div class="backdrop" @click=${this.close}></div>
|
|
3714
|
+
<div
|
|
3715
|
+
class="modal ${this.express ? "as-side" : this.orderConfirmed ? "as-confirm" : ""}"
|
|
3716
|
+
role="dialog"
|
|
3717
|
+
aria-label=${this.heading}
|
|
3718
|
+
>
|
|
3719
|
+
<div class="handle" @click=${this.close} aria-hidden="true"></div>
|
|
3720
|
+
<div class="topbar">
|
|
3721
|
+
<h2 class="heading">${this.orderConfirmed ? "Takk!" : this.heading}</h2>
|
|
3722
|
+
<button class="close" @click=${this.close} aria-label="Lukk">×</button>
|
|
3723
|
+
</div>
|
|
3724
|
+
|
|
3725
|
+
<div class="body">
|
|
3726
|
+
${this.orderConfirmed ? this.renderConfirmation() : this.express ? this.renderKlarnaExpress() : this.renderCheckoutBody()}
|
|
3727
|
+
</div>
|
|
3728
|
+
</div>
|
|
3729
|
+
`;
|
|
3730
|
+
}
|
|
3731
|
+
renderConfirmation() {
|
|
3732
|
+
const o = this.confirmedOrder;
|
|
3733
|
+
return html`
|
|
3734
|
+
<section class="confirmation">
|
|
3735
|
+
<div class="confirm-check" aria-hidden="true">✓</div>
|
|
3736
|
+
<h3 class="confirm-title">Takk for bestillingen!</h3>
|
|
3737
|
+
<p class="confirm-text">
|
|
3738
|
+
Betaling med ${this.methodLabel(this.confirmedMethod)} er bekreftet.${this.form.email ? html` Kvittering sendes til ${this.form.email}.` : ""}
|
|
3739
|
+
</p>
|
|
3740
|
+
${o?.orderId ? html`<div class="confirm-order-id">Ordrenummer: <b>${o.orderId}</b></div>` : ""}
|
|
3741
|
+
${o && o.items.length > 0 ? html`
|
|
3742
|
+
<div class="confirm-summary">
|
|
3743
|
+
${o.items.map(
|
|
3744
|
+
(it) => html`
|
|
3745
|
+
<div class="order-row">
|
|
3746
|
+
<span>${it.brand ? `${it.brand} ` : ""}${it.name} ×${it.quantity}</span>
|
|
3747
|
+
<span>${formatPrice(it.unitPrice * it.quantity, it.currency)}</span>
|
|
3748
|
+
</div>
|
|
3749
|
+
`
|
|
3750
|
+
)}
|
|
3751
|
+
<div class="order-row total">
|
|
3752
|
+
<span>Totalt</span><span>${formatPrice(o.total, o.currency)}</span>
|
|
3753
|
+
</div>
|
|
3754
|
+
</div>
|
|
3755
|
+
` : ""}
|
|
3756
|
+
<button class="confirm-close" @click=${this.close}>Lukk</button>
|
|
3757
|
+
</section>
|
|
3758
|
+
`;
|
|
3759
|
+
}
|
|
3760
|
+
/** Klarna Payments widget panel: shipping + category chips + widget + pay button. */
|
|
3761
|
+
renderKlarnaPanel() {
|
|
3762
|
+
const currency = this.checkoutState?.currency || getGlobalCurrency();
|
|
3763
|
+
return html`
|
|
3764
|
+
<div class="klarna-panel">
|
|
3765
|
+
${this.express ? html`
|
|
3766
|
+
<div class="ship-select" role="radiogroup" aria-label="Frakt">
|
|
3767
|
+
${this.shippingList.map(
|
|
3768
|
+
(o) => html`
|
|
3769
|
+
<button
|
|
3770
|
+
class="ship-opt ${this.selectedShipping === o.id ? "active" : ""}"
|
|
3771
|
+
role="radio"
|
|
3772
|
+
aria-checked=${this.selectedShipping === o.id}
|
|
3773
|
+
?disabled=${this.klarnaMounting}
|
|
3774
|
+
@click=${() => void this.onSelectShipping(o.id)}
|
|
3775
|
+
>
|
|
3776
|
+
<span class="ship-meta">
|
|
3777
|
+
<b>${o.method || o.name}</b><span>${o.description}</span>
|
|
3778
|
+
</span>
|
|
3779
|
+
<span class="ship-price">
|
|
3780
|
+
${formatPrice(o.priceMajor ?? o.price / 100, o.currency || currency)}
|
|
3781
|
+
</span>
|
|
3782
|
+
</button>
|
|
3783
|
+
`
|
|
3784
|
+
)}
|
|
3785
|
+
</div>
|
|
3786
|
+
` : ""}
|
|
3787
|
+
${this.klarnaCategories.length > 1 ? html`
|
|
3788
|
+
<div class="klarna-cats">
|
|
3789
|
+
${this.klarnaCategories.map(
|
|
3790
|
+
(c) => html`
|
|
3791
|
+
<button
|
|
3792
|
+
class="klarna-cat ${this.klarnaSelectedCat === c.identifier ? "active" : ""}"
|
|
3793
|
+
@click=${() => void this.onKlarnaCategory(c.identifier)}
|
|
3794
|
+
>
|
|
3795
|
+
${c.name}
|
|
3796
|
+
</button>
|
|
3797
|
+
`
|
|
3798
|
+
)}
|
|
3799
|
+
</div>
|
|
3800
|
+
` : ""}
|
|
3801
|
+
${this.klarnaMounting ? html`
|
|
3802
|
+
<div style="padding: 20px; text-align: center; color: var(--vio-color-text-secondary, #666); font-size: 14px;">
|
|
3803
|
+
Klargjør Klarna-betaling…
|
|
3804
|
+
</div>
|
|
3805
|
+
` : ""}
|
|
3806
|
+
<div class="klarna-widget" id="vio-klarna-payments-container"></div>
|
|
3807
|
+
<button
|
|
3808
|
+
class="payment-btn primary complete-cta"
|
|
3809
|
+
@click=${() => void this.onKlarnaAuthorize()}
|
|
3810
|
+
?disabled=${this.klarnaAuthorizing || !this.klarnaHandle || this.klarnaMounting}
|
|
3811
|
+
>
|
|
3812
|
+
${this.klarnaAuthorizing ? "Behandler\u2026" : this.klarnaMounting ? "Klargj\xF8r Klarna\u2026" : `Betal ${this.payTotalLabel()} med Klarna`}
|
|
3813
|
+
</button>
|
|
3814
|
+
</div>
|
|
3815
|
+
`;
|
|
3816
|
+
}
|
|
3817
|
+
/** Express layout — only the Klarna widget + a compact order summary. */
|
|
3818
|
+
renderKlarnaExpress() {
|
|
3819
|
+
const currency = this.checkoutState?.currency || getGlobalCurrency();
|
|
3820
|
+
if (!this.klarnaAvailable) {
|
|
3821
|
+
return html`
|
|
3822
|
+
<section class="section">
|
|
3823
|
+
<h3 class="section-heading">Kjøp med Klarna</h3>
|
|
3824
|
+
<div class="payment-error">
|
|
3825
|
+
Klarna er ikke tilgjengelig akkurat nå (kan være blokkert av nettleseren).
|
|
3826
|
+
</div>
|
|
3827
|
+
<button
|
|
3828
|
+
class="payment-btn primary complete-cta"
|
|
3829
|
+
@click=${() => {
|
|
3830
|
+
this.express = false;
|
|
3831
|
+
}}
|
|
3832
|
+
>
|
|
3833
|
+
Gå til vanlig kasse
|
|
3834
|
+
</button>
|
|
3835
|
+
</section>
|
|
3836
|
+
`;
|
|
3837
|
+
}
|
|
3838
|
+
return html`
|
|
3839
|
+
<section class="section">
|
|
3840
|
+
<h3 class="section-heading">Kjøp med Klarna</h3>
|
|
3841
|
+
${this.items.length > 0 ? html`
|
|
3842
|
+
<div class="express-summary">
|
|
3843
|
+
${this.items.map(
|
|
3844
|
+
(item) => html`
|
|
3845
|
+
<div class="order-row">
|
|
3846
|
+
<span>${item.brand ? `${item.brand} ` : ""}${item.name} ×${item.quantity}</span>
|
|
3847
|
+
<span>${formatPrice(item.unitPrice * item.quantity, item.currency || currency)}</span>
|
|
3848
|
+
</div>
|
|
3849
|
+
`
|
|
3850
|
+
)}
|
|
3851
|
+
<div class="order-row">
|
|
3852
|
+
<span>Frakt${this.shippingOption ? ` \u2013 ${this.shippingOption.method}` : ""}</span>
|
|
3853
|
+
<span>${formatPrice(this.shippingMajor(), currency)}</span>
|
|
3854
|
+
</div>
|
|
3855
|
+
<div class="order-row total">
|
|
3856
|
+
<span>Totalt</span><span>${this.payTotalLabel()}</span>
|
|
3857
|
+
</div>
|
|
3858
|
+
</div>
|
|
3859
|
+
` : ""}
|
|
3860
|
+
${this.renderKlarnaPanel()}
|
|
3861
|
+
${this.paymentNotice ? html`<div class="payment-notice">${this.paymentNotice}</div>` : ""}
|
|
3862
|
+
${this.paymentError ? html`<div class="payment-error">${this.paymentError}</div>` : ""}
|
|
3863
|
+
</section>
|
|
3864
|
+
`;
|
|
3865
|
+
}
|
|
3866
|
+
renderCheckoutBody() {
|
|
3867
|
+
const method = this.checkoutState?.paymentMethod;
|
|
3868
|
+
const showCompleteCta = !!method && !this.isExpressMethod(method);
|
|
3869
|
+
const isVipps = method === "vipps";
|
|
3870
|
+
return html`
|
|
3871
|
+
${!isVipps ? html`
|
|
3872
|
+
<section class="section">
|
|
3873
|
+
<div class="section-label">Steg 1</div>
|
|
3874
|
+
<h3 class="section-heading">Leveringsadresse</h3>
|
|
3875
|
+
<div class="form-row">
|
|
3876
|
+
<div class="field">
|
|
3877
|
+
<label>Fornavn *</label>
|
|
3878
|
+
<input
|
|
3879
|
+
type="text"
|
|
3880
|
+
required
|
|
3881
|
+
autocomplete="given-name"
|
|
3882
|
+
.value=${this.form.firstName}
|
|
3883
|
+
@input=${(e) => this.onFieldChange("firstName", e.target.value)}
|
|
3884
|
+
/>
|
|
3885
|
+
</div>
|
|
3886
|
+
<div class="field">
|
|
3887
|
+
<label>Etternavn *</label>
|
|
3888
|
+
<input
|
|
3889
|
+
type="text"
|
|
3890
|
+
required
|
|
3891
|
+
autocomplete="family-name"
|
|
3892
|
+
.value=${this.form.lastName}
|
|
3893
|
+
@input=${(e) => this.onFieldChange("lastName", e.target.value)}
|
|
3894
|
+
/>
|
|
3895
|
+
</div>
|
|
3896
|
+
</div>
|
|
3897
|
+
<div class="field">
|
|
3898
|
+
<label>E-post *</label>
|
|
3899
|
+
<input
|
|
3900
|
+
type="email"
|
|
3901
|
+
required
|
|
3902
|
+
autocomplete="email"
|
|
3903
|
+
.value=${this.form.email}
|
|
3904
|
+
@input=${(e) => this.onFieldChange("email", e.target.value)}
|
|
3905
|
+
/>
|
|
3906
|
+
</div>
|
|
3907
|
+
<div class="field">
|
|
3908
|
+
<label>Adresse *</label>
|
|
3909
|
+
<input
|
|
3910
|
+
type="text"
|
|
3911
|
+
required
|
|
3912
|
+
autocomplete="street-address"
|
|
3913
|
+
.value=${this.form.address}
|
|
3914
|
+
@input=${(e) => this.onFieldChange("address", e.target.value)}
|
|
3915
|
+
/>
|
|
3916
|
+
</div>
|
|
3917
|
+
<div class="form-row">
|
|
3918
|
+
<div class="field">
|
|
3919
|
+
<label>Postnummer *</label>
|
|
3920
|
+
<input
|
|
3921
|
+
type="text"
|
|
3922
|
+
required
|
|
3923
|
+
autocomplete="postal-code"
|
|
3924
|
+
.value=${this.form.postalCode}
|
|
3925
|
+
@input=${(e) => this.onFieldChange("postalCode", e.target.value)}
|
|
3926
|
+
/>
|
|
3927
|
+
</div>
|
|
3928
|
+
<div class="field">
|
|
3929
|
+
<label>Sted *</label>
|
|
3930
|
+
<input
|
|
3931
|
+
type="text"
|
|
3932
|
+
required
|
|
3933
|
+
autocomplete="address-level2"
|
|
3934
|
+
.value=${this.form.city}
|
|
3935
|
+
@input=${(e) => this.onFieldChange("city", e.target.value)}
|
|
3936
|
+
/>
|
|
3937
|
+
</div>
|
|
3938
|
+
</div>
|
|
3939
|
+
${this.availableShippingsList.length > 0 ? html`
|
|
3940
|
+
<div style="margin-top: 16px;">
|
|
3941
|
+
<label style="display: block; font-weight: 600; font-size: 13px; margin-bottom: 8px;">
|
|
3942
|
+
Fraktmetode *
|
|
3943
|
+
</label>
|
|
3944
|
+
<div class="ship-select">
|
|
3945
|
+
${this.availableShippingsList.map(
|
|
3946
|
+
(o) => html`
|
|
3947
|
+
<button
|
|
3948
|
+
type="button"
|
|
3949
|
+
class="ship-opt ${this.selectedShipping === o.id ? "active" : ""}"
|
|
3950
|
+
@click=${() => void this.onSelectShipping(o.id)}
|
|
3951
|
+
>
|
|
3952
|
+
<span class="ship-meta">
|
|
3953
|
+
<b>${o.method || o.name}</b>
|
|
3954
|
+
${o.description ? html`<span>${o.description}</span>` : ""}
|
|
3955
|
+
</span>
|
|
3956
|
+
<span class="ship-price">
|
|
3957
|
+
${formatPrice(
|
|
3958
|
+
o.priceMajor ?? o.price / 100,
|
|
3959
|
+
o.currency || this.checkoutState?.currency || "NOK"
|
|
3960
|
+
)}
|
|
3961
|
+
</span>
|
|
3962
|
+
</button>
|
|
3963
|
+
`
|
|
3964
|
+
)}
|
|
3965
|
+
</div>
|
|
3966
|
+
</div>
|
|
3967
|
+
` : ""}
|
|
3968
|
+
</section>
|
|
3969
|
+
` : ""}
|
|
3173
3970
|
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
overflow: hidden;
|
|
3190
|
-
}
|
|
3191
|
-
.order-line-img img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
|
3192
|
-
.order-line-meta { font-size: 13px; }
|
|
3193
|
-
.order-line-meta b { display: block; }
|
|
3194
|
-
.order-line-meta span {
|
|
3195
|
-
color: var(--vio-color-text-secondary, #666);
|
|
3196
|
-
font-size: 11px;
|
|
3197
|
-
text-transform: uppercase;
|
|
3198
|
-
letter-spacing: var(--vio-tracking-label, 0.1em);
|
|
3199
|
-
}
|
|
3200
|
-
.order-line-price { font-weight: 700; font-size: 14px; }
|
|
3201
|
-
.order-row {
|
|
3202
|
-
display: flex;
|
|
3203
|
-
justify-content: space-between;
|
|
3204
|
-
padding: 8px 0;
|
|
3205
|
-
font-size: 14px;
|
|
3206
|
-
}
|
|
3207
|
-
.order-row.total {
|
|
3208
|
-
border-top: 1px solid var(--vio-color-border-default, #ccc);
|
|
3209
|
-
margin-top: var(--vio-space-sm, 8px);
|
|
3210
|
-
padding-top: var(--vio-space-md, 16px);
|
|
3211
|
-
font-weight: 700;
|
|
3212
|
-
font-size: 16px;
|
|
3213
|
-
}
|
|
3971
|
+
${isVipps ? html`
|
|
3972
|
+
<section class="section">
|
|
3973
|
+
<div class="section-label">Kontakt</div>
|
|
3974
|
+
<div class="field">
|
|
3975
|
+
<label>E-post * <span style="font-weight:400;opacity:0.6">(for kvittering — adresse fylles ut i Vipps)</span></label>
|
|
3976
|
+
<input
|
|
3977
|
+
type="email"
|
|
3978
|
+
required
|
|
3979
|
+
autocomplete="email"
|
|
3980
|
+
.value=${this.form.email}
|
|
3981
|
+
@input=${(e) => this.onFieldChange("email", e.target.value)}
|
|
3982
|
+
/>
|
|
3983
|
+
</div>
|
|
3984
|
+
</section>
|
|
3985
|
+
` : ""}
|
|
3214
3986
|
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3987
|
+
<section class="section">
|
|
3988
|
+
<div class="section-label">${isVipps ? "Betaling" : "Steg 2"}</div>
|
|
3989
|
+
${method ? html`
|
|
3990
|
+
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px;">
|
|
3991
|
+
<h3 class="section-heading" style="margin:0;">
|
|
3992
|
+
Betalingsmåte: <b>${this.methodLabel(method)}</b>
|
|
3993
|
+
</h3>
|
|
3994
|
+
<button
|
|
3995
|
+
type="button"
|
|
3996
|
+
style="background:none; border:none; color:var(--vio-color-accent, #c14a3b); text-decoration:underline; font-size:13px; cursor:pointer;"
|
|
3997
|
+
@click=${() => {
|
|
3998
|
+
this.unmountKlarna();
|
|
3999
|
+
Vio.checkout.selectPaymentMethod("");
|
|
4000
|
+
}}
|
|
4001
|
+
>
|
|
4002
|
+
Endre
|
|
4003
|
+
</button>
|
|
4004
|
+
</div>
|
|
4005
|
+
|
|
4006
|
+
${method === "klarna" ? this.renderKlarnaPanel() : ""}
|
|
4007
|
+
${method === "stripe" ? html`
|
|
4008
|
+
<button
|
|
4009
|
+
class="payment-btn primary complete-cta"
|
|
4010
|
+
@click=${() => void this.onPay("stripe")}
|
|
4011
|
+
?disabled=${this.stripeLoading || !this.isAddressValid || !this.hasSelectedShipping}
|
|
4012
|
+
title=${!this.isAddressValid ? "Vennligst fyll ut leveringsadresse" : !this.hasSelectedShipping ? "Vennligst velg en fraktmetode" : ""}
|
|
4013
|
+
>
|
|
4014
|
+
${this.stripeLoading ? "G\xE5r til Stripe\u2026" : `Betal ${this.payTotalLabel()} med Stripe`}
|
|
4015
|
+
</button>
|
|
4016
|
+
` : ""}
|
|
4017
|
+
${method === "vipps" ? html`
|
|
4018
|
+
<button
|
|
4019
|
+
class="payment-btn primary complete-cta"
|
|
4020
|
+
@click=${() => void this.onPay("vipps")}
|
|
4021
|
+
?disabled=${this.vippsLoading}
|
|
4022
|
+
>
|
|
4023
|
+
${this.vippsLoading ? "G\xE5r til Vipps\u2026" : `Betal ${this.payTotalLabel()} med Vipps`}
|
|
4024
|
+
</button>
|
|
4025
|
+
` : ""}
|
|
4026
|
+
${method === "apple-pay" ? html`
|
|
4027
|
+
<button
|
|
4028
|
+
class="payment-btn primary complete-cta"
|
|
4029
|
+
@click=${() => void this.onPay("apple-pay")}
|
|
4030
|
+
?disabled=${this.applePayInProgress || !this.isAddressValid || !this.hasSelectedShipping}
|
|
4031
|
+
title=${!this.isAddressValid ? "Vennligst fyll ut leveringsadresse" : !this.hasSelectedShipping ? "Vennligst velg en fraktmetode" : ""}
|
|
4032
|
+
>
|
|
4033
|
+
${this.applePayInProgress ? "\xC5pner\u2026" : `Betal ${this.payTotalLabel()} med Apple Pay`}
|
|
4034
|
+
</button>
|
|
4035
|
+
` : ""}
|
|
4036
|
+
${showCompleteCta && method !== "stripe" && method !== "vipps" ? html`
|
|
4037
|
+
<button
|
|
4038
|
+
class="payment-btn primary complete-cta"
|
|
4039
|
+
@click=${this.onCompleteOrder}
|
|
4040
|
+
?disabled=${!this.isAddressValid || !this.hasSelectedShipping}
|
|
4041
|
+
title=${!this.isAddressValid ? "Vennligst fyll ut leveringsadresse" : !this.hasSelectedShipping ? "Vennligst velg en fraktmetode" : ""}
|
|
4042
|
+
>
|
|
4043
|
+
Betal ${this.payTotalLabel()} med ${this.methodLabel(method ?? null)}
|
|
4044
|
+
</button>
|
|
4045
|
+
` : ""}
|
|
4046
|
+
${!isVipps && !this.isAddressValid ? html`
|
|
4047
|
+
<div style="font-size: 12px; color: var(--vio-color-accent, #c14a3b); text-align: center; margin-top: 8px;">
|
|
4048
|
+
Vennligst fyll ut leveringsadresse for å fullføre betalingen
|
|
4049
|
+
</div>
|
|
4050
|
+
` : !isVipps && !this.hasSelectedShipping ? html`
|
|
4051
|
+
<div style="font-size: 12px; color: var(--vio-color-accent, #c14a3b); text-align: center; margin-top: 8px;">
|
|
4052
|
+
Vennligst velg en fraktmetode for å fullføre betalingen
|
|
4053
|
+
</div>
|
|
4054
|
+
` : ""}
|
|
4055
|
+
` : html`
|
|
4056
|
+
<h3 class="section-heading">Velg betalingsmåte</h3>
|
|
4057
|
+
<div class="payment-grid">
|
|
4058
|
+
${this.methodEnabled("apple-pay") && this.applePayAvailable ? html`
|
|
4059
|
+
<button
|
|
4060
|
+
class="payment-btn primary"
|
|
4061
|
+
@click=${() => this.onPay("apple-pay")}
|
|
4062
|
+
?disabled=${this.applePayInProgress}
|
|
4063
|
+
>
|
|
4064
|
+
${this.applePayInProgress ? "\xC5pner\u2026" : html`<span style="display:inline-flex;align-items:center;gap:6px"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" style="width:18px;height:18px"><path d="M17.05 20.28c-.98.95-2.05.8-3.08.35-1.09-.46-2.09-.48-3.24 0-1.44.62-2.2.44-3.06-.35C2.79 15.25 3.51 7.59 9.05 7.31c1.35.07 2.29.74 3.08.8 1.18-.24 2.31-.93 3.57-.84 1.51.12 2.65.72 3.4 1.8-3.12 1.87-2.38 5.98.48 7.13-.57 1.5-1.31 2.99-2.54 4.09l.01-.01zM12.03 7.25c-.15-2.23 1.66-4.07 3.74-4.25.29 2.58-2.34 4.5-3.74 4.25z"/></svg>Apple Pay</span>`}
|
|
4065
|
+
</button>
|
|
4066
|
+
` : ""}
|
|
4067
|
+
${this.methodEnabled("stripe") ? html`
|
|
4068
|
+
<button
|
|
4069
|
+
class="payment-btn"
|
|
4070
|
+
@click=${() => this.onPay("stripe")}
|
|
4071
|
+
?disabled=${this.stripeLoading}
|
|
4072
|
+
>
|
|
4073
|
+
<img src=${STRIPE_LOGO_SRC3} style="height: 20px; display: block;" alt="Stripe" />
|
|
4074
|
+
</button>
|
|
4075
|
+
` : ""}
|
|
4076
|
+
${this.methodEnabled("klarna") && this.klarnaAvailable ? html`
|
|
4077
|
+
<button class="payment-btn" @click=${() => this.onPay("klarna")}>
|
|
4078
|
+
Klarna
|
|
4079
|
+
</button>
|
|
4080
|
+
` : ""}
|
|
4081
|
+
${this.methodEnabled("vipps") ? html`
|
|
4082
|
+
<button class="payment-btn" @click=${() => this.onPay("vipps")}>
|
|
4083
|
+
<span style="color:#ff5b24;font-weight:800;font-size:17px;letter-spacing:-0.02em">vipps</span>
|
|
4084
|
+
</button>
|
|
4085
|
+
` : ""}
|
|
4086
|
+
${this.methodEnabled("apple-pay") && !this.applePayAvailable ? html`
|
|
4087
|
+
<div class="apple-pay-note">
|
|
4088
|
+
Pay krever Safari på iOS eller macOS med Apple Pay konfigurert.
|
|
4089
|
+
</div>
|
|
4090
|
+
` : ""}
|
|
4091
|
+
</div>
|
|
4092
|
+
`}
|
|
4093
|
+
${this.paymentNotice ? html`<div class="payment-notice">${this.paymentNotice}</div>` : ""}
|
|
4094
|
+
${this.paymentError ? html`<div class="payment-error">${this.paymentError}</div>` : ""}
|
|
4095
|
+
</section>
|
|
4096
|
+
|
|
4097
|
+
${this.items.length > 0 ? html`
|
|
4098
|
+
<section class="order-summary">
|
|
4099
|
+
${this.items.map(
|
|
4100
|
+
(item) => html`
|
|
4101
|
+
<div class="order-line">
|
|
4102
|
+
<div class="order-line-img">
|
|
4103
|
+
${item.imageUrl ? html`<img src=${item.imageUrl} alt=${item.name} />` : ""}
|
|
4104
|
+
</div>
|
|
4105
|
+
<div class="order-line-meta">
|
|
4106
|
+
<span>${item.brand}</span>
|
|
4107
|
+
<b>${item.name}</b>
|
|
4108
|
+
<span>×${item.quantity}</span>
|
|
4109
|
+
</div>
|
|
4110
|
+
<div class="order-line-price">
|
|
4111
|
+
${formatPrice(item.unitPrice * item.quantity, item.currency)}
|
|
4112
|
+
</div>
|
|
4113
|
+
</div>
|
|
4114
|
+
`
|
|
4115
|
+
)}
|
|
4116
|
+
<div class="order-row" style="margin-top: 16px;">
|
|
4117
|
+
<span>Sum</span><span>${this.orderTotal()}</span>
|
|
4118
|
+
</div>
|
|
4119
|
+
<div class="order-row">
|
|
4120
|
+
<span>Frakt${this.shippingMajor() > 0 && this.shippingOption ? ` \u2013 ${this.shippingOption.method || this.shippingOption.name}` : ""}</span>
|
|
4121
|
+
<span>${this.shippingMajor() > 0 ? formatPrice(this.shippingMajor(), this.checkoutState?.currency) : this.shippingLabel}</span>
|
|
4122
|
+
</div>
|
|
4123
|
+
<div class="order-row total">
|
|
4124
|
+
<span>Totalt</span><span>${this.payTotalLabel()}</span>
|
|
4125
|
+
</div>
|
|
4126
|
+
</section>
|
|
4127
|
+
` : ""}
|
|
4128
|
+
</div>
|
|
4129
|
+
</div>
|
|
4130
|
+
`;
|
|
4131
|
+
}
|
|
4132
|
+
};
|
|
3221
4133
|
__decorateClass([
|
|
3222
4134
|
property({ type: Boolean, reflect: true })
|
|
3223
|
-
],
|
|
4135
|
+
], _VioCheckout.prototype, "open", 2);
|
|
3224
4136
|
__decorateClass([
|
|
3225
4137
|
property({ type: String })
|
|
3226
|
-
],
|
|
4138
|
+
], _VioCheckout.prototype, "heading", 2);
|
|
3227
4139
|
__decorateClass([
|
|
3228
4140
|
property({ type: String, attribute: "shipping-label" })
|
|
3229
|
-
],
|
|
4141
|
+
], _VioCheckout.prototype, "shippingLabel", 2);
|
|
3230
4142
|
__decorateClass([
|
|
3231
4143
|
property({ type: Boolean, reflect: true })
|
|
3232
|
-
],
|
|
4144
|
+
], _VioCheckout.prototype, "express", 2);
|
|
4145
|
+
__decorateClass([
|
|
4146
|
+
state()
|
|
4147
|
+
], _VioCheckout.prototype, "checkoutState", 2);
|
|
4148
|
+
__decorateClass([
|
|
4149
|
+
state()
|
|
4150
|
+
], _VioCheckout.prototype, "items", 2);
|
|
3233
4151
|
__decorateClass([
|
|
3234
4152
|
state()
|
|
3235
|
-
],
|
|
4153
|
+
], _VioCheckout.prototype, "applePayAvailable", 2);
|
|
3236
4154
|
__decorateClass([
|
|
3237
4155
|
state()
|
|
3238
|
-
],
|
|
4156
|
+
], _VioCheckout.prototype, "applePayInProgress", 2);
|
|
3239
4157
|
__decorateClass([
|
|
3240
4158
|
state()
|
|
3241
|
-
],
|
|
4159
|
+
], _VioCheckout.prototype, "klarnaAvailable", 2);
|
|
3242
4160
|
__decorateClass([
|
|
3243
4161
|
state()
|
|
3244
|
-
],
|
|
4162
|
+
], _VioCheckout.prototype, "orderConfirmed", 2);
|
|
3245
4163
|
__decorateClass([
|
|
3246
4164
|
state()
|
|
3247
|
-
],
|
|
4165
|
+
], _VioCheckout.prototype, "confirmedMethod", 2);
|
|
3248
4166
|
__decorateClass([
|
|
3249
4167
|
state()
|
|
3250
|
-
],
|
|
4168
|
+
], _VioCheckout.prototype, "paymentError", 2);
|
|
3251
4169
|
__decorateClass([
|
|
3252
4170
|
state()
|
|
3253
|
-
],
|
|
4171
|
+
], _VioCheckout.prototype, "paymentNotice", 2);
|
|
3254
4172
|
__decorateClass([
|
|
3255
4173
|
state()
|
|
3256
|
-
],
|
|
4174
|
+
], _VioCheckout.prototype, "confirmedOrder", 2);
|
|
3257
4175
|
__decorateClass([
|
|
3258
4176
|
state()
|
|
3259
|
-
],
|
|
4177
|
+
], _VioCheckout.prototype, "klarnaCategories", 2);
|
|
3260
4178
|
__decorateClass([
|
|
3261
4179
|
state()
|
|
3262
|
-
],
|
|
4180
|
+
], _VioCheckout.prototype, "klarnaSelectedCat", 2);
|
|
3263
4181
|
__decorateClass([
|
|
3264
4182
|
state()
|
|
3265
|
-
],
|
|
4183
|
+
], _VioCheckout.prototype, "klarnaAuthorizing", 2);
|
|
3266
4184
|
__decorateClass([
|
|
3267
4185
|
state()
|
|
3268
|
-
],
|
|
4186
|
+
], _VioCheckout.prototype, "selectedShipping", 2);
|
|
3269
4187
|
__decorateClass([
|
|
3270
4188
|
state()
|
|
3271
|
-
],
|
|
4189
|
+
], _VioCheckout.prototype, "availableShippingsList", 2);
|
|
3272
4190
|
__decorateClass([
|
|
3273
4191
|
state()
|
|
3274
|
-
],
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
],
|
|
4192
|
+
], _VioCheckout.prototype, "availableMethods", 2);
|
|
4193
|
+
__decorateClass([
|
|
4194
|
+
state()
|
|
4195
|
+
], _VioCheckout.prototype, "stripeLoading", 2);
|
|
4196
|
+
__decorateClass([
|
|
4197
|
+
state()
|
|
4198
|
+
], _VioCheckout.prototype, "vippsLoading", 2);
|
|
4199
|
+
__decorateClass([
|
|
4200
|
+
state()
|
|
4201
|
+
], _VioCheckout.prototype, "form", 2);
|
|
4202
|
+
var VioCheckout = _VioCheckout;
|
|
3278
4203
|
|
|
3279
|
-
// src/ui/
|
|
3280
|
-
|
|
4204
|
+
// src/ui/elements.ts
|
|
4205
|
+
function registerVioElements() {
|
|
4206
|
+
if (typeof customElements === "undefined" || typeof customElements.get !== "function" || typeof customElements.define !== "function")
|
|
4207
|
+
return;
|
|
4208
|
+
const defs = [
|
|
4209
|
+
["vio-product", VioProduct],
|
|
4210
|
+
["vio-product-carousel", VioProductCarousel],
|
|
4211
|
+
["vio-product-detail", VioProductDetail],
|
|
4212
|
+
["vio-cart", VioCart],
|
|
4213
|
+
["vio-checkout", VioCheckout]
|
|
4214
|
+
];
|
|
4215
|
+
for (const [tag, ctor] of defs) {
|
|
4216
|
+
if (!customElements.get(tag)) customElements.define(tag, ctor);
|
|
4217
|
+
}
|
|
4218
|
+
}
|
|
3281
4219
|
|
|
3282
|
-
export {
|
|
3283
|
-
//# sourceMappingURL=chunk-
|
|
3284
|
-
//# sourceMappingURL=chunk-
|
|
4220
|
+
export { VioCart, VioCheckout, VioProduct, VioProductCarousel, VioProductDetail, registerVioElements };
|
|
4221
|
+
//# sourceMappingURL=chunk-NWORYWIL.js.map
|
|
4222
|
+
//# sourceMappingURL=chunk-NWORYWIL.js.map
|