@vio-live/web-sdk 0.2.0 → 0.4.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 +29 -2
- package/dist/chunk-NP2KCM7X.js +3859 -0
- package/dist/chunk-NP2KCM7X.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/{chunk-7JSSYPRN.js → chunk-V5ZQG2H5.js} +1419 -169
- package/dist/chunk-V5ZQG2H5.js.map +1 -0
- package/dist/core/index.d.ts +314 -12
- package/dist/core/index.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -2
- package/dist/react/index.d.ts +2 -1
- package/dist/react/index.js +3 -2
- package/dist/react/index.js.map +1 -1
- package/dist/tokens-CvqQYURw.d.ts +555 -0
- package/dist/ui/index.d.ts +3 -83
- package/dist/ui/index.js +5 -132
- package/dist/ui/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-7JSSYPRN.js.map +0 -1
- package/dist/chunk-GGM2AIK5.js +0 -3
- package/dist/chunk-SOO2G6CF.js +0 -1618
- package/dist/chunk-SOO2G6CF.js.map +0 -1
- package/dist/vio-checkout-CrC5tvmh.d.ts +0 -300
|
@@ -1,7 +1,159 @@
|
|
|
1
|
-
import { Vio, __decorateClass, primaryImageUrl, displayPrice, formatPrice, KLARNA_SHIPPING_OPTIONS } from './chunk-
|
|
1
|
+
import { Vio, __decorateClass, primaryImageUrl, displayPrice, getGlobalCurrency, formatPrice, KLARNA_SHIPPING_OPTIONS } from './chunk-NP2KCM7X.js';
|
|
2
2
|
import { LitElement, css, html } from 'lit';
|
|
3
3
|
import { property, state, query } from 'lit/decorators.js';
|
|
4
4
|
|
|
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
|
+
var THEME_KEY_TO_CSS_VAR = {
|
|
132
|
+
colorText: "--vio-color-text",
|
|
133
|
+
colorTextSecondary: "--vio-color-text-secondary",
|
|
134
|
+
colorTextTertiary: "--vio-color-text-tertiary",
|
|
135
|
+
colorTextOnPrimary: "--vio-color-text-on-primary",
|
|
136
|
+
colorAccent: "--vio-color-accent",
|
|
137
|
+
colorSurface: "--vio-color-surface",
|
|
138
|
+
colorSurfaceMuted: "--vio-color-surface-muted",
|
|
139
|
+
colorSurfaceHover: "--vio-color-surface-hover",
|
|
140
|
+
colorBorder: "--vio-color-border",
|
|
141
|
+
colorBorderDefault: "--vio-color-border-default",
|
|
142
|
+
fontSerif: "--vio-font-serif",
|
|
143
|
+
fontSans: "--vio-font-sans"
|
|
144
|
+
};
|
|
145
|
+
function applyVioTheme(overrides, target = typeof document !== "undefined" ? document.documentElement : null) {
|
|
146
|
+
if (!target) return;
|
|
147
|
+
for (const key of Object.keys(THEME_KEY_TO_CSS_VAR)) {
|
|
148
|
+
const cssVar = THEME_KEY_TO_CSS_VAR[key];
|
|
149
|
+
const value = overrides[key];
|
|
150
|
+
if (value) {
|
|
151
|
+
target.style.setProperty(cssVar, value);
|
|
152
|
+
} else if (key in overrides) {
|
|
153
|
+
target.style.removeProperty(cssVar);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
5
157
|
var VioProduct = class extends LitElement {
|
|
6
158
|
constructor() {
|
|
7
159
|
super(...arguments);
|
|
@@ -13,6 +165,9 @@ var VioProduct = class extends LitElement {
|
|
|
13
165
|
this.productId = "";
|
|
14
166
|
this.sponsorId = "";
|
|
15
167
|
this.hasVariants = false;
|
|
168
|
+
this.variants = void 0;
|
|
169
|
+
this.options = void 0;
|
|
170
|
+
this.hideMeta = false;
|
|
16
171
|
this.inCart = false;
|
|
17
172
|
this.cartQty = 0;
|
|
18
173
|
this.justAdded = false;
|
|
@@ -185,7 +340,7 @@ var VioProduct = class extends LitElement {
|
|
|
185
340
|
* `vio:product-add` so the host adds it (bumping quantity on repeat taps). */
|
|
186
341
|
onAdd(e) {
|
|
187
342
|
e.stopPropagation();
|
|
188
|
-
if (this.hasVariants) {
|
|
343
|
+
if (this.hasVariants || Array.isArray(this.variants) && this.variants.length > 1 || Array.isArray(this.options) && this.options.length > 0) {
|
|
189
344
|
this.onClick();
|
|
190
345
|
return;
|
|
191
346
|
}
|
|
@@ -236,12 +391,13 @@ var VioProduct = class extends LitElement {
|
|
|
236
391
|
</svg>`}
|
|
237
392
|
</button>
|
|
238
393
|
</div>
|
|
394
|
+
${this.hideMeta ? "" : html`
|
|
239
395
|
<div class="meta">
|
|
240
396
|
${this.brand ? html`<div class="brand">${this.brand}</div>` : ""}
|
|
241
397
|
${this.name ? html`<div class="name">${this.name}</div>` : ""}
|
|
242
398
|
${this.price ? html`<div class="price">${this.price}</div>` : ""}
|
|
243
399
|
${this.retailer ? html`<div class="retailer">${this.retailer}</div>` : ""}
|
|
244
|
-
</div
|
|
400
|
+
</div>`}
|
|
245
401
|
</div>
|
|
246
402
|
`;
|
|
247
403
|
}
|
|
@@ -270,6 +426,15 @@ __decorateClass([
|
|
|
270
426
|
__decorateClass([
|
|
271
427
|
property({ type: Boolean, attribute: "has-variants" })
|
|
272
428
|
], VioProduct.prototype, "hasVariants", 2);
|
|
429
|
+
__decorateClass([
|
|
430
|
+
property({ attribute: false })
|
|
431
|
+
], VioProduct.prototype, "variants", 2);
|
|
432
|
+
__decorateClass([
|
|
433
|
+
property({ attribute: false })
|
|
434
|
+
], VioProduct.prototype, "options", 2);
|
|
435
|
+
__decorateClass([
|
|
436
|
+
property({ type: Boolean, attribute: "hide-meta" })
|
|
437
|
+
], VioProduct.prototype, "hideMeta", 2);
|
|
273
438
|
__decorateClass([
|
|
274
439
|
state()
|
|
275
440
|
], VioProduct.prototype, "inCart", 2);
|
|
@@ -625,6 +790,9 @@ function sponsorNameFromBootstrap(sponsorId) {
|
|
|
625
790
|
if (boot.primarySponsor?.id === sponsorId) return boot.primarySponsor.name;
|
|
626
791
|
return boot.secondarySponsors?.find((s) => s.id === sponsorId)?.name;
|
|
627
792
|
}
|
|
793
|
+
var STRIPE_LOGO_SRC = "data:image/svg+xml;utf8," + encodeURIComponent(
|
|
794
|
+
'<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>'
|
|
795
|
+
);
|
|
628
796
|
var VioProductDetail = class extends LitElement {
|
|
629
797
|
constructor() {
|
|
630
798
|
super(...arguments);
|
|
@@ -641,6 +809,12 @@ var VioProductDetail = class extends LitElement {
|
|
|
641
809
|
this.activeImageIndex = 0;
|
|
642
810
|
this.adding = false;
|
|
643
811
|
this.applePayOk = false;
|
|
812
|
+
this.availableMethods = null;
|
|
813
|
+
/** Race guard: only the latest fetch may write state (two quick taps on
|
|
814
|
+
* different cards would otherwise let the slow response win). */
|
|
815
|
+
this.fetchSeq = 0;
|
|
816
|
+
/** Memoised option maps per variant object (recomputed per product). */
|
|
817
|
+
this.optMapCache = /* @__PURE__ */ new WeakMap();
|
|
644
818
|
/** Pre-prepared Apple Pay handle — show() fires synchronously on tap. */
|
|
645
819
|
this.applePayHandle = null;
|
|
646
820
|
this.boundProductClick = (e) => {
|
|
@@ -652,6 +826,7 @@ var VioProductDetail = class extends LitElement {
|
|
|
652
826
|
if (Number.isNaN(sponsorIdNum) || sponsorIdNum <= 0) return;
|
|
653
827
|
this.productId = ev.detail.productId;
|
|
654
828
|
this.sponsorId = sponsorIdNum;
|
|
829
|
+
this.currency = ev.detail.currency || getGlobalCurrency();
|
|
655
830
|
this.show();
|
|
656
831
|
void this.fetchProduct();
|
|
657
832
|
};
|
|
@@ -890,6 +1065,13 @@ var VioProductDetail = class extends LitElement {
|
|
|
890
1065
|
background: var(--vio-color-text, #0a0a0a);
|
|
891
1066
|
color: var(--vio-color-text-on-primary, #fff);
|
|
892
1067
|
}
|
|
1068
|
+
.option-pill.unavailable {
|
|
1069
|
+
opacity: 0.4;
|
|
1070
|
+
cursor: not-allowed;
|
|
1071
|
+
text-decoration: line-through;
|
|
1072
|
+
background: repeating-linear-gradient(-45deg, #fafafa, #fafafa 5px, #f0f0f0 5px, #f0f0f0 10px);
|
|
1073
|
+
}
|
|
1074
|
+
.option-pill.unavailable:hover { border-color: var(--vio-color-border, #e5e5e5); }
|
|
893
1075
|
|
|
894
1076
|
/* Qty + CTA row */
|
|
895
1077
|
.qty-cta-row {
|
|
@@ -960,6 +1142,35 @@ var VioProductDetail = class extends LitElement {
|
|
|
960
1142
|
.buy-klarna .klarna-badge { height: 22px; width: auto; display: block; }
|
|
961
1143
|
.buy-klarna:hover:not(:disabled) { background: #ffa0b9; }
|
|
962
1144
|
.buy-klarna:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
1145
|
+
.buy-applepay .ap-logo { width: 20px; height: 20px; }
|
|
1146
|
+
/* Vipps — brand orange buy button. */
|
|
1147
|
+
.buy-vipps {
|
|
1148
|
+
width: 100%;
|
|
1149
|
+
margin-top: 10px;
|
|
1150
|
+
display: flex;
|
|
1151
|
+
align-items: center;
|
|
1152
|
+
justify-content: center;
|
|
1153
|
+
gap: 8px;
|
|
1154
|
+
background: #ff5b24;
|
|
1155
|
+
color: #fff;
|
|
1156
|
+
border: none;
|
|
1157
|
+
padding: 14px 24px;
|
|
1158
|
+
font-size: 12px;
|
|
1159
|
+
text-transform: uppercase;
|
|
1160
|
+
letter-spacing: var(--vio-tracking-label, 0.1em);
|
|
1161
|
+
font-weight: 700;
|
|
1162
|
+
cursor: pointer;
|
|
1163
|
+
font-family: inherit;
|
|
1164
|
+
transition: background 0.15s;
|
|
1165
|
+
}
|
|
1166
|
+
.buy-vipps .vipps-badge {
|
|
1167
|
+
font-size: 20px;
|
|
1168
|
+
font-weight: 800;
|
|
1169
|
+
letter-spacing: -0.02em;
|
|
1170
|
+
text-transform: none;
|
|
1171
|
+
}
|
|
1172
|
+
.buy-vipps:hover:not(:disabled) { background: #ec4f1c; }
|
|
1173
|
+
.buy-vipps:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
963
1174
|
/* Apple Pay express button — black, per Apple's button guidelines. Only
|
|
964
1175
|
shown in Safari (canApplePay). The glyph is the Apple logo (Safari). */
|
|
965
1176
|
.buy-applepay {
|
|
@@ -1071,49 +1282,262 @@ var VioProductDetail = class extends LitElement {
|
|
|
1071
1282
|
this.product = null;
|
|
1072
1283
|
return;
|
|
1073
1284
|
}
|
|
1285
|
+
const seq = ++this.fetchSeq;
|
|
1074
1286
|
this.isLoading = true;
|
|
1075
1287
|
this.fetchError = "";
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
const values = normalizeValues(opt.values);
|
|
1088
|
-
if (values.length > 0) initial[opt.name] = values[0];
|
|
1089
|
-
}
|
|
1090
|
-
this.selectedOptions = initial;
|
|
1091
|
-
void Vio.checkout.prepareApplePayFor(this.unitPrice * this.quantity, this.currency).then((handle) => {
|
|
1092
|
-
this.applePayHandle = handle;
|
|
1093
|
-
this.applePayOk = handle.available;
|
|
1094
|
-
}).catch(() => {
|
|
1095
|
-
this.applePayHandle = null;
|
|
1096
|
-
this.applePayOk = false;
|
|
1288
|
+
let products = null;
|
|
1289
|
+
let lastError = null;
|
|
1290
|
+
for (let attempt = 0; attempt < 4; attempt++) {
|
|
1291
|
+
try {
|
|
1292
|
+
const activeCurrency = getGlobalCurrency() || this.currency;
|
|
1293
|
+
this.currency = activeCurrency;
|
|
1294
|
+
const commerce = Vio.commerceFor(this.sponsorId);
|
|
1295
|
+
products = await commerce.channel.product.getByIds({
|
|
1296
|
+
product_ids: [productIdNum],
|
|
1297
|
+
currency: activeCurrency,
|
|
1298
|
+
image_size: "large"
|
|
1097
1299
|
});
|
|
1300
|
+
lastError = null;
|
|
1301
|
+
break;
|
|
1302
|
+
} catch (err) {
|
|
1303
|
+
lastError = err;
|
|
1304
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
1098
1305
|
}
|
|
1099
|
-
}
|
|
1100
|
-
|
|
1101
|
-
|
|
1306
|
+
}
|
|
1307
|
+
if (seq !== this.fetchSeq) return;
|
|
1308
|
+
if (lastError) {
|
|
1309
|
+
this.fetchError = lastError instanceof Error ? lastError.message : String(lastError);
|
|
1310
|
+
console.warn("[VioDetail] fetch failed after retries \u2192", this.fetchError);
|
|
1102
1311
|
this.isLoading = false;
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1314
|
+
this.optMapCache = /* @__PURE__ */ new WeakMap();
|
|
1315
|
+
this.product = products?.[0] ?? null;
|
|
1316
|
+
if (!this.product) {
|
|
1317
|
+
this.fetchError = "Fant ikke produktet.";
|
|
1103
1318
|
}
|
|
1319
|
+
if (this.product) {
|
|
1320
|
+
const initial = {};
|
|
1321
|
+
for (const opt of this.product.options ?? []) {
|
|
1322
|
+
const values = normalizeValues(opt.values);
|
|
1323
|
+
if (values.length > 0) initial[opt.name] = values[0];
|
|
1324
|
+
}
|
|
1325
|
+
this.selectedOptions = initial;
|
|
1326
|
+
void Vio.checkout.prepareApplePayFor(this.unitPrice * this.quantity, this.currency).then((handle) => {
|
|
1327
|
+
this.applePayHandle = handle;
|
|
1328
|
+
this.applePayOk = handle.available;
|
|
1329
|
+
}).catch(() => {
|
|
1330
|
+
this.applePayHandle = null;
|
|
1331
|
+
this.applePayOk = false;
|
|
1332
|
+
});
|
|
1333
|
+
void this.loadAvailablePaymentMethods();
|
|
1334
|
+
}
|
|
1335
|
+
this.isLoading = false;
|
|
1336
|
+
}
|
|
1337
|
+
/** Which buy buttons to render — backend-configured per sponsor. */
|
|
1338
|
+
async loadAvailablePaymentMethods() {
|
|
1339
|
+
const spId = this.sponsorId || globalThis.__VIO_SPONSOR_ID__;
|
|
1340
|
+
if (!spId) return;
|
|
1341
|
+
try {
|
|
1342
|
+
const methods = await Vio.checkout.getAvailablePaymentMethods(spId);
|
|
1343
|
+
if (Array.isArray(methods)) {
|
|
1344
|
+
this.availableMethods = methods.map((m) => m.name);
|
|
1345
|
+
} else {
|
|
1346
|
+
this.availableMethods = null;
|
|
1347
|
+
}
|
|
1348
|
+
} catch (err) {
|
|
1349
|
+
if (typeof console !== "undefined") {
|
|
1350
|
+
console.warn("[VioProductDetail] Failed to load payment methods:", err);
|
|
1351
|
+
}
|
|
1352
|
+
this.availableMethods = null;
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
/** Backend names arrive as e.g. "Apple Pay" — compare letters only. */
|
|
1356
|
+
methodEnabled(...names) {
|
|
1357
|
+
if (this.availableMethods === null) return true;
|
|
1358
|
+
const norm = (s) => s.toLowerCase().replace(/[^a-z]/g, "");
|
|
1359
|
+
return this.availableMethods.some((m) => names.some((n) => norm(m) === norm(n)));
|
|
1360
|
+
}
|
|
1361
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
1362
|
+
/**
|
|
1363
|
+
* Build an option-name → value map for a variant, from every shape the feeds
|
|
1364
|
+
* ship: option1/2/3 columns, options/selected_options/variant arrays, plain
|
|
1365
|
+
* object maps, and finally the title (value scan first, position fallback).
|
|
1366
|
+
*/
|
|
1367
|
+
getVariantOptionMap(v, productOptions) {
|
|
1368
|
+
if (v && typeof v === "object") {
|
|
1369
|
+
const cached = this.optMapCache.get(v);
|
|
1370
|
+
if (cached) return cached;
|
|
1371
|
+
}
|
|
1372
|
+
const map = {};
|
|
1373
|
+
if (!v) return { map, titleParts: [] };
|
|
1374
|
+
if (v.option1 != null && productOptions?.[0]) {
|
|
1375
|
+
map[productOptions[0].name.toLowerCase()] = String(v.option1).trim().toLowerCase();
|
|
1376
|
+
}
|
|
1377
|
+
if (v.option2 != null && productOptions?.[1]) {
|
|
1378
|
+
map[productOptions[1].name.toLowerCase()] = String(v.option2).trim().toLowerCase();
|
|
1379
|
+
}
|
|
1380
|
+
if (v.option3 != null && productOptions?.[2]) {
|
|
1381
|
+
map[productOptions[2].name.toLowerCase()] = String(v.option3).trim().toLowerCase();
|
|
1382
|
+
}
|
|
1383
|
+
const optArray = Array.isArray(v.options) ? v.options : Array.isArray(v.selected_options) ? v.selected_options : Array.isArray(v.variant) ? v.variant : null;
|
|
1384
|
+
if (optArray) {
|
|
1385
|
+
for (const item of optArray) {
|
|
1386
|
+
const key = (item.name || item.option || "").trim().toLowerCase();
|
|
1387
|
+
const val = (item.value || "").trim().toLowerCase();
|
|
1388
|
+
if (key && val) map[key] = val;
|
|
1389
|
+
}
|
|
1390
|
+
} else if (v.options && typeof v.options === "object") {
|
|
1391
|
+
for (const [k, val] of Object.entries(v.options)) {
|
|
1392
|
+
map[k.trim().toLowerCase()] = String(val).trim().toLowerCase();
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
const title = String(v.title ?? "").trim();
|
|
1396
|
+
const titleLower = title.toLowerCase();
|
|
1397
|
+
if (Array.isArray(productOptions)) {
|
|
1398
|
+
for (const opt of productOptions) {
|
|
1399
|
+
const key = opt.name.toLowerCase();
|
|
1400
|
+
if (!map[key]) {
|
|
1401
|
+
const rawValues = normalizeValues(opt.values).filter(
|
|
1402
|
+
(val) => !productOptions.some(
|
|
1403
|
+
(other) => other !== opt && normalizeValues(other.values).some(
|
|
1404
|
+
(ov) => ov.trim().toLowerCase() === val.trim().toLowerCase()
|
|
1405
|
+
)
|
|
1406
|
+
)
|
|
1407
|
+
);
|
|
1408
|
+
const sortedVals = [...rawValues].sort((a, b) => b.length - a.length);
|
|
1409
|
+
for (const valStr of sortedVals) {
|
|
1410
|
+
const valLower = valStr.trim().toLowerCase();
|
|
1411
|
+
const escaped = valLower.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1412
|
+
const regex = new RegExp(`(?:^|[/|\\-,;\\s])${escaped}(?:$|[/|\\-,;\\s])`, "i");
|
|
1413
|
+
if (regex.test(titleLower) || titleLower === valLower) {
|
|
1414
|
+
map[key] = valLower;
|
|
1415
|
+
break;
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
const titleParts = title.split(/\s*[/|,]\s*|\s+-\s+/).map((s) => s.trim()).filter(Boolean);
|
|
1422
|
+
if (Array.isArray(productOptions)) {
|
|
1423
|
+
productOptions.forEach((opt, idx) => {
|
|
1424
|
+
const key = opt.name.toLowerCase();
|
|
1425
|
+
if (!map[key] && titleParts[idx] != null) {
|
|
1426
|
+
map[key] = titleParts[idx].toLowerCase();
|
|
1427
|
+
}
|
|
1428
|
+
});
|
|
1429
|
+
}
|
|
1430
|
+
const result = { map, titleParts };
|
|
1431
|
+
if (v && typeof v === "object") this.optMapCache.set(v, result);
|
|
1432
|
+
return result;
|
|
1104
1433
|
}
|
|
1105
|
-
/**
|
|
1434
|
+
/** Option values a variant represents (map values ∪ title parts). */
|
|
1435
|
+
variantOptionValues(v) {
|
|
1436
|
+
const { map, titleParts } = this.getVariantOptionMap(v, this.product?.options);
|
|
1437
|
+
const set = new Set(titleParts.map((s) => s.toLowerCase()));
|
|
1438
|
+
for (const val of Object.values(map)) set.add(val);
|
|
1439
|
+
return Array.from(set);
|
|
1440
|
+
}
|
|
1441
|
+
/** Does a variant part represent this option value? Exact match, plus a
|
|
1442
|
+
* whole-token match ONLY for numeric values ("30" matches "30 ml") — word
|
|
1443
|
+
* subsets of text values are not matches ("Large" must NOT match
|
|
1444
|
+
* "Extra Large"). */
|
|
1445
|
+
partMatchesValue(part, value) {
|
|
1446
|
+
if (!part || !value) return false;
|
|
1447
|
+
const p = String(part).trim().toLowerCase();
|
|
1448
|
+
const v = String(value).trim().toLowerCase();
|
|
1449
|
+
if (p === v) return true;
|
|
1450
|
+
if (!/^[\d.,]+$/.test(v)) return false;
|
|
1451
|
+
return p.split(/\s+/).includes(v);
|
|
1452
|
+
}
|
|
1453
|
+
/** Does a variant satisfy every currently-selected option value? */
|
|
1454
|
+
variantMatchesSelection(v, selection) {
|
|
1455
|
+
if (!v || !selection) return false;
|
|
1456
|
+
const entries = Object.entries(selection);
|
|
1457
|
+
if (entries.length === 0) return true;
|
|
1458
|
+
const { map, titleParts } = this.getVariantOptionMap(v, this.product?.options);
|
|
1459
|
+
const titlePartsLower = titleParts.map((t) => t.toLowerCase());
|
|
1460
|
+
return entries.every(([optName, val]) => {
|
|
1461
|
+
const key = optName.trim().toLowerCase();
|
|
1462
|
+
const targetVal = String(val).trim().toLowerCase();
|
|
1463
|
+
if (map[key]) {
|
|
1464
|
+
return this.partMatchesValue(map[key], targetVal);
|
|
1465
|
+
}
|
|
1466
|
+
return titlePartsLower.some((part) => this.partMatchesValue(part, targetVal));
|
|
1467
|
+
});
|
|
1468
|
+
}
|
|
1469
|
+
/** Sellable quantity for a variant, across the stock field zoo. Boolean
|
|
1470
|
+
* availability flags win; a numeric field wins next; `null` = the feed does
|
|
1471
|
+
* not report stock for this variant (callers fall back to product stock —
|
|
1472
|
+
* never assume "in stock" from absent data). */
|
|
1473
|
+
getVariantQty(v) {
|
|
1474
|
+
if (!v) return 0;
|
|
1475
|
+
if (v.available === false || v.in_stock === false || v.is_available === false) return 0;
|
|
1476
|
+
if (v.available === true || v.in_stock === true || v.is_available === true) return 999;
|
|
1477
|
+
const rawQty = v.quantity ?? v.inventory_quantity ?? v.stock ?? v.available_quantity ?? v.stock_quantity;
|
|
1478
|
+
if (rawQty !== null && rawQty !== void 0 && rawQty !== "") {
|
|
1479
|
+
const parsed = Number(rawQty);
|
|
1480
|
+
if (!Number.isNaN(parsed)) return parsed;
|
|
1481
|
+
}
|
|
1482
|
+
return null;
|
|
1483
|
+
}
|
|
1484
|
+
/** Variant stock with the product-level fallback applied (0 = not sellable). */
|
|
1485
|
+
variantSellableQty(v) {
|
|
1486
|
+
const q = this.getVariantQty(v);
|
|
1487
|
+
if (q !== null) return q;
|
|
1488
|
+
return this.product?.quantity ?? 0;
|
|
1489
|
+
}
|
|
1490
|
+
/** Resolve the active variant: full in-stock match → full match → best score
|
|
1491
|
+
* (2 per mapped option match, 1 per title match, +0.5 if in stock). */
|
|
1106
1492
|
selectedVariant() {
|
|
1107
|
-
if (!this.product || this.product.variants.length === 0)
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
const
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
)
|
|
1493
|
+
if (!this.product || !Array.isArray(this.product.variants) || this.product.variants.length === 0) {
|
|
1494
|
+
return null;
|
|
1495
|
+
}
|
|
1496
|
+
const sel = this.selectedOptions;
|
|
1497
|
+
const entries = Object.entries(sel);
|
|
1498
|
+
if (entries.length === 0) return this.product.variants[0] ?? null;
|
|
1499
|
+
const fullInStock = this.product.variants.find(
|
|
1500
|
+
(v) => this.variantMatchesSelection(v, sel) && this.variantSellableQty(v) > 0
|
|
1501
|
+
);
|
|
1502
|
+
if (fullInStock) return fullInStock;
|
|
1503
|
+
const full = this.product.variants.find((v) => this.variantMatchesSelection(v, sel));
|
|
1504
|
+
if (full) return full;
|
|
1505
|
+
let bestVariant = null;
|
|
1506
|
+
let maxScore = -1;
|
|
1507
|
+
for (const v of this.product.variants) {
|
|
1508
|
+
const { map, titleParts } = this.getVariantOptionMap(v, this.product?.options);
|
|
1509
|
+
const titlePartsLower = titleParts.map((t) => t.toLowerCase());
|
|
1510
|
+
let score = 0;
|
|
1511
|
+
for (const [optName, val] of entries) {
|
|
1512
|
+
const key = optName.trim().toLowerCase();
|
|
1513
|
+
const targetVal = String(val).trim().toLowerCase();
|
|
1514
|
+
if (map[key] && this.partMatchesValue(map[key], targetVal)) {
|
|
1515
|
+
score += 2;
|
|
1516
|
+
} else if (titlePartsLower.some((part) => this.partMatchesValue(part, targetVal))) {
|
|
1517
|
+
score += 1;
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
if (this.variantSellableQty(v) > 0) score += 0.5;
|
|
1521
|
+
if (score > maxScore) {
|
|
1522
|
+
maxScore = score;
|
|
1523
|
+
bestVariant = v;
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
return bestVariant ?? this.product.variants[0] ?? null;
|
|
1527
|
+
}
|
|
1528
|
+
/** True if some in-stock variant carries this option value. */
|
|
1529
|
+
isValueAvailable(optionName, value) {
|
|
1530
|
+
if (!this.product || !Array.isArray(this.product.variants) || this.product.variants.length === 0) {
|
|
1531
|
+
return true;
|
|
1532
|
+
}
|
|
1533
|
+
const valLower = String(value).trim().toLowerCase();
|
|
1534
|
+
const key = optionName.trim().toLowerCase();
|
|
1535
|
+
return this.product.variants.some((v) => {
|
|
1536
|
+
const { map, titleParts } = this.getVariantOptionMap(v, this.product?.options);
|
|
1537
|
+
if (this.variantSellableQty(v) <= 0) return false;
|
|
1538
|
+
if (map[key]) return this.partMatchesValue(map[key], valLower);
|
|
1539
|
+
return titleParts.some((part) => this.partMatchesValue(part.toLowerCase(), valLower));
|
|
1115
1540
|
});
|
|
1116
|
-
return found ?? this.product.variants[0] ?? null;
|
|
1117
1541
|
}
|
|
1118
1542
|
get unitPrice() {
|
|
1119
1543
|
const v = this.selectedVariant();
|
|
@@ -1127,7 +1551,7 @@ var VioProductDetail = class extends LitElement {
|
|
|
1127
1551
|
}
|
|
1128
1552
|
get availableQuantity() {
|
|
1129
1553
|
const v = this.selectedVariant();
|
|
1130
|
-
if (v) return v
|
|
1554
|
+
if (v) return this.variantSellableQty(v);
|
|
1131
1555
|
return this.product?.quantity ?? 0;
|
|
1132
1556
|
}
|
|
1133
1557
|
stockLabel() {
|
|
@@ -1136,9 +1560,50 @@ var VioProductDetail = class extends LitElement {
|
|
|
1136
1560
|
if (qty <= 3) return { text: `F\xE5 igjen \u2014 ${qty} stk`, cls: "low-stock" };
|
|
1137
1561
|
return { text: "P\xE5 lager", cls: "in-stock" };
|
|
1138
1562
|
}
|
|
1563
|
+
/** Pick an option value. If the exact combination doesn't exist, jump to an
|
|
1564
|
+
* in-stock variant that carries the value and adopt its full option map
|
|
1565
|
+
* (keeps the selection always resolvable to a real variant). */
|
|
1139
1566
|
selectOption(optionName, value) {
|
|
1140
|
-
|
|
1567
|
+
const newSelection = { ...this.selectedOptions, [optionName]: value };
|
|
1568
|
+
const exactInStock = this.product?.variants?.find(
|
|
1569
|
+
(v) => this.variantMatchesSelection(v, newSelection) && this.variantSellableQty(v) > 0
|
|
1570
|
+
);
|
|
1571
|
+
if (exactInStock) {
|
|
1572
|
+
this.selectedOptions = newSelection;
|
|
1573
|
+
return;
|
|
1574
|
+
}
|
|
1575
|
+
const exactAnyStock = this.product?.variants?.find(
|
|
1576
|
+
(v) => this.variantMatchesSelection(v, newSelection)
|
|
1577
|
+
);
|
|
1578
|
+
if (exactAnyStock) {
|
|
1579
|
+
this.selectedOptions = newSelection;
|
|
1580
|
+
return;
|
|
1581
|
+
}
|
|
1582
|
+
const valLower = String(value).trim().toLowerCase();
|
|
1583
|
+
const key = optionName.trim().toLowerCase();
|
|
1584
|
+
const anyInStockWithVal = this.product?.variants?.find((v) => {
|
|
1585
|
+
const { map, titleParts } = this.getVariantOptionMap(v, this.product?.options);
|
|
1586
|
+
if (this.variantSellableQty(v) <= 0) return false;
|
|
1587
|
+
if (map[key]) return this.partMatchesValue(map[key], valLower);
|
|
1588
|
+
return titleParts.some((p) => this.partMatchesValue(p.toLowerCase(), valLower));
|
|
1589
|
+
});
|
|
1590
|
+
if (anyInStockWithVal) {
|
|
1591
|
+
const { map } = this.getVariantOptionMap(anyInStockWithVal, this.product?.options);
|
|
1592
|
+
const updated = { ...newSelection };
|
|
1593
|
+
for (const opt of this.product?.options ?? []) {
|
|
1594
|
+
const k = opt.name.toLowerCase();
|
|
1595
|
+
if (map[k]) {
|
|
1596
|
+
const optValues = normalizeValues(opt.values);
|
|
1597
|
+
const origCase = optValues.find((val) => val.toLowerCase() === map[k]) || map[k];
|
|
1598
|
+
updated[opt.name] = origCase;
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
this.selectedOptions = updated;
|
|
1602
|
+
} else {
|
|
1603
|
+
this.selectedOptions = newSelection;
|
|
1604
|
+
}
|
|
1141
1605
|
}
|
|
1606
|
+
/* eslint-enable @typescript-eslint/no-explicit-any */
|
|
1142
1607
|
allImages() {
|
|
1143
1608
|
if (!this.product) return [];
|
|
1144
1609
|
const variantImages = this.selectedVariant()?.images ?? [];
|
|
@@ -1209,6 +1674,32 @@ var VioProductDetail = class extends LitElement {
|
|
|
1209
1674
|
this.adding = false;
|
|
1210
1675
|
}
|
|
1211
1676
|
}
|
|
1677
|
+
/** Buy with Vipps — opens the checkout with Vipps preselected. */
|
|
1678
|
+
buyWithVipps() {
|
|
1679
|
+
if (!this.product || this.availableQuantity <= 0 || this.adding) return;
|
|
1680
|
+
this.addCurrentToCart();
|
|
1681
|
+
this.close();
|
|
1682
|
+
this.dispatchEvent(
|
|
1683
|
+
new CustomEvent("vio:checkout-open", {
|
|
1684
|
+
bubbles: true,
|
|
1685
|
+
composed: true,
|
|
1686
|
+
detail: { sponsorId: this.sponsorId, paymentMethod: "vipps", express: false }
|
|
1687
|
+
})
|
|
1688
|
+
);
|
|
1689
|
+
}
|
|
1690
|
+
/** Buy with Stripe — opens the checkout with Stripe preselected. */
|
|
1691
|
+
buyWithStripe() {
|
|
1692
|
+
if (!this.product || this.availableQuantity <= 0 || this.adding) return;
|
|
1693
|
+
this.addCurrentToCart();
|
|
1694
|
+
this.close();
|
|
1695
|
+
this.dispatchEvent(
|
|
1696
|
+
new CustomEvent("vio:checkout-open", {
|
|
1697
|
+
bubbles: true,
|
|
1698
|
+
composed: true,
|
|
1699
|
+
detail: { sponsorId: this.sponsorId, paymentMethod: "stripe", express: false }
|
|
1700
|
+
})
|
|
1701
|
+
);
|
|
1702
|
+
}
|
|
1212
1703
|
/**
|
|
1213
1704
|
* Express buy with Apple Pay. Runs synchronously inside the click so the
|
|
1214
1705
|
* Apple Pay sheet keeps the user gesture: add to cart → open checkout (sets
|
|
@@ -1333,17 +1824,35 @@ var VioProductDetail = class extends LitElement {
|
|
|
1333
1824
|
</button>
|
|
1334
1825
|
</div>
|
|
1335
1826
|
|
|
1336
|
-
${this.availableQuantity > 0 && this.applePayOk ? html`
|
|
1827
|
+
${this.availableQuantity > 0 && this.applePayOk && this.methodEnabled("apple-pay", "applepay") ? html`
|
|
1337
1828
|
<button
|
|
1338
1829
|
class="buy-applepay"
|
|
1339
1830
|
@click=${this.buyWithApplePay}
|
|
1340
1831
|
?disabled=${this.adding}
|
|
1341
1832
|
aria-label="Kjøp nå med Apple Pay"
|
|
1342
1833
|
>
|
|
1343
|
-
Kjøp nå med
|
|
1834
|
+
<span>Kjøp nå med</span>
|
|
1835
|
+
<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>
|
|
1836
|
+
<span>Apple Pay</span>
|
|
1837
|
+
</button>
|
|
1838
|
+
` : ""}
|
|
1839
|
+
${this.availableQuantity > 0 && this.methodEnabled("stripe") ? html`
|
|
1840
|
+
<button
|
|
1841
|
+
class="buy-stripe"
|
|
1842
|
+
@click=${this.buyWithStripe}
|
|
1843
|
+
?disabled=${this.adding}
|
|
1844
|
+
aria-label="Kjøp nå med Stripe"
|
|
1845
|
+
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;"
|
|
1846
|
+
>
|
|
1847
|
+
<span>Kjøp nå med</span>
|
|
1848
|
+
<img
|
|
1849
|
+
src=${STRIPE_LOGO_SRC}
|
|
1850
|
+
style="height: 20px; display: block; filter: brightness(0) invert(1);"
|
|
1851
|
+
alt="Stripe"
|
|
1852
|
+
/>
|
|
1344
1853
|
</button>
|
|
1345
1854
|
` : ""}
|
|
1346
|
-
${this.availableQuantity > 0 ? html`
|
|
1855
|
+
${this.availableQuantity > 0 && this.methodEnabled("klarna") ? html`
|
|
1347
1856
|
<button
|
|
1348
1857
|
class="buy-klarna"
|
|
1349
1858
|
@click=${this.buyWithKlarna}
|
|
@@ -1358,6 +1867,18 @@ var VioProductDetail = class extends LitElement {
|
|
|
1358
1867
|
</button>
|
|
1359
1868
|
` : ""}
|
|
1360
1869
|
|
|
1870
|
+
${this.availableQuantity > 0 && this.methodEnabled("vipps") ? html`
|
|
1871
|
+
<button
|
|
1872
|
+
class="buy-vipps"
|
|
1873
|
+
@click=${this.buyWithVipps}
|
|
1874
|
+
?disabled=${this.adding}
|
|
1875
|
+
aria-label="Kjøp nå med Vipps"
|
|
1876
|
+
>
|
|
1877
|
+
<span>Kjøp nå med</span>
|
|
1878
|
+
<span class="vipps-badge">vipps</span>
|
|
1879
|
+
</button>
|
|
1880
|
+
` : ""}
|
|
1881
|
+
|
|
1361
1882
|
${p.description ? html`
|
|
1362
1883
|
<div>
|
|
1363
1884
|
<div class="description-label">Beskrivelse</div>
|
|
@@ -1377,17 +1898,22 @@ var VioProductDetail = class extends LitElement {
|
|
|
1377
1898
|
<div class="option-group">
|
|
1378
1899
|
<div class="option-label">${opt.name}</div>
|
|
1379
1900
|
<div class="option-values">
|
|
1380
|
-
${values.map(
|
|
1381
|
-
(val)
|
|
1901
|
+
${values.map((val) => {
|
|
1902
|
+
const available = this.isValueAvailable(opt.name, val);
|
|
1903
|
+
const currentVal = this.selectedOptions[opt.name] ?? "";
|
|
1904
|
+
const isSelected = String(currentVal).trim().toLowerCase() === String(val).trim().toLowerCase();
|
|
1905
|
+
return html`
|
|
1382
1906
|
<button
|
|
1383
|
-
class="option-pill"
|
|
1907
|
+
class="option-pill ${available ? "" : "unavailable"}"
|
|
1384
1908
|
@click=${() => this.selectOption(opt.name, val)}
|
|
1385
|
-
aria-pressed=${
|
|
1909
|
+
aria-pressed=${isSelected}
|
|
1910
|
+
?disabled=${!available}
|
|
1911
|
+
title=${available ? "" : "Utsolgt"}
|
|
1386
1912
|
>
|
|
1387
1913
|
${val}
|
|
1388
1914
|
</button>
|
|
1389
|
-
|
|
1390
|
-
)}
|
|
1915
|
+
`;
|
|
1916
|
+
})}
|
|
1391
1917
|
</div>
|
|
1392
1918
|
</div>
|
|
1393
1919
|
`;
|
|
@@ -1437,6 +1963,9 @@ __decorateClass([
|
|
|
1437
1963
|
__decorateClass([
|
|
1438
1964
|
state()
|
|
1439
1965
|
], VioProductDetail.prototype, "applePayOk", 2);
|
|
1966
|
+
__decorateClass([
|
|
1967
|
+
state()
|
|
1968
|
+
], VioProductDetail.prototype, "availableMethods", 2);
|
|
1440
1969
|
function normalizeValues(raw) {
|
|
1441
1970
|
if (Array.isArray(raw)) {
|
|
1442
1971
|
return raw.map((v) => String(v).trim()).filter((v) => v.length > 0);
|
|
@@ -1446,6 +1975,9 @@ function normalizeValues(raw) {
|
|
|
1446
1975
|
}
|
|
1447
1976
|
return [];
|
|
1448
1977
|
}
|
|
1978
|
+
var STRIPE_LOGO_SRC2 = "data:image/svg+xml;utf8," + encodeURIComponent(
|
|
1979
|
+
'<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>'
|
|
1980
|
+
);
|
|
1449
1981
|
var VioCart = class extends LitElement {
|
|
1450
1982
|
constructor() {
|
|
1451
1983
|
super(...arguments);
|
|
@@ -1456,15 +1988,21 @@ var VioCart = class extends LitElement {
|
|
|
1456
1988
|
this.carts = /* @__PURE__ */ new Map();
|
|
1457
1989
|
this.itemCount = 0;
|
|
1458
1990
|
this.applePayAvailable = false;
|
|
1991
|
+
this.availableMethods = null;
|
|
1459
1992
|
this.confirmedOrder = null;
|
|
1460
1993
|
/** Pre-resolved Apple Pay handle; `show()` fires the sheet synchronously. */
|
|
1461
1994
|
this.applePayHandle = null;
|
|
1462
1995
|
/** Cart total Apple Pay was last prepared for (avoids redundant prep). */
|
|
1463
1996
|
this.preparedTotal = -1;
|
|
1997
|
+
/** In-flight guard for loadAvailablePaymentMethods. */
|
|
1998
|
+
this.loadingPaymentMethods = false;
|
|
1464
1999
|
this.boundOnCartChange = (e) => {
|
|
1465
2000
|
const detail = e.detail;
|
|
1466
2001
|
this.carts = detail.cartsBySponsor;
|
|
1467
2002
|
this.itemCount = detail.itemCount;
|
|
2003
|
+
if (this.availableMethods === null) {
|
|
2004
|
+
void this.loadAvailablePaymentMethods();
|
|
2005
|
+
}
|
|
1468
2006
|
};
|
|
1469
2007
|
}
|
|
1470
2008
|
static {
|
|
@@ -1672,7 +2210,7 @@ var VioCart = class extends LitElement {
|
|
|
1672
2210
|
transition: opacity 0.15s;
|
|
1673
2211
|
}
|
|
1674
2212
|
.applepay-btn:hover { opacity: 0.86; }
|
|
1675
|
-
.applepay-btn .ap-logo {
|
|
2213
|
+
.applepay-btn .ap-logo { width: 20px; height: 20px; }
|
|
1676
2214
|
/* Klarna express — pink brand button, same express row as Apple Pay. */
|
|
1677
2215
|
.klarna-btn {
|
|
1678
2216
|
width: 100%;
|
|
@@ -1694,6 +2232,50 @@ var VioCart = class extends LitElement {
|
|
|
1694
2232
|
}
|
|
1695
2233
|
.klarna-btn:hover { background: #ffa1ba; }
|
|
1696
2234
|
.klarna-btn .klarna-logo { height: 15px; width: auto; display: inline-block; }
|
|
2235
|
+
/* Vipps — brand orange button, same express row. */
|
|
2236
|
+
.vipps-btn {
|
|
2237
|
+
width: 100%;
|
|
2238
|
+
padding: 15px;
|
|
2239
|
+
margin-bottom: 10px;
|
|
2240
|
+
background: #ff5b24;
|
|
2241
|
+
color: #fff;
|
|
2242
|
+
border: none;
|
|
2243
|
+
border-radius: 8px;
|
|
2244
|
+
cursor: pointer;
|
|
2245
|
+
display: flex;
|
|
2246
|
+
align-items: center;
|
|
2247
|
+
justify-content: center;
|
|
2248
|
+
font-family: var(--vio-font-sans, -apple-system, sans-serif);
|
|
2249
|
+
transition: background 0.15s;
|
|
2250
|
+
}
|
|
2251
|
+
.vipps-btn:hover { background: #ec4f1c; }
|
|
2252
|
+
.vipps-btn .vipps-logo {
|
|
2253
|
+
font-size: 20px;
|
|
2254
|
+
font-weight: 800;
|
|
2255
|
+
letter-spacing: -0.02em;
|
|
2256
|
+
line-height: 1;
|
|
2257
|
+
}
|
|
2258
|
+
/* Stripe — brand blurple button, same express row. */
|
|
2259
|
+
.stripe-btn {
|
|
2260
|
+
width: 100%;
|
|
2261
|
+
padding: 15px;
|
|
2262
|
+
margin-bottom: 10px;
|
|
2263
|
+
background: #635bff;
|
|
2264
|
+
color: #fff;
|
|
2265
|
+
border: none;
|
|
2266
|
+
border-radius: 8px;
|
|
2267
|
+
font-size: 16px;
|
|
2268
|
+
font-weight: 600;
|
|
2269
|
+
cursor: pointer;
|
|
2270
|
+
display: flex;
|
|
2271
|
+
align-items: center;
|
|
2272
|
+
justify-content: center;
|
|
2273
|
+
gap: 6px;
|
|
2274
|
+
font-family: var(--vio-font-sans, -apple-system, sans-serif);
|
|
2275
|
+
transition: background 0.15s;
|
|
2276
|
+
}
|
|
2277
|
+
.stripe-btn:hover { background: #5248ff; }
|
|
2278
|
+
.stripe-btn .stripe-logo { height: 20px; width: auto; display: inline-block; filter: brightness(0) invert(1); }
|
|
1697
2279
|
|
|
1698
2280
|
/* In-drawer confirmation after Apple Pay (nothing else is shown). */
|
|
1699
2281
|
.confirm {
|
|
@@ -1768,6 +2350,7 @@ var VioCart = class extends LitElement {
|
|
|
1768
2350
|
this.carts = Vio.cart.getAllCarts();
|
|
1769
2351
|
this.itemCount = Vio.cart.itemCount;
|
|
1770
2352
|
Vio.cart.addEventListener("change", this.boundOnCartChange);
|
|
2353
|
+
void this.loadAvailablePaymentMethods();
|
|
1771
2354
|
}
|
|
1772
2355
|
disconnectedCallback() {
|
|
1773
2356
|
Vio.cart.removeEventListener("change", this.boundOnCartChange);
|
|
@@ -1778,6 +2361,39 @@ var VioCart = class extends LitElement {
|
|
|
1778
2361
|
}
|
|
1779
2362
|
show() {
|
|
1780
2363
|
this.open = true;
|
|
2364
|
+
void this.loadAvailablePaymentMethods();
|
|
2365
|
+
}
|
|
2366
|
+
/** Which payment buttons to render — backend-configured per sponsor.
|
|
2367
|
+
* Falls back to showing everything if the query fails or none is set. */
|
|
2368
|
+
async loadAvailablePaymentMethods() {
|
|
2369
|
+
if (this.loadingPaymentMethods) return;
|
|
2370
|
+
this.loadingPaymentMethods = true;
|
|
2371
|
+
const firstSponsorId = [...this.carts.keys()][0] ?? globalThis.__VIO_SPONSOR_ID__;
|
|
2372
|
+
if (firstSponsorId === void 0) {
|
|
2373
|
+
this.loadingPaymentMethods = false;
|
|
2374
|
+
return;
|
|
2375
|
+
}
|
|
2376
|
+
try {
|
|
2377
|
+
const methods = await Vio.checkout.getAvailablePaymentMethods(firstSponsorId);
|
|
2378
|
+
if (Array.isArray(methods)) {
|
|
2379
|
+
this.availableMethods = methods.map((m) => m.name);
|
|
2380
|
+
} else {
|
|
2381
|
+
this.availableMethods = null;
|
|
2382
|
+
}
|
|
2383
|
+
} catch (err) {
|
|
2384
|
+
if (typeof console !== "undefined") {
|
|
2385
|
+
console.warn("[VioCart] Failed to load payment methods:", err);
|
|
2386
|
+
}
|
|
2387
|
+
this.availableMethods = null;
|
|
2388
|
+
} finally {
|
|
2389
|
+
this.loadingPaymentMethods = false;
|
|
2390
|
+
}
|
|
2391
|
+
}
|
|
2392
|
+
/** Backend names arrive as e.g. "Apple Pay" — compare letters only. */
|
|
2393
|
+
methodEnabled(name) {
|
|
2394
|
+
if (this.availableMethods === null) return true;
|
|
2395
|
+
const norm = (s) => s.toLowerCase().replace(/[^a-z]/g, "");
|
|
2396
|
+
return this.availableMethods.some((m) => norm(m) === norm(name));
|
|
1781
2397
|
}
|
|
1782
2398
|
updated(changed) {
|
|
1783
2399
|
if (changed.has("open") && this.open) {
|
|
@@ -1864,6 +2480,32 @@ var VioCart = class extends LitElement {
|
|
|
1864
2480
|
})
|
|
1865
2481
|
);
|
|
1866
2482
|
}
|
|
2483
|
+
/** Vipps — opens the checkout with Vipps preselected. */
|
|
2484
|
+
onVipps() {
|
|
2485
|
+
if (this.itemCount === 0) return;
|
|
2486
|
+
const firstSponsorId = [...this.carts.keys()][0];
|
|
2487
|
+
if (firstSponsorId === void 0) return;
|
|
2488
|
+
this.dispatchEvent(
|
|
2489
|
+
new CustomEvent("vio:checkout-open", {
|
|
2490
|
+
bubbles: true,
|
|
2491
|
+
composed: true,
|
|
2492
|
+
detail: { sponsorId: firstSponsorId, paymentMethod: "vipps", express: false }
|
|
2493
|
+
})
|
|
2494
|
+
);
|
|
2495
|
+
}
|
|
2496
|
+
/** Stripe — opens the checkout with Stripe preselected. */
|
|
2497
|
+
onStripe() {
|
|
2498
|
+
if (this.itemCount === 0) return;
|
|
2499
|
+
const firstSponsorId = [...this.carts.keys()][0];
|
|
2500
|
+
if (firstSponsorId === void 0) return;
|
|
2501
|
+
this.dispatchEvent(
|
|
2502
|
+
new CustomEvent("vio:checkout-open", {
|
|
2503
|
+
bubbles: true,
|
|
2504
|
+
composed: true,
|
|
2505
|
+
detail: { sponsorId: firstSponsorId, paymentMethod: "stripe", express: false }
|
|
2506
|
+
})
|
|
2507
|
+
);
|
|
2508
|
+
}
|
|
1867
2509
|
get totalAcrossSponsors() {
|
|
1868
2510
|
let sum = 0;
|
|
1869
2511
|
for (const cart of this.carts.values()) {
|
|
@@ -1959,27 +2601,48 @@ var VioCart = class extends LitElement {
|
|
|
1959
2601
|
${formatPrice(this.totalAcrossSponsors, this.primaryCurrency)}
|
|
1960
2602
|
</span>
|
|
1961
2603
|
</div>
|
|
1962
|
-
${this.applePayAvailable ? html`
|
|
2604
|
+
${this.methodEnabled("apple-pay") && this.applePayAvailable ? html`
|
|
1963
2605
|
<button
|
|
1964
2606
|
class="applepay-btn"
|
|
1965
2607
|
@click=${this.onApplePay}
|
|
1966
2608
|
aria-label="Betal med Apple Pay"
|
|
1967
2609
|
>
|
|
1968
|
-
<
|
|
2610
|
+
<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>
|
|
2611
|
+
Apple Pay
|
|
2612
|
+
</button>
|
|
2613
|
+
` : ""}
|
|
2614
|
+
${this.methodEnabled("stripe") ? html`
|
|
2615
|
+
<button
|
|
2616
|
+
class="stripe-btn"
|
|
2617
|
+
@click=${this.onStripe}
|
|
2618
|
+
aria-label="Betal med Stripe"
|
|
2619
|
+
>
|
|
2620
|
+
<img class="stripe-logo" src=${STRIPE_LOGO_SRC2} alt="Stripe" />
|
|
2621
|
+
</button>
|
|
2622
|
+
` : ""}
|
|
2623
|
+
${this.methodEnabled("klarna") ? html`
|
|
2624
|
+
<button
|
|
2625
|
+
class="klarna-btn"
|
|
2626
|
+
@click=${this.onKlarnaExpress}
|
|
2627
|
+
aria-label="Betal med Klarna"
|
|
2628
|
+
>
|
|
2629
|
+
Kjøp med
|
|
2630
|
+
<img
|
|
2631
|
+
class="klarna-logo"
|
|
2632
|
+
src="https://x.klarnacdn.net/payment-method/assets/badges/generic/klarna.svg"
|
|
2633
|
+
alt="Klarna"
|
|
2634
|
+
/>
|
|
2635
|
+
</button>
|
|
2636
|
+
` : ""}
|
|
2637
|
+
${this.methodEnabled("vipps") ? html`
|
|
2638
|
+
<button
|
|
2639
|
+
class="vipps-btn"
|
|
2640
|
+
@click=${this.onVipps}
|
|
2641
|
+
aria-label="Betal med Vipps"
|
|
2642
|
+
>
|
|
2643
|
+
<span class="vipps-logo">vipps</span>
|
|
1969
2644
|
</button>
|
|
1970
2645
|
` : ""}
|
|
1971
|
-
<button
|
|
1972
|
-
class="klarna-btn"
|
|
1973
|
-
@click=${this.onKlarnaExpress}
|
|
1974
|
-
aria-label="Betal med Klarna"
|
|
1975
|
-
>
|
|
1976
|
-
Kjøp med
|
|
1977
|
-
<img
|
|
1978
|
-
class="klarna-logo"
|
|
1979
|
-
src="https://x.klarnacdn.net/payment-method/assets/badges/generic/klarna.svg"
|
|
1980
|
-
alt="Klarna"
|
|
1981
|
-
/>
|
|
1982
|
-
</button>
|
|
1983
2646
|
</div>
|
|
1984
2647
|
`}
|
|
1985
2648
|
</aside>
|
|
@@ -2033,10 +2696,16 @@ __decorateClass([
|
|
|
2033
2696
|
__decorateClass([
|
|
2034
2697
|
state()
|
|
2035
2698
|
], VioCart.prototype, "applePayAvailable", 2);
|
|
2699
|
+
__decorateClass([
|
|
2700
|
+
state()
|
|
2701
|
+
], VioCart.prototype, "availableMethods", 2);
|
|
2036
2702
|
__decorateClass([
|
|
2037
2703
|
state()
|
|
2038
2704
|
], VioCart.prototype, "confirmedOrder", 2);
|
|
2039
|
-
var
|
|
2705
|
+
var STRIPE_LOGO_SRC3 = "data:image/svg+xml;utf8," + encodeURIComponent(
|
|
2706
|
+
'<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>'
|
|
2707
|
+
);
|
|
2708
|
+
var _VioCheckout = class _VioCheckout extends LitElement {
|
|
2040
2709
|
constructor() {
|
|
2041
2710
|
super(...arguments);
|
|
2042
2711
|
this.open = false;
|
|
@@ -2051,6 +2720,7 @@ var VioCheckout = class extends LitElement {
|
|
|
2051
2720
|
this.orderConfirmed = false;
|
|
2052
2721
|
this.confirmedMethod = null;
|
|
2053
2722
|
this.paymentError = null;
|
|
2723
|
+
this.paymentNotice = null;
|
|
2054
2724
|
this.confirmedOrder = null;
|
|
2055
2725
|
/** Active Klarna Payments widget handle + the subtotal it was mounted for. */
|
|
2056
2726
|
this.klarnaHandle = null;
|
|
@@ -2061,6 +2731,10 @@ var VioCheckout = class extends LitElement {
|
|
|
2061
2731
|
this.klarnaSelectedCat = "";
|
|
2062
2732
|
this.klarnaAuthorizing = false;
|
|
2063
2733
|
this.selectedShipping = KLARNA_SHIPPING_OPTIONS.find((o) => o.preselected)?.id ?? KLARNA_SHIPPING_OPTIONS[0]?.id ?? "";
|
|
2734
|
+
this.availableShippingsList = [];
|
|
2735
|
+
this.availableMethods = null;
|
|
2736
|
+
this.stripeLoading = false;
|
|
2737
|
+
this.vippsLoading = false;
|
|
2064
2738
|
this.form = {
|
|
2065
2739
|
firstName: "",
|
|
2066
2740
|
lastName: "",
|
|
@@ -2069,13 +2743,27 @@ var VioCheckout = class extends LitElement {
|
|
|
2069
2743
|
postalCode: "",
|
|
2070
2744
|
city: ""
|
|
2071
2745
|
};
|
|
2746
|
+
/** In-flight guard for loadAvailablePaymentMethods. */
|
|
2747
|
+
this.loadingPaymentMethods = false;
|
|
2748
|
+
/** In-flight + attempted guards for loadAvailableShippings (change events
|
|
2749
|
+
* fire per keystroke — without these, typing an address hammers the API). */
|
|
2750
|
+
this.loadingShippings = false;
|
|
2751
|
+
this.shippingsAttempted = false;
|
|
2072
2752
|
this.boundOnCheckoutChange = (e) => {
|
|
2073
2753
|
const detail = e.detail;
|
|
2754
|
+
const prevSponsorId = this.checkoutState?.sponsorId;
|
|
2755
|
+
const sponsorChanged = !prevSponsorId || prevSponsorId !== detail.state?.sponsorId;
|
|
2074
2756
|
this.checkoutState = detail.state;
|
|
2075
2757
|
this.items = Vio.checkout.items;
|
|
2076
2758
|
if (this.checkoutState) {
|
|
2077
|
-
this.
|
|
2078
|
-
|
|
2759
|
+
if (sponsorChanged || this.availableMethods === null) {
|
|
2760
|
+
void this.refreshApplePay();
|
|
2761
|
+
void this.refreshKlarna();
|
|
2762
|
+
void this.loadAvailablePaymentMethods();
|
|
2763
|
+
void this.loadAvailableShippings();
|
|
2764
|
+
} else if (!this.shippingsAttempted) {
|
|
2765
|
+
void this.loadAvailableShippings();
|
|
2766
|
+
}
|
|
2079
2767
|
} else {
|
|
2080
2768
|
this.applePayAvailable = false;
|
|
2081
2769
|
this.klarnaAvailable = false;
|
|
@@ -2083,7 +2771,9 @@ var VioCheckout = class extends LitElement {
|
|
|
2083
2771
|
this.confirmedMethod = null;
|
|
2084
2772
|
this.confirmedOrder = null;
|
|
2085
2773
|
this.paymentError = null;
|
|
2774
|
+
this.paymentNotice = null;
|
|
2086
2775
|
this.express = false;
|
|
2776
|
+
this.availableMethods = null;
|
|
2087
2777
|
this.unmountKlarna();
|
|
2088
2778
|
}
|
|
2089
2779
|
};
|
|
@@ -2164,6 +2854,21 @@ var VioCheckout = class extends LitElement {
|
|
|
2164
2854
|
box-shadow: -8px 0 24px rgba(0, 0, 0, 0.1);
|
|
2165
2855
|
}
|
|
2166
2856
|
:host([open]) .modal.as-side { transform: translateX(0); }
|
|
2857
|
+
|
|
2858
|
+
/* Desktop: the checkout ALWAYS lives as a right-side panel (like the cart,
|
|
2859
|
+
product detail and express drawer) — never a full-width overlay. Only the
|
|
2860
|
+
compact confirmation card stays centered. */
|
|
2861
|
+
@media (min-width: 601px) {
|
|
2862
|
+
.modal:not(.as-confirm) {
|
|
2863
|
+
inset: 0 0 0 auto;
|
|
2864
|
+
width: min(440px, 100%);
|
|
2865
|
+
max-width: 440px;
|
|
2866
|
+
transform: translateX(100%);
|
|
2867
|
+
border-radius: 0;
|
|
2868
|
+
box-shadow: -8px 0 24px rgba(0, 0, 0, 0.1);
|
|
2869
|
+
}
|
|
2870
|
+
:host([open]) .modal:not(.as-confirm) { transform: translateX(0); }
|
|
2871
|
+
}
|
|
2167
2872
|
@media (max-width: 600px) {
|
|
2168
2873
|
/* On phones, keep it a bottom sheet for reachability. */
|
|
2169
2874
|
.modal.as-side {
|
|
@@ -2344,8 +3049,9 @@ var VioCheckout = class extends LitElement {
|
|
|
2344
3049
|
background: var(--vio-color-surface-hover, #fafafa);
|
|
2345
3050
|
}
|
|
2346
3051
|
.payment-btn:disabled {
|
|
2347
|
-
cursor:
|
|
2348
|
-
opacity: 0.
|
|
3052
|
+
cursor: not-allowed;
|
|
3053
|
+
opacity: 0.45;
|
|
3054
|
+
pointer-events: none;
|
|
2349
3055
|
}
|
|
2350
3056
|
.payment-btn.primary {
|
|
2351
3057
|
background: var(--vio-color-text, #0a0a0a);
|
|
@@ -2429,9 +3135,27 @@ var VioCheckout = class extends LitElement {
|
|
|
2429
3135
|
color: var(--vio-color-accent, #c14a3b);
|
|
2430
3136
|
text-align: center;
|
|
2431
3137
|
}
|
|
3138
|
+
.payment-notice {
|
|
3139
|
+
grid-column: 1 / -1;
|
|
3140
|
+
padding: 12px 16px;
|
|
3141
|
+
background: #f4f4f2;
|
|
3142
|
+
border: 1px solid var(--vio-color-border, #ddd);
|
|
3143
|
+
border-radius: 8px;
|
|
3144
|
+
font-size: 13px;
|
|
3145
|
+
color: var(--vio-color-text, #1a1a1a);
|
|
3146
|
+
text-align: center;
|
|
3147
|
+
}
|
|
2432
3148
|
/* "Betal" CTA — sits full-width below the method grid once a method is
|
|
2433
3149
|
picked. Reuses .payment-btn.primary (its grid-column spans the row). */
|
|
2434
|
-
.complete-cta {
|
|
3150
|
+
.complete-cta {
|
|
3151
|
+
width: 100% !important;
|
|
3152
|
+
box-sizing: border-box !important;
|
|
3153
|
+
margin-top: 8px;
|
|
3154
|
+
display: flex !important;
|
|
3155
|
+
align-items: center !important;
|
|
3156
|
+
justify-content: center !important;
|
|
3157
|
+
text-align: center !important;
|
|
3158
|
+
}
|
|
2435
3159
|
|
|
2436
3160
|
/* Order confirmation — compact, shown inside the drawer. */
|
|
2437
3161
|
.confirmation {
|
|
@@ -2559,6 +3283,209 @@ var VioCheckout = class extends LitElement {
|
|
|
2559
3283
|
if (this.checkoutState) {
|
|
2560
3284
|
void this.refreshApplePay();
|
|
2561
3285
|
void this.refreshKlarna();
|
|
3286
|
+
void this.loadAvailablePaymentMethods();
|
|
3287
|
+
}
|
|
3288
|
+
void Vio.checkout.resumeKlarnaReturn();
|
|
3289
|
+
void this.checkReturnPaymentStatus();
|
|
3290
|
+
}
|
|
3291
|
+
static {
|
|
3292
|
+
/**
|
|
3293
|
+
* Detect the redirect return from a hosted payment page (Stripe / Vipps /
|
|
3294
|
+
* Klarna): the return URL carries vio_payment/vio_method/vio_sponsor. On
|
|
3295
|
+
* success: clear that sponsor's cart, show the confirmation drawer, emit
|
|
3296
|
+
* vio:payment-success (analytics/host hooks) and scrub the URL params.
|
|
3297
|
+
*/
|
|
3298
|
+
/** Backend checkout statuses that mean the payment went through. */
|
|
3299
|
+
this.PAID_STATUSES = /* @__PURE__ */ new Set([
|
|
3300
|
+
"paid",
|
|
3301
|
+
"completed",
|
|
3302
|
+
"complete",
|
|
3303
|
+
"success",
|
|
3304
|
+
"succeeded",
|
|
3305
|
+
"confirmed",
|
|
3306
|
+
"captured"
|
|
3307
|
+
]);
|
|
3308
|
+
}
|
|
3309
|
+
static {
|
|
3310
|
+
/** Statuses that mean the payment definitively did NOT go through. */
|
|
3311
|
+
this.FAILED_STATUSES = /* @__PURE__ */ new Set([
|
|
3312
|
+
"failed",
|
|
3313
|
+
"cancelled",
|
|
3314
|
+
"canceled",
|
|
3315
|
+
"rejected",
|
|
3316
|
+
"declined",
|
|
3317
|
+
"expired",
|
|
3318
|
+
"aborted",
|
|
3319
|
+
"terminated",
|
|
3320
|
+
"error",
|
|
3321
|
+
"voided",
|
|
3322
|
+
"unpaid"
|
|
3323
|
+
]);
|
|
3324
|
+
}
|
|
3325
|
+
static {
|
|
3326
|
+
/** Waits between GetCheckout attempts — PSP webhooks (Vipps especially) can
|
|
3327
|
+
* land seconds after the shopper is redirected back, so a single immediate
|
|
3328
|
+
* check misreads a successful charge as unpaid. ~20s total. */
|
|
3329
|
+
this.RETURN_VERIFY_DELAYS_MS = [0, 2e3, 4e3, 6e3, 8e3];
|
|
3330
|
+
}
|
|
3331
|
+
static {
|
|
3332
|
+
/** Vipps polling cadence — up to 2 minutes. Its webhook can lag well behind
|
|
3333
|
+
* the redirect; this window was verified against a live checkout. */
|
|
3334
|
+
this.VIPPS_STATUS_MAX_ATTEMPTS = 24;
|
|
3335
|
+
}
|
|
3336
|
+
static {
|
|
3337
|
+
this.VIPPS_STATUS_DELAY_MS = 5e3;
|
|
3338
|
+
}
|
|
3339
|
+
/**
|
|
3340
|
+
* Vipps redirects to our return URL the same way on cancel as on success —
|
|
3341
|
+
* the URL alone can't tell them apart, and the generic checkout status can
|
|
3342
|
+
* lag behind Vipps' own webhook. Poll Vipps' own state directly instead:
|
|
3343
|
+
* AUTHORIZED confirms, CREATED means still processing (keep polling),
|
|
3344
|
+
* anything else (including an unrecognized state) is treated as failed —
|
|
3345
|
+
* fail-closed by default rather than guessing.
|
|
3346
|
+
*/
|
|
3347
|
+
async pollVippsStatus(checkoutId, sponsorId) {
|
|
3348
|
+
let sawStatus = false;
|
|
3349
|
+
for (let attempt = 0; attempt < _VioCheckout.VIPPS_STATUS_MAX_ATTEMPTS; attempt++) {
|
|
3350
|
+
if (attempt > 0) {
|
|
3351
|
+
await new Promise((r) => setTimeout(r, _VioCheckout.VIPPS_STATUS_DELAY_MS));
|
|
3352
|
+
}
|
|
3353
|
+
try {
|
|
3354
|
+
const vippsStatus = await Vio.checkout.getVippsStatus(checkoutId, sponsorId || void 0);
|
|
3355
|
+
const state6 = String(vippsStatus?.state ?? "").toUpperCase();
|
|
3356
|
+
sawStatus = true;
|
|
3357
|
+
if (state6 === "AUTHORIZED") return { outcome: "paid", sawStatus };
|
|
3358
|
+
if (state6 === "CREATED") continue;
|
|
3359
|
+
return { outcome: "failed", sawStatus };
|
|
3360
|
+
} catch (err) {
|
|
3361
|
+
if (typeof console !== "undefined") {
|
|
3362
|
+
console.warn("[VioCheckout] GetVippsStatus attempt failed:", err);
|
|
3363
|
+
}
|
|
3364
|
+
}
|
|
3365
|
+
}
|
|
3366
|
+
return { outcome: "pending", sawStatus };
|
|
3367
|
+
}
|
|
3368
|
+
/** Shared outcome handler for both the generic (GetCheckout) and the
|
|
3369
|
+
* Vipps-specific (GetVippsStatus) return-verification paths. */
|
|
3370
|
+
applyReturnOutcome(outcome, sawStatus, vioMethod, sponsorId, checkoutId) {
|
|
3371
|
+
if (outcome === "paid") {
|
|
3372
|
+
if (sponsorId > 0) Vio.cart.clearSponsorCart(sponsorId);
|
|
3373
|
+
this.confirmedOrder = {
|
|
3374
|
+
items: [],
|
|
3375
|
+
currency: getGlobalCurrency(),
|
|
3376
|
+
total: 0,
|
|
3377
|
+
orderId: checkoutId || void 0
|
|
3378
|
+
};
|
|
3379
|
+
this.confirmedMethod = vioMethod;
|
|
3380
|
+
this.orderConfirmed = true;
|
|
3381
|
+
this.open = true;
|
|
3382
|
+
this.dispatchEvent(
|
|
3383
|
+
new CustomEvent("vio:payment-success", {
|
|
3384
|
+
bubbles: true,
|
|
3385
|
+
composed: true,
|
|
3386
|
+
detail: { method: vioMethod, sponsorId, orderId: checkoutId }
|
|
3387
|
+
})
|
|
3388
|
+
);
|
|
3389
|
+
} else if (outcome === "failed") {
|
|
3390
|
+
this.paymentError = "Betalingen ble avbrutt eller feilet. Vennligst pr\xF8v igjen.";
|
|
3391
|
+
this.open = true;
|
|
3392
|
+
} else if (sawStatus) {
|
|
3393
|
+
this.paymentNotice = "Betalingen behandles fortsatt. Du f\xE5r ordrebekreftelse p\xE5 e-post n\xE5r den er gjennomf\xF8rt. Handlekurven er uendret.";
|
|
3394
|
+
this.open = true;
|
|
3395
|
+
} else {
|
|
3396
|
+
this.paymentError = "Vi kunne ikke bekrefte betalingen. Handlekurven er uendret \u2014 pr\xF8v igjen, eller kontakt support hvis du ble belastet.";
|
|
3397
|
+
this.open = true;
|
|
3398
|
+
}
|
|
3399
|
+
}
|
|
3400
|
+
async checkReturnPaymentStatus() {
|
|
3401
|
+
if (typeof window === "undefined" || !window.location || !window.location.search) return;
|
|
3402
|
+
try {
|
|
3403
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
3404
|
+
const vioPayment = urlParams.get("vio_payment");
|
|
3405
|
+
const vioMethod = urlParams.get("vio_method") || "stripe";
|
|
3406
|
+
const sponsorId = Number(urlParams.get("vio_sponsor") || 0);
|
|
3407
|
+
const checkoutId = urlParams.get("checkout_id") || "";
|
|
3408
|
+
if (!vioPayment && !checkoutId) return;
|
|
3409
|
+
if (vioMethod === "klarna" && typeof sessionStorage !== "undefined" && sessionStorage.getItem("vio.klarna.pending.v1")) {
|
|
3410
|
+
this.cleanReturnQueryParams(["vio_payment", "vio_method", "vio_sponsor", "checkout_id"]);
|
|
3411
|
+
return;
|
|
3412
|
+
}
|
|
3413
|
+
if (vioMethod === "vipps" && checkoutId) {
|
|
3414
|
+
if (!this.checkoutState) {
|
|
3415
|
+
const spIdToUse = sponsorId || [...Vio.cart.getAllCarts().keys()][0] || 0;
|
|
3416
|
+
if (spIdToUse) {
|
|
3417
|
+
try {
|
|
3418
|
+
Vio.checkout.open(spIdToUse);
|
|
3419
|
+
Vio.checkout.selectPaymentMethod("vipps");
|
|
3420
|
+
} catch {
|
|
3421
|
+
}
|
|
3422
|
+
}
|
|
3423
|
+
}
|
|
3424
|
+
this.open = true;
|
|
3425
|
+
this.paymentNotice = "Bekrefter betalingen med Vipps\u2026";
|
|
3426
|
+
const { outcome, sawStatus } = await this.pollVippsStatus(checkoutId, sponsorId);
|
|
3427
|
+
this.paymentNotice = null;
|
|
3428
|
+
this.applyReturnOutcome(outcome, sawStatus, vioMethod, sponsorId, checkoutId);
|
|
3429
|
+
this.cleanReturnQueryParams(["vio_payment", "vio_method", "vio_sponsor", "checkout_id"]);
|
|
3430
|
+
} else if (vioPayment === "success") {
|
|
3431
|
+
let outcome = "pending";
|
|
3432
|
+
let sawStatus = false;
|
|
3433
|
+
if (checkoutId) {
|
|
3434
|
+
this.open = true;
|
|
3435
|
+
this.paymentNotice = "Bekrefter betalingen\u2026";
|
|
3436
|
+
for (const delayMs of _VioCheckout.RETURN_VERIFY_DELAYS_MS) {
|
|
3437
|
+
if (delayMs > 0) await new Promise((r) => setTimeout(r, delayMs));
|
|
3438
|
+
try {
|
|
3439
|
+
const checkout = await Vio.checkout.getCheckout(checkoutId, sponsorId || void 0);
|
|
3440
|
+
const status = String(checkout?.status ?? "").toLowerCase();
|
|
3441
|
+
sawStatus = true;
|
|
3442
|
+
if (_VioCheckout.FAILED_STATUSES.has(status)) {
|
|
3443
|
+
outcome = "failed";
|
|
3444
|
+
break;
|
|
3445
|
+
}
|
|
3446
|
+
if (_VioCheckout.PAID_STATUSES.has(status) || Boolean(checkout?.origin_payment_id)) {
|
|
3447
|
+
outcome = "paid";
|
|
3448
|
+
break;
|
|
3449
|
+
}
|
|
3450
|
+
} catch (err) {
|
|
3451
|
+
if (typeof console !== "undefined") {
|
|
3452
|
+
console.warn("[VioCheckout] return verification attempt failed:", err);
|
|
3453
|
+
}
|
|
3454
|
+
}
|
|
3455
|
+
}
|
|
3456
|
+
this.paymentNotice = null;
|
|
3457
|
+
}
|
|
3458
|
+
this.applyReturnOutcome(outcome, sawStatus, vioMethod, sponsorId, checkoutId);
|
|
3459
|
+
this.cleanReturnQueryParams(["vio_payment", "vio_method", "vio_sponsor", "checkout_id"]);
|
|
3460
|
+
} else if (vioPayment === "cancel" || vioPayment === "error") {
|
|
3461
|
+
this.paymentError = "Betalingen ble avbrutt eller feilet. Vennligst pr\xF8v igjen.";
|
|
3462
|
+
this.open = true;
|
|
3463
|
+
this.cleanReturnQueryParams(["vio_payment", "vio_method", "vio_sponsor", "checkout_id"]);
|
|
3464
|
+
}
|
|
3465
|
+
} catch (err) {
|
|
3466
|
+
if (typeof console !== "undefined") {
|
|
3467
|
+
console.warn("[VioCheckout] checkReturnPaymentStatus failed:", err);
|
|
3468
|
+
}
|
|
3469
|
+
}
|
|
3470
|
+
}
|
|
3471
|
+
/** Remove our payment-return params from the URL without a reload. */
|
|
3472
|
+
cleanReturnQueryParams(keys) {
|
|
3473
|
+
try {
|
|
3474
|
+
if (typeof window === "undefined" || !window.history || !window.location) return;
|
|
3475
|
+
const url = new URL(window.location.href);
|
|
3476
|
+
let changed = false;
|
|
3477
|
+
keys.forEach((k) => {
|
|
3478
|
+
if (url.searchParams.has(k)) {
|
|
3479
|
+
url.searchParams.delete(k);
|
|
3480
|
+
changed = true;
|
|
3481
|
+
}
|
|
3482
|
+
});
|
|
3483
|
+
if (changed) {
|
|
3484
|
+
const newSearch = url.searchParams.toString();
|
|
3485
|
+
const newUrl = url.pathname + (newSearch ? "?" + newSearch : "") + url.hash;
|
|
3486
|
+
window.history.replaceState({}, document.title, newUrl);
|
|
3487
|
+
}
|
|
3488
|
+
} catch {
|
|
2562
3489
|
}
|
|
2563
3490
|
}
|
|
2564
3491
|
disconnectedCallback() {
|
|
@@ -2568,7 +3495,10 @@ var VioCheckout = class extends LitElement {
|
|
|
2568
3495
|
this.unmountKlarna();
|
|
2569
3496
|
super.disconnectedCallback();
|
|
2570
3497
|
}
|
|
2571
|
-
updated() {
|
|
3498
|
+
updated(changed) {
|
|
3499
|
+
if (changed.has("open") && this.open && this.availableShippingsList.length === 0) {
|
|
3500
|
+
void this.loadAvailableShippings();
|
|
3501
|
+
}
|
|
2572
3502
|
void this.mountKlarnaIfNeeded();
|
|
2573
3503
|
}
|
|
2574
3504
|
close() {
|
|
@@ -2578,6 +3508,83 @@ var VioCheckout = class extends LitElement {
|
|
|
2578
3508
|
}
|
|
2579
3509
|
show() {
|
|
2580
3510
|
this.open = true;
|
|
3511
|
+
if (this.availableShippingsList.length === 0) {
|
|
3512
|
+
void this.loadAvailableShippings();
|
|
3513
|
+
}
|
|
3514
|
+
}
|
|
3515
|
+
/** Fetch the real per-supplier shippings and preselect + persist the first. */
|
|
3516
|
+
async loadAvailableShippings() {
|
|
3517
|
+
const spId = this.checkoutState?.sponsorId;
|
|
3518
|
+
if (!spId) return;
|
|
3519
|
+
if (this.loadingShippings) return;
|
|
3520
|
+
this.loadingShippings = true;
|
|
3521
|
+
this.shippingsAttempted = true;
|
|
3522
|
+
try {
|
|
3523
|
+
const shippings = await Vio.checkout.fetchAvailableShippings(spId);
|
|
3524
|
+
if (Array.isArray(shippings) && shippings.length > 0) {
|
|
3525
|
+
this.availableShippingsList = shippings;
|
|
3526
|
+
if (!this.selectedShipping || !shippings.some((s) => s.id === this.selectedShipping)) {
|
|
3527
|
+
const firstOpt = shippings[0];
|
|
3528
|
+
this.selectedShipping = firstOpt.id;
|
|
3529
|
+
if (firstOpt.supplierId) {
|
|
3530
|
+
void Vio.checkout.updateShippingsBySupplier(
|
|
3531
|
+
[{ shipping_id: firstOpt.id, supplier_id: Number(firstOpt.supplierId) }],
|
|
3532
|
+
spId
|
|
3533
|
+
);
|
|
3534
|
+
}
|
|
3535
|
+
}
|
|
3536
|
+
}
|
|
3537
|
+
} catch (err) {
|
|
3538
|
+
if (typeof console !== "undefined") {
|
|
3539
|
+
console.warn("[VioCheckout] loadAvailableShippings failed:", err);
|
|
3540
|
+
}
|
|
3541
|
+
} finally {
|
|
3542
|
+
this.loadingShippings = false;
|
|
3543
|
+
}
|
|
3544
|
+
}
|
|
3545
|
+
/** Whether all mandatory Leveringsadresse fields are filled. */
|
|
3546
|
+
get isAddressValid() {
|
|
3547
|
+
const f = this.form;
|
|
3548
|
+
if (!f) return false;
|
|
3549
|
+
return Boolean(
|
|
3550
|
+
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
|
|
3551
|
+
);
|
|
3552
|
+
}
|
|
3553
|
+
/** Whether a valid shipping option is selected (when shippings exist). */
|
|
3554
|
+
get hasSelectedShipping() {
|
|
3555
|
+
if (this.availableShippingsList.length === 0) return true;
|
|
3556
|
+
return Boolean(
|
|
3557
|
+
this.selectedShipping && this.shippingList.some((s) => s.id === this.selectedShipping)
|
|
3558
|
+
);
|
|
3559
|
+
}
|
|
3560
|
+
/** Which payment buttons to render — backend-configured per sponsor. */
|
|
3561
|
+
async loadAvailablePaymentMethods() {
|
|
3562
|
+
if (!this.checkoutState) return;
|
|
3563
|
+
if (this.loadingPaymentMethods) return;
|
|
3564
|
+
this.loadingPaymentMethods = true;
|
|
3565
|
+
try {
|
|
3566
|
+
const methods = await Vio.checkout.getAvailablePaymentMethods(
|
|
3567
|
+
this.checkoutState.sponsorId
|
|
3568
|
+
);
|
|
3569
|
+
if (Array.isArray(methods)) {
|
|
3570
|
+
this.availableMethods = methods.map((m) => m.name);
|
|
3571
|
+
} else {
|
|
3572
|
+
this.availableMethods = null;
|
|
3573
|
+
}
|
|
3574
|
+
} catch (err) {
|
|
3575
|
+
if (typeof console !== "undefined") {
|
|
3576
|
+
console.warn("[VioCheckout] Failed to load payment methods:", err);
|
|
3577
|
+
}
|
|
3578
|
+
this.availableMethods = null;
|
|
3579
|
+
} finally {
|
|
3580
|
+
this.loadingPaymentMethods = false;
|
|
3581
|
+
}
|
|
3582
|
+
}
|
|
3583
|
+
/** Backend names arrive as e.g. "Apple Pay" — compare letters only. */
|
|
3584
|
+
methodEnabled(name) {
|
|
3585
|
+
if (this.availableMethods === null) return true;
|
|
3586
|
+
const norm = (s) => s.toLowerCase().replace(/[^a-z]/g, "");
|
|
3587
|
+
return this.availableMethods.some((m) => norm(m) === norm(name));
|
|
2581
3588
|
}
|
|
2582
3589
|
async refreshApplePay() {
|
|
2583
3590
|
Vio.checkout.clearApplePayCache();
|
|
@@ -2616,6 +3623,12 @@ var VioCheckout = class extends LitElement {
|
|
|
2616
3623
|
this.klarnaMountedShipping = this.selectedShipping;
|
|
2617
3624
|
this.klarnaCategories = handle.categories;
|
|
2618
3625
|
this.klarnaSelectedCat = handle.selected;
|
|
3626
|
+
if (Array.isArray(handle.shippings) && handle.shippings.length > 0) {
|
|
3627
|
+
this.availableShippingsList = handle.shippings;
|
|
3628
|
+
if (!this.selectedShipping || !handle.shippings.some((s) => s.id === this.selectedShipping)) {
|
|
3629
|
+
this.selectedShipping = handle.shippings[0].id;
|
|
3630
|
+
}
|
|
3631
|
+
}
|
|
2619
3632
|
} catch (err) {
|
|
2620
3633
|
if (typeof console !== "undefined") {
|
|
2621
3634
|
console.warn("[VioCheckout] Klarna Payments mount failed:", err);
|
|
@@ -2636,14 +3649,33 @@ var VioCheckout = class extends LitElement {
|
|
|
2636
3649
|
this.klarnaSelectedCat = "";
|
|
2637
3650
|
this.klarnaAuthorizing = false;
|
|
2638
3651
|
}
|
|
3652
|
+
/** Shippings in effect: backend per-supplier when loaded, static otherwise. */
|
|
3653
|
+
get shippingList() {
|
|
3654
|
+
return this.availableShippingsList.length > 0 ? this.availableShippingsList : KLARNA_SHIPPING_OPTIONS;
|
|
3655
|
+
}
|
|
2639
3656
|
/** Selected shipping option (express flow). */
|
|
2640
3657
|
get shippingOption() {
|
|
2641
|
-
|
|
3658
|
+
const list = this.shippingList;
|
|
3659
|
+
return list.find((o) => o.id === this.selectedShipping) ?? list[0] ?? null;
|
|
2642
3660
|
}
|
|
2643
|
-
/** Pick a shipping option → re-mounts the Klarna widget with the new total.
|
|
2644
|
-
|
|
3661
|
+
/** Pick a shipping option → re-mounts the Klarna widget with the new total.
|
|
3662
|
+
* Also persists the choice on the backend cart (per supplier). */
|
|
3663
|
+
async onSelectShipping(id) {
|
|
2645
3664
|
if (this.selectedShipping === id || this.klarnaMounting) return;
|
|
2646
3665
|
this.selectedShipping = id;
|
|
3666
|
+
const selectedOpt = this.shippingList.find((s) => s.id === id);
|
|
3667
|
+
if (selectedOpt?.supplierId) {
|
|
3668
|
+
try {
|
|
3669
|
+
await Vio.checkout.updateShippingsBySupplier(
|
|
3670
|
+
[{ shipping_id: selectedOpt.id, supplier_id: Number(selectedOpt.supplierId) }],
|
|
3671
|
+
this.checkoutState?.sponsorId
|
|
3672
|
+
);
|
|
3673
|
+
} catch (err) {
|
|
3674
|
+
if (typeof console !== "undefined") {
|
|
3675
|
+
console.warn("[VioCheckout] updateShippingsBySupplier failed:", err);
|
|
3676
|
+
}
|
|
3677
|
+
}
|
|
3678
|
+
}
|
|
2647
3679
|
}
|
|
2648
3680
|
/** Switch the Klarna widget to a different payment_method_category. */
|
|
2649
3681
|
async onKlarnaCategory(identifier) {
|
|
@@ -2661,6 +3693,7 @@ var VioCheckout = class extends LitElement {
|
|
|
2661
3693
|
async onKlarnaAuthorize() {
|
|
2662
3694
|
if (!this.klarnaHandle || this.klarnaAuthorizing) return;
|
|
2663
3695
|
this.paymentError = null;
|
|
3696
|
+
this.paymentNotice = null;
|
|
2664
3697
|
this.klarnaAuthorizing = true;
|
|
2665
3698
|
try {
|
|
2666
3699
|
await this.klarnaHandle.authorize();
|
|
@@ -2685,24 +3718,80 @@ var VioCheckout = class extends LitElement {
|
|
|
2685
3718
|
this.applePayInProgress = false;
|
|
2686
3719
|
}
|
|
2687
3720
|
}
|
|
2688
|
-
onPay(method) {
|
|
3721
|
+
async onPay(method) {
|
|
2689
3722
|
this.paymentError = null;
|
|
3723
|
+
this.paymentNotice = null;
|
|
2690
3724
|
if (method === "apple-pay") {
|
|
2691
3725
|
void this.onApplePay();
|
|
2692
3726
|
return;
|
|
2693
3727
|
}
|
|
3728
|
+
if (method === "vipps") {
|
|
3729
|
+
const email = String(this.form.email ?? "").trim();
|
|
3730
|
+
if (!email || !email.includes("@")) {
|
|
3731
|
+
this.paymentError = "Vennligst fyll inn e-postadressen din.";
|
|
3732
|
+
return;
|
|
3733
|
+
}
|
|
3734
|
+
} else {
|
|
3735
|
+
if (!this.isAddressValid) {
|
|
3736
|
+
this.paymentError = "Vennligst fyll ut alle feltene i leveringsadresse.";
|
|
3737
|
+
return;
|
|
3738
|
+
}
|
|
3739
|
+
if (!this.hasSelectedShipping) {
|
|
3740
|
+
this.paymentError = "Vennligst velg en fraktmetode.";
|
|
3741
|
+
return;
|
|
3742
|
+
}
|
|
3743
|
+
}
|
|
2694
3744
|
Vio.checkout.selectPaymentMethod(method);
|
|
3745
|
+
if (method === "stripe") {
|
|
3746
|
+
const selectedOpt = this.shippingList.find((s) => s.id === this.selectedShipping);
|
|
3747
|
+
if (selectedOpt?.supplierId) {
|
|
3748
|
+
try {
|
|
3749
|
+
await Vio.checkout.updateShippingsBySupplier(
|
|
3750
|
+
[{ shipping_id: selectedOpt.id, supplier_id: Number(selectedOpt.supplierId) }],
|
|
3751
|
+
this.checkoutState?.sponsorId
|
|
3752
|
+
);
|
|
3753
|
+
} catch (err) {
|
|
3754
|
+
if (typeof console !== "undefined") {
|
|
3755
|
+
console.warn("[VioCheckout] updateShippingsBySupplier on stripe pay failed:", err);
|
|
3756
|
+
}
|
|
3757
|
+
}
|
|
3758
|
+
}
|
|
3759
|
+
this.stripeLoading = true;
|
|
3760
|
+
void Vio.checkout.startStripePayment(this.checkoutState?.sponsorId, this.form).catch((err) => {
|
|
3761
|
+
this.paymentError = `Kunne ikke starte Stripe-betaling: ${err instanceof Error ? err.message : String(err)}`;
|
|
3762
|
+
}).finally(() => {
|
|
3763
|
+
this.stripeLoading = false;
|
|
3764
|
+
});
|
|
3765
|
+
}
|
|
3766
|
+
if (method === "vipps") {
|
|
3767
|
+
this.vippsLoading = true;
|
|
3768
|
+
void Vio.checkout.startVippsPayment(this.checkoutState?.sponsorId, this.form).catch((err) => {
|
|
3769
|
+
this.paymentError = `Kunne ikke starte Vipps-betaling: ${err instanceof Error ? err.message : String(err)}`;
|
|
3770
|
+
}).finally(() => {
|
|
3771
|
+
this.vippsLoading = false;
|
|
3772
|
+
});
|
|
3773
|
+
}
|
|
2695
3774
|
}
|
|
2696
3775
|
/**
|
|
2697
|
-
* Complete the order for the selected method
|
|
2698
|
-
*
|
|
2699
|
-
*
|
|
2700
|
-
* the capture happens backend-side in the full flow — so this confirms the
|
|
2701
|
-
* order state-only and shows the confirmation screen.
|
|
3776
|
+
* Complete the order for the selected method. Apple Pay and Klarna Express
|
|
3777
|
+
* finish inside their own sheet/popup; Stripe and Vipps redirect to their
|
|
3778
|
+
* hosted pages (via onPay). Anything else confirms state-only.
|
|
2702
3779
|
*/
|
|
2703
3780
|
onCompleteOrder() {
|
|
2704
3781
|
const s = this.checkoutState;
|
|
2705
3782
|
if (!s || !s.paymentMethod) return;
|
|
3783
|
+
if (s.paymentMethod !== "vipps" && !this.isAddressValid) {
|
|
3784
|
+
this.paymentError = "Vennligst fyll ut alle feltene i leveringsadresse.";
|
|
3785
|
+
return;
|
|
3786
|
+
}
|
|
3787
|
+
if (s.paymentMethod === "stripe" || s.paymentMethod === "vipps") {
|
|
3788
|
+
void this.onPay(s.paymentMethod);
|
|
3789
|
+
return;
|
|
3790
|
+
}
|
|
3791
|
+
if (s.paymentMethod !== "klarna" && s.paymentMethod !== "apple-pay") {
|
|
3792
|
+
this.paymentError = "Betalingsmetoden er ikke tilgjengelig enn\xE5.";
|
|
3793
|
+
return;
|
|
3794
|
+
}
|
|
2706
3795
|
this.confirmOrder(s.paymentMethod, s.sponsorId);
|
|
2707
3796
|
}
|
|
2708
3797
|
/**
|
|
@@ -2739,8 +3828,10 @@ var VioCheckout = class extends LitElement {
|
|
|
2739
3828
|
return "Klarna";
|
|
2740
3829
|
case "vipps":
|
|
2741
3830
|
return "Vipps";
|
|
3831
|
+
case "stripe":
|
|
3832
|
+
return "Stripe";
|
|
2742
3833
|
case "card":
|
|
2743
|
-
return "
|
|
3834
|
+
return "Kort";
|
|
2744
3835
|
default:
|
|
2745
3836
|
return "";
|
|
2746
3837
|
}
|
|
@@ -2754,9 +3845,14 @@ var VioCheckout = class extends LitElement {
|
|
|
2754
3845
|
if (!s) return "";
|
|
2755
3846
|
return formatPrice(s.subtotal, s.currency);
|
|
2756
3847
|
}
|
|
2757
|
-
/** Shipping cost (major units)
|
|
3848
|
+
/** Shipping cost (major units) whenever a shipping option is in effect —
|
|
3849
|
+
* express always has one; the normal flow has one once the backend
|
|
3850
|
+
* shippings loaded (the choice IS persisted on the cart and charged). */
|
|
2758
3851
|
shippingMajor() {
|
|
2759
|
-
|
|
3852
|
+
if (!this.express && this.availableShippingsList.length === 0) return 0;
|
|
3853
|
+
const o = this.shippingOption;
|
|
3854
|
+
if (!o) return 0;
|
|
3855
|
+
return o.priceMajor ?? o.price / 100;
|
|
2760
3856
|
}
|
|
2761
3857
|
/** Total to charge — items + shipping (express). */
|
|
2762
3858
|
payTotalLabel() {
|
|
@@ -2815,24 +3911,26 @@ var VioCheckout = class extends LitElement {
|
|
|
2815
3911
|
}
|
|
2816
3912
|
/** Klarna Payments widget panel: shipping + category chips + widget + pay button. */
|
|
2817
3913
|
renderKlarnaPanel() {
|
|
2818
|
-
const currency = this.checkoutState?.currency
|
|
3914
|
+
const currency = this.checkoutState?.currency || getGlobalCurrency();
|
|
2819
3915
|
return html`
|
|
2820
3916
|
<div class="klarna-panel">
|
|
2821
3917
|
${this.express ? html`
|
|
2822
3918
|
<div class="ship-select" role="radiogroup" aria-label="Frakt">
|
|
2823
|
-
${
|
|
3919
|
+
${this.shippingList.map(
|
|
2824
3920
|
(o) => html`
|
|
2825
3921
|
<button
|
|
2826
3922
|
class="ship-opt ${this.selectedShipping === o.id ? "active" : ""}"
|
|
2827
3923
|
role="radio"
|
|
2828
3924
|
aria-checked=${this.selectedShipping === o.id}
|
|
2829
3925
|
?disabled=${this.klarnaMounting}
|
|
2830
|
-
@click=${() => this.onSelectShipping(o.id)}
|
|
3926
|
+
@click=${() => void this.onSelectShipping(o.id)}
|
|
2831
3927
|
>
|
|
2832
3928
|
<span class="ship-meta">
|
|
2833
|
-
<b>${o.method}</b><span>${o.description}</span>
|
|
3929
|
+
<b>${o.method || o.name}</b><span>${o.description}</span>
|
|
3930
|
+
</span>
|
|
3931
|
+
<span class="ship-price">
|
|
3932
|
+
${formatPrice(o.priceMajor ?? o.price / 100, o.currency || currency)}
|
|
2834
3933
|
</span>
|
|
2835
|
-
<span class="ship-price">${formatPrice(o.price / 100, currency)}</span>
|
|
2836
3934
|
</button>
|
|
2837
3935
|
`
|
|
2838
3936
|
)}
|
|
@@ -2852,19 +3950,43 @@ var VioCheckout = class extends LitElement {
|
|
|
2852
3950
|
)}
|
|
2853
3951
|
</div>
|
|
2854
3952
|
` : ""}
|
|
3953
|
+
${this.klarnaMounting ? html`
|
|
3954
|
+
<div style="padding: 20px; text-align: center; color: var(--vio-color-text-secondary, #666); font-size: 14px;">
|
|
3955
|
+
Klargjør Klarna-betaling…
|
|
3956
|
+
</div>
|
|
3957
|
+
` : ""}
|
|
2855
3958
|
<div class="klarna-widget" id="vio-klarna-payments-container"></div>
|
|
2856
3959
|
<button
|
|
2857
3960
|
class="payment-btn primary complete-cta"
|
|
2858
3961
|
@click=${() => void this.onKlarnaAuthorize()}
|
|
2859
3962
|
?disabled=${this.klarnaAuthorizing || !this.klarnaHandle || this.klarnaMounting}
|
|
2860
3963
|
>
|
|
2861
|
-
${this.klarnaAuthorizing ? "Behandler\u2026" : `Betal ${this.payTotalLabel()} med Klarna`}
|
|
3964
|
+
${this.klarnaAuthorizing ? "Behandler\u2026" : this.klarnaMounting ? "Klargj\xF8r Klarna\u2026" : `Betal ${this.payTotalLabel()} med Klarna`}
|
|
2862
3965
|
</button>
|
|
2863
3966
|
</div>
|
|
2864
3967
|
`;
|
|
2865
3968
|
}
|
|
2866
3969
|
/** Express layout — only the Klarna widget + a compact order summary. */
|
|
2867
3970
|
renderKlarnaExpress() {
|
|
3971
|
+
const currency = this.checkoutState?.currency || getGlobalCurrency();
|
|
3972
|
+
if (!this.klarnaAvailable) {
|
|
3973
|
+
return html`
|
|
3974
|
+
<section class="section">
|
|
3975
|
+
<h3 class="section-heading">Kjøp med Klarna</h3>
|
|
3976
|
+
<div class="payment-error">
|
|
3977
|
+
Klarna er ikke tilgjengelig akkurat nå (kan være blokkert av nettleseren).
|
|
3978
|
+
</div>
|
|
3979
|
+
<button
|
|
3980
|
+
class="payment-btn primary complete-cta"
|
|
3981
|
+
@click=${() => {
|
|
3982
|
+
this.express = false;
|
|
3983
|
+
}}
|
|
3984
|
+
>
|
|
3985
|
+
Gå til vanlig kasse
|
|
3986
|
+
</button>
|
|
3987
|
+
</section>
|
|
3988
|
+
`;
|
|
3989
|
+
}
|
|
2868
3990
|
return html`
|
|
2869
3991
|
<section class="section">
|
|
2870
3992
|
<h3 class="section-heading">Kjøp med Klarna</h3>
|
|
@@ -2874,13 +3996,13 @@ var VioCheckout = class extends LitElement {
|
|
|
2874
3996
|
(item) => html`
|
|
2875
3997
|
<div class="order-row">
|
|
2876
3998
|
<span>${item.brand ? `${item.brand} ` : ""}${item.name} ×${item.quantity}</span>
|
|
2877
|
-
<span>${formatPrice(item.unitPrice * item.quantity, item.currency)}</span>
|
|
3999
|
+
<span>${formatPrice(item.unitPrice * item.quantity, item.currency || currency)}</span>
|
|
2878
4000
|
</div>
|
|
2879
4001
|
`
|
|
2880
4002
|
)}
|
|
2881
4003
|
<div class="order-row">
|
|
2882
4004
|
<span>Frakt${this.shippingOption ? ` \u2013 ${this.shippingOption.method}` : ""}</span>
|
|
2883
|
-
<span>${formatPrice(this.shippingMajor(),
|
|
4005
|
+
<span>${formatPrice(this.shippingMajor(), currency)}</span>
|
|
2884
4006
|
</div>
|
|
2885
4007
|
<div class="order-row total">
|
|
2886
4008
|
<span>Totalt</span><span>${this.payTotalLabel()}</span>
|
|
@@ -2888,6 +4010,7 @@ var VioCheckout = class extends LitElement {
|
|
|
2888
4010
|
</div>
|
|
2889
4011
|
` : ""}
|
|
2890
4012
|
${this.renderKlarnaPanel()}
|
|
4013
|
+
${this.paymentNotice ? html`<div class="payment-notice">${this.paymentNotice}</div>` : ""}
|
|
2891
4014
|
${this.paymentError ? html`<div class="payment-error">${this.paymentError}</div>` : ""}
|
|
2892
4015
|
</section>
|
|
2893
4016
|
`;
|
|
@@ -2895,24 +4018,28 @@ var VioCheckout = class extends LitElement {
|
|
|
2895
4018
|
renderCheckoutBody() {
|
|
2896
4019
|
const method = this.checkoutState?.paymentMethod;
|
|
2897
4020
|
const showCompleteCta = !!method && !this.isExpressMethod(method);
|
|
4021
|
+
const isVipps = method === "vipps";
|
|
2898
4022
|
return html`
|
|
4023
|
+
${!isVipps ? html`
|
|
2899
4024
|
<section class="section">
|
|
2900
4025
|
<div class="section-label">Steg 1</div>
|
|
2901
4026
|
<h3 class="section-heading">Leveringsadresse</h3>
|
|
2902
4027
|
<div class="form-row">
|
|
2903
4028
|
<div class="field">
|
|
2904
|
-
<label>Fornavn
|
|
4029
|
+
<label>Fornavn *</label>
|
|
2905
4030
|
<input
|
|
2906
4031
|
type="text"
|
|
4032
|
+
required
|
|
2907
4033
|
autocomplete="given-name"
|
|
2908
4034
|
.value=${this.form.firstName}
|
|
2909
4035
|
@input=${(e) => this.onFieldChange("firstName", e.target.value)}
|
|
2910
4036
|
/>
|
|
2911
4037
|
</div>
|
|
2912
4038
|
<div class="field">
|
|
2913
|
-
<label>Etternavn
|
|
4039
|
+
<label>Etternavn *</label>
|
|
2914
4040
|
<input
|
|
2915
4041
|
type="text"
|
|
4042
|
+
required
|
|
2916
4043
|
autocomplete="family-name"
|
|
2917
4044
|
.value=${this.form.lastName}
|
|
2918
4045
|
@input=${(e) => this.onFieldChange("lastName", e.target.value)}
|
|
@@ -2920,18 +4047,20 @@ var VioCheckout = class extends LitElement {
|
|
|
2920
4047
|
</div>
|
|
2921
4048
|
</div>
|
|
2922
4049
|
<div class="field">
|
|
2923
|
-
<label>E-post
|
|
4050
|
+
<label>E-post *</label>
|
|
2924
4051
|
<input
|
|
2925
4052
|
type="email"
|
|
4053
|
+
required
|
|
2926
4054
|
autocomplete="email"
|
|
2927
4055
|
.value=${this.form.email}
|
|
2928
4056
|
@input=${(e) => this.onFieldChange("email", e.target.value)}
|
|
2929
4057
|
/>
|
|
2930
4058
|
</div>
|
|
2931
4059
|
<div class="field">
|
|
2932
|
-
<label>Adresse
|
|
4060
|
+
<label>Adresse *</label>
|
|
2933
4061
|
<input
|
|
2934
4062
|
type="text"
|
|
4063
|
+
required
|
|
2935
4064
|
autocomplete="street-address"
|
|
2936
4065
|
.value=${this.form.address}
|
|
2937
4066
|
@input=${(e) => this.onFieldChange("address", e.target.value)}
|
|
@@ -2939,79 +4068,182 @@ var VioCheckout = class extends LitElement {
|
|
|
2939
4068
|
</div>
|
|
2940
4069
|
<div class="form-row">
|
|
2941
4070
|
<div class="field">
|
|
2942
|
-
<label>Postnummer
|
|
4071
|
+
<label>Postnummer *</label>
|
|
2943
4072
|
<input
|
|
2944
4073
|
type="text"
|
|
4074
|
+
required
|
|
2945
4075
|
autocomplete="postal-code"
|
|
2946
4076
|
.value=${this.form.postalCode}
|
|
2947
4077
|
@input=${(e) => this.onFieldChange("postalCode", e.target.value)}
|
|
2948
4078
|
/>
|
|
2949
4079
|
</div>
|
|
2950
4080
|
<div class="field">
|
|
2951
|
-
<label>Sted
|
|
4081
|
+
<label>Sted *</label>
|
|
2952
4082
|
<input
|
|
2953
4083
|
type="text"
|
|
4084
|
+
required
|
|
2954
4085
|
autocomplete="address-level2"
|
|
2955
4086
|
.value=${this.form.city}
|
|
2956
4087
|
@input=${(e) => this.onFieldChange("city", e.target.value)}
|
|
2957
4088
|
/>
|
|
2958
4089
|
</div>
|
|
2959
4090
|
</div>
|
|
4091
|
+
${this.availableShippingsList.length > 0 ? html`
|
|
4092
|
+
<div style="margin-top: 16px;">
|
|
4093
|
+
<label style="display: block; font-weight: 600; font-size: 13px; margin-bottom: 8px;">
|
|
4094
|
+
Fraktmetode *
|
|
4095
|
+
</label>
|
|
4096
|
+
<div class="ship-select">
|
|
4097
|
+
${this.availableShippingsList.map(
|
|
4098
|
+
(o) => html`
|
|
4099
|
+
<button
|
|
4100
|
+
type="button"
|
|
4101
|
+
class="ship-opt ${this.selectedShipping === o.id ? "active" : ""}"
|
|
4102
|
+
@click=${() => void this.onSelectShipping(o.id)}
|
|
4103
|
+
>
|
|
4104
|
+
<span class="ship-meta">
|
|
4105
|
+
<b>${o.method || o.name}</b>
|
|
4106
|
+
${o.description ? html`<span>${o.description}</span>` : ""}
|
|
4107
|
+
</span>
|
|
4108
|
+
<span class="ship-price">
|
|
4109
|
+
${formatPrice(
|
|
4110
|
+
o.priceMajor ?? o.price / 100,
|
|
4111
|
+
o.currency || this.checkoutState?.currency || "NOK"
|
|
4112
|
+
)}
|
|
4113
|
+
</span>
|
|
4114
|
+
</button>
|
|
4115
|
+
`
|
|
4116
|
+
)}
|
|
4117
|
+
</div>
|
|
4118
|
+
</div>
|
|
4119
|
+
` : ""}
|
|
2960
4120
|
</section>
|
|
4121
|
+
` : ""}
|
|
4122
|
+
|
|
4123
|
+
${isVipps ? html`
|
|
4124
|
+
<section class="section">
|
|
4125
|
+
<div class="section-label">Kontakt</div>
|
|
4126
|
+
<div class="field">
|
|
4127
|
+
<label>E-post * <span style="font-weight:400;opacity:0.6">(for kvittering — adresse fylles ut i Vipps)</span></label>
|
|
4128
|
+
<input
|
|
4129
|
+
type="email"
|
|
4130
|
+
required
|
|
4131
|
+
autocomplete="email"
|
|
4132
|
+
.value=${this.form.email}
|
|
4133
|
+
@input=${(e) => this.onFieldChange("email", e.target.value)}
|
|
4134
|
+
/>
|
|
4135
|
+
</div>
|
|
4136
|
+
</section>
|
|
4137
|
+
` : ""}
|
|
2961
4138
|
|
|
2962
4139
|
<section class="section">
|
|
2963
|
-
<div class="section-label"
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
@click=${() => this.onPay("apple-pay")}
|
|
2970
|
-
?disabled=${this.applePayInProgress}
|
|
2971
|
-
aria-pressed=${this.checkoutState?.paymentMethod === "apple-pay"}
|
|
2972
|
-
>
|
|
2973
|
-
${this.applePayInProgress ? "\xC5pner\u2026" : " Pay"}
|
|
2974
|
-
</button>
|
|
2975
|
-
` : ""}
|
|
2976
|
-
${this.klarnaAvailable ? html`
|
|
2977
|
-
<button
|
|
2978
|
-
class="payment-btn"
|
|
2979
|
-
@click=${() => this.onPay("klarna")}
|
|
2980
|
-
aria-pressed=${this.checkoutState?.paymentMethod === "klarna"}
|
|
2981
|
-
>
|
|
2982
|
-
Klarna
|
|
2983
|
-
</button>
|
|
2984
|
-
` : ""}
|
|
2985
|
-
<button
|
|
2986
|
-
class="payment-btn"
|
|
2987
|
-
@click=${() => this.onPay("vipps")}
|
|
2988
|
-
aria-pressed=${this.checkoutState?.paymentMethod === "vipps"}
|
|
2989
|
-
>
|
|
2990
|
-
Vipps
|
|
2991
|
-
</button>
|
|
2992
|
-
<button
|
|
2993
|
-
class="payment-btn"
|
|
2994
|
-
@click=${() => this.onPay("card")}
|
|
2995
|
-
aria-pressed=${this.checkoutState?.paymentMethod === "card"}
|
|
2996
|
-
>
|
|
2997
|
-
Kort
|
|
2998
|
-
</button>
|
|
2999
|
-
${!this.applePayAvailable ? html`
|
|
3000
|
-
<div class="apple-pay-note">
|
|
3001
|
-
Pay krever Safari på iOS eller macOS med Apple Pay konfigurert.
|
|
3002
|
-
</div>
|
|
3003
|
-
` : ""}
|
|
3004
|
-
${showCompleteCta ? html`
|
|
4140
|
+
<div class="section-label">${isVipps ? "Betaling" : "Steg 2"}</div>
|
|
4141
|
+
${method ? html`
|
|
4142
|
+
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px;">
|
|
4143
|
+
<h3 class="section-heading" style="margin:0;">
|
|
4144
|
+
Betalingsmåte: <b>${this.methodLabel(method)}</b>
|
|
4145
|
+
</h3>
|
|
3005
4146
|
<button
|
|
3006
|
-
|
|
3007
|
-
|
|
4147
|
+
type="button"
|
|
4148
|
+
style="background:none; border:none; color:var(--vio-color-accent, #c14a3b); text-decoration:underline; font-size:13px; cursor:pointer;"
|
|
4149
|
+
@click=${() => {
|
|
4150
|
+
this.unmountKlarna();
|
|
4151
|
+
Vio.checkout.selectPaymentMethod("");
|
|
4152
|
+
}}
|
|
3008
4153
|
>
|
|
3009
|
-
|
|
4154
|
+
Endre
|
|
3010
4155
|
</button>
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
4156
|
+
</div>
|
|
4157
|
+
|
|
4158
|
+
${method === "klarna" ? this.renderKlarnaPanel() : ""}
|
|
4159
|
+
${method === "stripe" ? html`
|
|
4160
|
+
<button
|
|
4161
|
+
class="payment-btn primary complete-cta"
|
|
4162
|
+
@click=${() => void this.onPay("stripe")}
|
|
4163
|
+
?disabled=${this.stripeLoading || !this.isAddressValid || !this.hasSelectedShipping}
|
|
4164
|
+
title=${!this.isAddressValid ? "Vennligst fyll ut leveringsadresse" : !this.hasSelectedShipping ? "Vennligst velg en fraktmetode" : ""}
|
|
4165
|
+
>
|
|
4166
|
+
${this.stripeLoading ? "G\xE5r til Stripe\u2026" : `Betal ${this.payTotalLabel()} med Stripe`}
|
|
4167
|
+
</button>
|
|
4168
|
+
` : ""}
|
|
4169
|
+
${method === "vipps" ? html`
|
|
4170
|
+
<button
|
|
4171
|
+
class="payment-btn primary complete-cta"
|
|
4172
|
+
@click=${() => void this.onPay("vipps")}
|
|
4173
|
+
?disabled=${this.vippsLoading}
|
|
4174
|
+
>
|
|
4175
|
+
${this.vippsLoading ? "G\xE5r til Vipps\u2026" : `Betal ${this.payTotalLabel()} med Vipps`}
|
|
4176
|
+
</button>
|
|
4177
|
+
` : ""}
|
|
4178
|
+
${method === "apple-pay" ? html`
|
|
4179
|
+
<button
|
|
4180
|
+
class="payment-btn primary complete-cta"
|
|
4181
|
+
@click=${() => void this.onPay("apple-pay")}
|
|
4182
|
+
?disabled=${this.applePayInProgress || !this.isAddressValid || !this.hasSelectedShipping}
|
|
4183
|
+
title=${!this.isAddressValid ? "Vennligst fyll ut leveringsadresse" : !this.hasSelectedShipping ? "Vennligst velg en fraktmetode" : ""}
|
|
4184
|
+
>
|
|
4185
|
+
${this.applePayInProgress ? "\xC5pner\u2026" : `Betal ${this.payTotalLabel()} med Apple Pay`}
|
|
4186
|
+
</button>
|
|
4187
|
+
` : ""}
|
|
4188
|
+
${showCompleteCta && method !== "stripe" && method !== "vipps" ? html`
|
|
4189
|
+
<button
|
|
4190
|
+
class="payment-btn primary complete-cta"
|
|
4191
|
+
@click=${this.onCompleteOrder}
|
|
4192
|
+
?disabled=${!this.isAddressValid || !this.hasSelectedShipping}
|
|
4193
|
+
title=${!this.isAddressValid ? "Vennligst fyll ut leveringsadresse" : !this.hasSelectedShipping ? "Vennligst velg en fraktmetode" : ""}
|
|
4194
|
+
>
|
|
4195
|
+
Betal ${this.payTotalLabel()} med ${this.methodLabel(method ?? null)}
|
|
4196
|
+
</button>
|
|
4197
|
+
` : ""}
|
|
4198
|
+
${!isVipps && !this.isAddressValid ? html`
|
|
4199
|
+
<div style="font-size: 12px; color: var(--vio-color-accent, #c14a3b); text-align: center; margin-top: 8px;">
|
|
4200
|
+
Vennligst fyll ut leveringsadresse for å fullføre betalingen
|
|
4201
|
+
</div>
|
|
4202
|
+
` : !isVipps && !this.hasSelectedShipping ? html`
|
|
4203
|
+
<div style="font-size: 12px; color: var(--vio-color-accent, #c14a3b); text-align: center; margin-top: 8px;">
|
|
4204
|
+
Vennligst velg en fraktmetode for å fullføre betalingen
|
|
4205
|
+
</div>
|
|
4206
|
+
` : ""}
|
|
4207
|
+
` : html`
|
|
4208
|
+
<h3 class="section-heading">Velg betalingsmåte</h3>
|
|
4209
|
+
<div class="payment-grid">
|
|
4210
|
+
${this.methodEnabled("apple-pay") && this.applePayAvailable ? html`
|
|
4211
|
+
<button
|
|
4212
|
+
class="payment-btn primary"
|
|
4213
|
+
@click=${() => this.onPay("apple-pay")}
|
|
4214
|
+
?disabled=${this.applePayInProgress}
|
|
4215
|
+
>
|
|
4216
|
+
${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>`}
|
|
4217
|
+
</button>
|
|
4218
|
+
` : ""}
|
|
4219
|
+
${this.methodEnabled("stripe") ? html`
|
|
4220
|
+
<button
|
|
4221
|
+
class="payment-btn"
|
|
4222
|
+
@click=${() => this.onPay("stripe")}
|
|
4223
|
+
?disabled=${this.stripeLoading}
|
|
4224
|
+
>
|
|
4225
|
+
<img src=${STRIPE_LOGO_SRC3} style="height: 20px; display: block;" alt="Stripe" />
|
|
4226
|
+
</button>
|
|
4227
|
+
` : ""}
|
|
4228
|
+
${this.methodEnabled("klarna") && this.klarnaAvailable ? html`
|
|
4229
|
+
<button class="payment-btn" @click=${() => this.onPay("klarna")}>
|
|
4230
|
+
Klarna
|
|
4231
|
+
</button>
|
|
4232
|
+
` : ""}
|
|
4233
|
+
${this.methodEnabled("vipps") ? html`
|
|
4234
|
+
<button class="payment-btn" @click=${() => this.onPay("vipps")}>
|
|
4235
|
+
<span style="color:#ff5b24;font-weight:800;font-size:17px;letter-spacing:-0.02em">vipps</span>
|
|
4236
|
+
</button>
|
|
4237
|
+
` : ""}
|
|
4238
|
+
${this.methodEnabled("apple-pay") && !this.applePayAvailable ? html`
|
|
4239
|
+
<div class="apple-pay-note">
|
|
4240
|
+
Pay krever Safari på iOS eller macOS med Apple Pay konfigurert.
|
|
4241
|
+
</div>
|
|
4242
|
+
` : ""}
|
|
4243
|
+
</div>
|
|
4244
|
+
`}
|
|
4245
|
+
${this.paymentNotice ? html`<div class="payment-notice">${this.paymentNotice}</div>` : ""}
|
|
4246
|
+
${this.paymentError ? html`<div class="payment-error">${this.paymentError}</div>` : ""}
|
|
3015
4247
|
</section>
|
|
3016
4248
|
|
|
3017
4249
|
${this.items.length > 0 ? html`
|
|
@@ -3037,10 +4269,11 @@ var VioCheckout = class extends LitElement {
|
|
|
3037
4269
|
<span>Sum</span><span>${this.orderTotal()}</span>
|
|
3038
4270
|
</div>
|
|
3039
4271
|
<div class="order-row">
|
|
3040
|
-
<span>Frakt
|
|
4272
|
+
<span>Frakt${this.shippingMajor() > 0 && this.shippingOption ? ` \u2013 ${this.shippingOption.method || this.shippingOption.name}` : ""}</span>
|
|
4273
|
+
<span>${this.shippingMajor() > 0 ? formatPrice(this.shippingMajor(), this.checkoutState?.currency) : this.shippingLabel}</span>
|
|
3041
4274
|
</div>
|
|
3042
4275
|
<div class="order-row total">
|
|
3043
|
-
<span>Totalt</span><span>${this.
|
|
4276
|
+
<span>Totalt</span><span>${this.payTotalLabel()}</span>
|
|
3044
4277
|
</div>
|
|
3045
4278
|
</section>
|
|
3046
4279
|
` : ""}
|
|
@@ -3051,62 +4284,79 @@ var VioCheckout = class extends LitElement {
|
|
|
3051
4284
|
};
|
|
3052
4285
|
__decorateClass([
|
|
3053
4286
|
property({ type: Boolean, reflect: true })
|
|
3054
|
-
],
|
|
4287
|
+
], _VioCheckout.prototype, "open", 2);
|
|
3055
4288
|
__decorateClass([
|
|
3056
4289
|
property({ type: String })
|
|
3057
|
-
],
|
|
4290
|
+
], _VioCheckout.prototype, "heading", 2);
|
|
3058
4291
|
__decorateClass([
|
|
3059
4292
|
property({ type: String, attribute: "shipping-label" })
|
|
3060
|
-
],
|
|
4293
|
+
], _VioCheckout.prototype, "shippingLabel", 2);
|
|
3061
4294
|
__decorateClass([
|
|
3062
4295
|
property({ type: Boolean, reflect: true })
|
|
3063
|
-
],
|
|
4296
|
+
], _VioCheckout.prototype, "express", 2);
|
|
4297
|
+
__decorateClass([
|
|
4298
|
+
state()
|
|
4299
|
+
], _VioCheckout.prototype, "checkoutState", 2);
|
|
4300
|
+
__decorateClass([
|
|
4301
|
+
state()
|
|
4302
|
+
], _VioCheckout.prototype, "items", 2);
|
|
4303
|
+
__decorateClass([
|
|
4304
|
+
state()
|
|
4305
|
+
], _VioCheckout.prototype, "applePayAvailable", 2);
|
|
4306
|
+
__decorateClass([
|
|
4307
|
+
state()
|
|
4308
|
+
], _VioCheckout.prototype, "applePayInProgress", 2);
|
|
4309
|
+
__decorateClass([
|
|
4310
|
+
state()
|
|
4311
|
+
], _VioCheckout.prototype, "klarnaAvailable", 2);
|
|
3064
4312
|
__decorateClass([
|
|
3065
4313
|
state()
|
|
3066
|
-
],
|
|
4314
|
+
], _VioCheckout.prototype, "orderConfirmed", 2);
|
|
3067
4315
|
__decorateClass([
|
|
3068
4316
|
state()
|
|
3069
|
-
],
|
|
4317
|
+
], _VioCheckout.prototype, "confirmedMethod", 2);
|
|
3070
4318
|
__decorateClass([
|
|
3071
4319
|
state()
|
|
3072
|
-
],
|
|
4320
|
+
], _VioCheckout.prototype, "paymentError", 2);
|
|
3073
4321
|
__decorateClass([
|
|
3074
4322
|
state()
|
|
3075
|
-
],
|
|
4323
|
+
], _VioCheckout.prototype, "paymentNotice", 2);
|
|
3076
4324
|
__decorateClass([
|
|
3077
4325
|
state()
|
|
3078
|
-
],
|
|
4326
|
+
], _VioCheckout.prototype, "confirmedOrder", 2);
|
|
3079
4327
|
__decorateClass([
|
|
3080
4328
|
state()
|
|
3081
|
-
],
|
|
4329
|
+
], _VioCheckout.prototype, "klarnaCategories", 2);
|
|
3082
4330
|
__decorateClass([
|
|
3083
4331
|
state()
|
|
3084
|
-
],
|
|
4332
|
+
], _VioCheckout.prototype, "klarnaSelectedCat", 2);
|
|
3085
4333
|
__decorateClass([
|
|
3086
4334
|
state()
|
|
3087
|
-
],
|
|
4335
|
+
], _VioCheckout.prototype, "klarnaAuthorizing", 2);
|
|
3088
4336
|
__decorateClass([
|
|
3089
4337
|
state()
|
|
3090
|
-
],
|
|
4338
|
+
], _VioCheckout.prototype, "selectedShipping", 2);
|
|
3091
4339
|
__decorateClass([
|
|
3092
4340
|
state()
|
|
3093
|
-
],
|
|
4341
|
+
], _VioCheckout.prototype, "availableShippingsList", 2);
|
|
3094
4342
|
__decorateClass([
|
|
3095
4343
|
state()
|
|
3096
|
-
],
|
|
4344
|
+
], _VioCheckout.prototype, "availableMethods", 2);
|
|
3097
4345
|
__decorateClass([
|
|
3098
4346
|
state()
|
|
3099
|
-
],
|
|
4347
|
+
], _VioCheckout.prototype, "stripeLoading", 2);
|
|
3100
4348
|
__decorateClass([
|
|
3101
4349
|
state()
|
|
3102
|
-
],
|
|
4350
|
+
], _VioCheckout.prototype, "vippsLoading", 2);
|
|
3103
4351
|
__decorateClass([
|
|
3104
4352
|
state()
|
|
3105
|
-
],
|
|
4353
|
+
], _VioCheckout.prototype, "form", 2);
|
|
4354
|
+
var VioCheckout = _VioCheckout;
|
|
3106
4355
|
|
|
3107
4356
|
// src/ui/elements.ts
|
|
3108
4357
|
function registerVioElements() {
|
|
3109
|
-
if (typeof customElements === "undefined")
|
|
4358
|
+
if (typeof customElements === "undefined" || typeof customElements.get !== "function" || typeof customElements.define !== "function")
|
|
4359
|
+
return;
|
|
3110
4360
|
const defs = [
|
|
3111
4361
|
["vio-product", VioProduct],
|
|
3112
4362
|
["vio-product-carousel", VioProductCarousel],
|
|
@@ -3119,6 +4369,6 @@ function registerVioElements() {
|
|
|
3119
4369
|
}
|
|
3120
4370
|
}
|
|
3121
4371
|
|
|
3122
|
-
export { VioCart, VioCheckout, VioProduct, VioProductCarousel, VioProductDetail, registerVioElements };
|
|
3123
|
-
//# sourceMappingURL=chunk-
|
|
3124
|
-
//# sourceMappingURL=chunk-
|
|
4372
|
+
export { VioCart, VioCheckout, VioProduct, VioProductCarousel, VioProductDetail, VioTokens, applyVioTheme, registerVioElements };
|
|
4373
|
+
//# sourceMappingURL=chunk-V5ZQG2H5.js.map
|
|
4374
|
+
//# sourceMappingURL=chunk-V5ZQG2H5.js.map
|