@vio-live/web-sdk 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -24
- package/dist/chunk-NP2KCM7X.js +3859 -0
- package/dist/chunk-NP2KCM7X.js.map +1 -0
- package/dist/{chunk-4CCGE7DE.js → chunk-NWORYWIL.js} +2649 -1711
- package/dist/chunk-NWORYWIL.js.map +1 -0
- package/dist/chunk-PBQSHIID.js +3 -0
- package/dist/{chunk-GGM2AIK5.js.map → chunk-PBQSHIID.js.map} +1 -1
- package/dist/core/index.d.ts +314 -12
- package/dist/core/index.js +2 -2
- package/dist/index.d.ts +1 -4
- package/dist/index.js +2 -3
- package/dist/react/index.d.ts +23 -0
- package/dist/react/index.js +74 -0
- package/dist/react/index.js.map +1 -0
- package/dist/ui/index.d.ts +29 -4
- package/dist/ui/index.js +136 -2
- package/dist/ui/index.js.map +1 -1
- package/dist/{vio-checkout-D8B4Upk8.d.ts → vio-checkout-T9R5hpDx.d.ts} +137 -10
- package/package.json +18 -2
- package/dist/chunk-4CCGE7DE.js.map +0 -1
- package/dist/chunk-5SPY7TH6.js +0 -1618
- package/dist/chunk-5SPY7TH6.js.map +0 -1
- package/dist/chunk-GGM2AIK5.js +0 -3
package/dist/chunk-5SPY7TH6.js
DELETED
|
@@ -1,1618 +0,0 @@
|
|
|
1
|
-
import { GraphQLClient } from 'graphql-request';
|
|
2
|
-
|
|
3
|
-
var __defProp = Object.defineProperty;
|
|
4
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
-
var __decorateClass = (decorators, target, key, kind) => {
|
|
6
|
-
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
|
|
7
|
-
for (var i = decorators.length - 1, decorator; i >= 0; i--)
|
|
8
|
-
if (decorator = decorators[i])
|
|
9
|
-
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
10
|
-
if (kind && result) __defProp(target, key, result);
|
|
11
|
-
return result;
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
// src/core/configuration.ts
|
|
15
|
-
var DEFAULT_URLS = {
|
|
16
|
-
development: {
|
|
17
|
-
apiBase: "https://api-dev.vio.live",
|
|
18
|
-
graphQLBase: "https://graph-ql-dev.vio.live"
|
|
19
|
-
},
|
|
20
|
-
testing: {
|
|
21
|
-
apiBase: "https://api-dev.vio.live",
|
|
22
|
-
graphQLBase: "https://graph-ql-dev.vio.live"
|
|
23
|
-
},
|
|
24
|
-
production: {
|
|
25
|
-
apiBase: "https://api.vio.live",
|
|
26
|
-
graphQLBase: "https://graph-ql.vio.live"
|
|
27
|
-
}
|
|
28
|
-
};
|
|
29
|
-
var ConfigurationSingleton = class {
|
|
30
|
-
constructor() {
|
|
31
|
-
this.resolved = null;
|
|
32
|
-
}
|
|
33
|
-
init(input) {
|
|
34
|
-
if (!input.apiKey || typeof input.apiKey !== "string") {
|
|
35
|
-
throw new Error("[Vio] apiKey is required in Vio.init({ apiKey })");
|
|
36
|
-
}
|
|
37
|
-
const env = input.environment ?? "development";
|
|
38
|
-
const defaults = DEFAULT_URLS[env];
|
|
39
|
-
this.resolved = {
|
|
40
|
-
apiKey: input.apiKey,
|
|
41
|
-
environment: env,
|
|
42
|
-
apiBase: input.apiBase ?? defaults.apiBase,
|
|
43
|
-
graphQLBase: input.graphQLBase ?? defaults.graphQLBase,
|
|
44
|
-
stripePublishableKey: input.stripePublishableKey,
|
|
45
|
-
klarnaClientId: input.klarnaClientId,
|
|
46
|
-
// Sandbox by default — live Klarna must be opted into explicitly.
|
|
47
|
-
klarnaEnvironment: input.klarnaEnvironment ?? "playground"
|
|
48
|
-
};
|
|
49
|
-
if (typeof console !== "undefined") {
|
|
50
|
-
const masked = input.apiKey.length > 4 ? "*".repeat(Math.max(0, input.apiKey.length - 4)) + input.apiKey.slice(-4) : "****";
|
|
51
|
-
console.info("[Vio] Initialized", {
|
|
52
|
-
environment: env,
|
|
53
|
-
apiBase: this.resolved.apiBase,
|
|
54
|
-
apiKey: masked,
|
|
55
|
-
stripeConfigured: !!input.stripePublishableKey,
|
|
56
|
-
klarnaConfigured: !!input.klarnaClientId,
|
|
57
|
-
klarnaEnvironment: this.resolved.klarnaEnvironment
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
get() {
|
|
62
|
-
if (!this.resolved) {
|
|
63
|
-
throw new Error(
|
|
64
|
-
"[Vio] Not initialized. Call Vio.init({ apiKey }) before using the SDK."
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
return this.resolved;
|
|
68
|
-
}
|
|
69
|
-
get isInitialized() {
|
|
70
|
-
return this.resolved !== null;
|
|
71
|
-
}
|
|
72
|
-
/** For tests only. */
|
|
73
|
-
reset() {
|
|
74
|
-
this.resolved = null;
|
|
75
|
-
}
|
|
76
|
-
};
|
|
77
|
-
var Configuration = new ConfigurationSingleton();
|
|
78
|
-
|
|
79
|
-
// src/core/api/vio.ts
|
|
80
|
-
var VioApi = class {
|
|
81
|
-
constructor(options) {
|
|
82
|
-
if (!options.apiBase) throw new Error("[VioApi] apiBase is required");
|
|
83
|
-
if (!options.apiKey) throw new Error("[VioApi] apiKey is required");
|
|
84
|
-
this.apiBase = options.apiBase.replace(/\/+$/, "");
|
|
85
|
-
this.apiKey = options.apiKey;
|
|
86
|
-
}
|
|
87
|
-
/**
|
|
88
|
-
* Bootstrap — fetches client app config (sponsors with commerce keys,
|
|
89
|
-
* default market, campaign id). Called once at SDK init.
|
|
90
|
-
*/
|
|
91
|
-
async bootstrap() {
|
|
92
|
-
return this.get("/v2/mobile/config");
|
|
93
|
-
}
|
|
94
|
-
/**
|
|
95
|
-
* Get placement components for a campaign. Used by the carousel auto-fetch
|
|
96
|
-
* mode (`<vio-product-carousel campaign-id="36" location="news-favorites">`).
|
|
97
|
-
*/
|
|
98
|
-
async getComponents(campaignId) {
|
|
99
|
-
const path = `/v2/mobile/campaigns/${encodeURIComponent(String(campaignId))}/components`;
|
|
100
|
-
const res = await this.get(path);
|
|
101
|
-
return Array.isArray(res) ? res : res?.components ?? [];
|
|
102
|
-
}
|
|
103
|
-
// MARK: - HTTP plumbing
|
|
104
|
-
async get(path) {
|
|
105
|
-
const url = `${this.apiBase}${path}`;
|
|
106
|
-
const res = await fetch(url, {
|
|
107
|
-
method: "GET",
|
|
108
|
-
headers: {
|
|
109
|
-
"X-API-Key": this.apiKey,
|
|
110
|
-
Accept: "application/json"
|
|
111
|
-
}
|
|
112
|
-
});
|
|
113
|
-
if (!res.ok) {
|
|
114
|
-
throw new VioApiError(res.status, await safeText(res), `GET ${path}`);
|
|
115
|
-
}
|
|
116
|
-
return await res.json();
|
|
117
|
-
}
|
|
118
|
-
};
|
|
119
|
-
var VioApiError = class extends Error {
|
|
120
|
-
constructor(status, body, endpoint) {
|
|
121
|
-
super(`[VioApi] ${endpoint} \u2192 HTTP ${status}: ${body.slice(0, 200)}`);
|
|
122
|
-
this.status = status;
|
|
123
|
-
this.body = body;
|
|
124
|
-
this.endpoint = endpoint;
|
|
125
|
-
this.name = "VioApiError";
|
|
126
|
-
}
|
|
127
|
-
};
|
|
128
|
-
async function safeText(res) {
|
|
129
|
-
try {
|
|
130
|
-
return await res.text();
|
|
131
|
-
} catch {
|
|
132
|
-
return "";
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
function createVioApi(options) {
|
|
136
|
-
return new VioApi(options);
|
|
137
|
-
}
|
|
138
|
-
var GET_PRODUCTS_BY_IDS = (
|
|
139
|
-
/* GraphQL */
|
|
140
|
-
`
|
|
141
|
-
query GetProductsByIds(
|
|
142
|
-
$currency: String
|
|
143
|
-
$imageSize: ImageSize
|
|
144
|
-
$productIds: [Int!]
|
|
145
|
-
$useCache: Boolean!
|
|
146
|
-
$shippingCountryCode: String
|
|
147
|
-
) {
|
|
148
|
-
Channel {
|
|
149
|
-
GetProductsByIds(
|
|
150
|
-
currency: $currency
|
|
151
|
-
image_size: $imageSize
|
|
152
|
-
product_ids: $productIds
|
|
153
|
-
useCache: $useCache
|
|
154
|
-
shipping_country_code: $shippingCountryCode
|
|
155
|
-
) {
|
|
156
|
-
id
|
|
157
|
-
brand
|
|
158
|
-
title
|
|
159
|
-
description
|
|
160
|
-
tags
|
|
161
|
-
sku
|
|
162
|
-
quantity
|
|
163
|
-
price {
|
|
164
|
-
amount
|
|
165
|
-
currency_code
|
|
166
|
-
compare_at
|
|
167
|
-
amount_incl_taxes
|
|
168
|
-
compare_at_incl_taxes
|
|
169
|
-
tax_amount
|
|
170
|
-
tax_rate
|
|
171
|
-
}
|
|
172
|
-
variants {
|
|
173
|
-
id
|
|
174
|
-
barcode
|
|
175
|
-
quantity
|
|
176
|
-
sku
|
|
177
|
-
title
|
|
178
|
-
price {
|
|
179
|
-
amount
|
|
180
|
-
currency_code
|
|
181
|
-
compare_at
|
|
182
|
-
amount_incl_taxes
|
|
183
|
-
compare_at_incl_taxes
|
|
184
|
-
tax_amount
|
|
185
|
-
tax_rate
|
|
186
|
-
}
|
|
187
|
-
images {
|
|
188
|
-
id
|
|
189
|
-
url
|
|
190
|
-
width
|
|
191
|
-
height
|
|
192
|
-
order
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
barcode
|
|
196
|
-
options {
|
|
197
|
-
id
|
|
198
|
-
name
|
|
199
|
-
order
|
|
200
|
-
values
|
|
201
|
-
}
|
|
202
|
-
categories {
|
|
203
|
-
id
|
|
204
|
-
name
|
|
205
|
-
}
|
|
206
|
-
images {
|
|
207
|
-
id
|
|
208
|
-
url
|
|
209
|
-
width
|
|
210
|
-
height
|
|
211
|
-
order
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
`
|
|
217
|
-
);
|
|
218
|
-
var ChannelProduct = class {
|
|
219
|
-
constructor(client) {
|
|
220
|
-
this.client = client;
|
|
221
|
-
}
|
|
222
|
-
/**
|
|
223
|
-
* Fetch a batch of products by their numeric ids.
|
|
224
|
-
* Returns an empty array if no ids are provided.
|
|
225
|
-
*/
|
|
226
|
-
async getByIds(options) {
|
|
227
|
-
if (!options.product_ids || options.product_ids.length === 0) return [];
|
|
228
|
-
const variables = {
|
|
229
|
-
productIds: options.product_ids,
|
|
230
|
-
currency: options.currency ?? null,
|
|
231
|
-
imageSize: options.image_size ?? "large",
|
|
232
|
-
useCache: options.useCache ?? true,
|
|
233
|
-
shippingCountryCode: options.shipping_country_code ?? null
|
|
234
|
-
};
|
|
235
|
-
const data = await this.client.request(GET_PRODUCTS_BY_IDS, variables);
|
|
236
|
-
return data.Channel?.GetProductsByIds ?? [];
|
|
237
|
-
}
|
|
238
|
-
};
|
|
239
|
-
var Channel = class {
|
|
240
|
-
constructor(client) {
|
|
241
|
-
this.product = new ChannelProduct(client);
|
|
242
|
-
}
|
|
243
|
-
};
|
|
244
|
-
var CommerceClient = class {
|
|
245
|
-
constructor(options) {
|
|
246
|
-
if (!options.endpoint) throw new Error("[VioCommerce] endpoint is required");
|
|
247
|
-
if (!options.apiKey) throw new Error("[VioCommerce] apiKey is required");
|
|
248
|
-
this.raw = new GraphQLClient(options.endpoint, {
|
|
249
|
-
headers: { Authorization: options.apiKey }
|
|
250
|
-
});
|
|
251
|
-
this.channel = new Channel(this.raw);
|
|
252
|
-
}
|
|
253
|
-
};
|
|
254
|
-
function createCommerceClient(options) {
|
|
255
|
-
return new CommerceClient(options);
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// src/core/types.ts
|
|
259
|
-
function displayPrice(product) {
|
|
260
|
-
const p = product.price;
|
|
261
|
-
const amount = p.amount_incl_taxes ?? p.amount;
|
|
262
|
-
return formatPrice(amount, p.currency_code);
|
|
263
|
-
}
|
|
264
|
-
function formatPrice(amount, currency) {
|
|
265
|
-
const formatted = amount.toLocaleString("no-NO", { minimumFractionDigits: 0, maximumFractionDigits: 2 }).replace(/ /g, " ");
|
|
266
|
-
const symbol = currency === "NOK" ? "kr" : currency;
|
|
267
|
-
return `${formatted} ${symbol}`;
|
|
268
|
-
}
|
|
269
|
-
function primaryImageUrl(product) {
|
|
270
|
-
const variantImage = product.variants[0]?.images[0]?.url;
|
|
271
|
-
if (variantImage) return variantImage;
|
|
272
|
-
return product.images[0]?.url;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// src/core/cart/cart-manager.ts
|
|
276
|
-
var STORAGE_KEY = "vio.cart.v1";
|
|
277
|
-
function lineId() {
|
|
278
|
-
return "li_" + Math.random().toString(36).slice(2, 10) + Date.now().toString(36);
|
|
279
|
-
}
|
|
280
|
-
var CartManager = class extends EventTarget {
|
|
281
|
-
constructor() {
|
|
282
|
-
super();
|
|
283
|
-
this.cartsBySponsor = this.loadFromStorage();
|
|
284
|
-
}
|
|
285
|
-
// MARK: - Mutations
|
|
286
|
-
/** Add a Product DTO to a sponsor's cart. Bumps quantity if already present. */
|
|
287
|
-
addProduct(opts) {
|
|
288
|
-
const unitPrice = opts.product.price.amount_incl_taxes ?? opts.product.price.amount;
|
|
289
|
-
return this.addManual({
|
|
290
|
-
productId: opts.product.id,
|
|
291
|
-
sponsorId: opts.sponsorId,
|
|
292
|
-
sponsorName: opts.sponsorName,
|
|
293
|
-
brand: opts.product.brand ?? "",
|
|
294
|
-
name: opts.product.title,
|
|
295
|
-
unitPrice,
|
|
296
|
-
currency: opts.product.price.currency_code,
|
|
297
|
-
imageUrl: primaryImageUrl(opts.product) ?? "",
|
|
298
|
-
quantity: opts.quantity,
|
|
299
|
-
variantId: opts.variantId
|
|
300
|
-
});
|
|
301
|
-
}
|
|
302
|
-
/** Add a manually-specified line item (no Product DTO). */
|
|
303
|
-
addManual(opts) {
|
|
304
|
-
const qty = Math.max(1, opts.quantity ?? 1);
|
|
305
|
-
let cart = this.cartsBySponsor.get(opts.sponsorId);
|
|
306
|
-
if (!cart) {
|
|
307
|
-
cart = {
|
|
308
|
-
sponsorId: opts.sponsorId,
|
|
309
|
-
sponsorName: opts.sponsorName,
|
|
310
|
-
items: [],
|
|
311
|
-
currency: opts.currency
|
|
312
|
-
};
|
|
313
|
-
this.cartsBySponsor.set(opts.sponsorId, cart);
|
|
314
|
-
} else if (opts.sponsorName && !cart.sponsorName) {
|
|
315
|
-
cart.sponsorName = opts.sponsorName;
|
|
316
|
-
}
|
|
317
|
-
const existing = cart.items.find(
|
|
318
|
-
(i) => i.productId === opts.productId && i.variantId === opts.variantId
|
|
319
|
-
);
|
|
320
|
-
if (existing) {
|
|
321
|
-
existing.quantity += qty;
|
|
322
|
-
this.persist();
|
|
323
|
-
this.emit();
|
|
324
|
-
return existing;
|
|
325
|
-
}
|
|
326
|
-
const item = {
|
|
327
|
-
id: lineId(),
|
|
328
|
-
productId: opts.productId,
|
|
329
|
-
sponsorId: opts.sponsorId,
|
|
330
|
-
variantId: opts.variantId,
|
|
331
|
-
brand: opts.brand,
|
|
332
|
-
name: opts.name,
|
|
333
|
-
unitPrice: opts.unitPrice,
|
|
334
|
-
currency: opts.currency,
|
|
335
|
-
imageUrl: opts.imageUrl,
|
|
336
|
-
quantity: qty
|
|
337
|
-
};
|
|
338
|
-
cart.items.push(item);
|
|
339
|
-
this.persist();
|
|
340
|
-
this.emit();
|
|
341
|
-
return item;
|
|
342
|
-
}
|
|
343
|
-
updateQuantity(lineItemId, sponsorId, quantity) {
|
|
344
|
-
const cart = this.cartsBySponsor.get(sponsorId);
|
|
345
|
-
if (!cart) return;
|
|
346
|
-
const item = cart.items.find((i) => i.id === lineItemId);
|
|
347
|
-
if (!item) return;
|
|
348
|
-
if (quantity <= 0) {
|
|
349
|
-
cart.items = cart.items.filter((i) => i.id !== lineItemId);
|
|
350
|
-
} else {
|
|
351
|
-
item.quantity = quantity;
|
|
352
|
-
}
|
|
353
|
-
if (cart.items.length === 0) this.cartsBySponsor.delete(sponsorId);
|
|
354
|
-
this.persist();
|
|
355
|
-
this.emit();
|
|
356
|
-
}
|
|
357
|
-
removeItem(lineItemId, sponsorId) {
|
|
358
|
-
this.updateQuantity(lineItemId, sponsorId, 0);
|
|
359
|
-
}
|
|
360
|
-
clearSponsorCart(sponsorId) {
|
|
361
|
-
if (this.cartsBySponsor.delete(sponsorId)) {
|
|
362
|
-
this.persist();
|
|
363
|
-
this.emit();
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
clearAllCarts() {
|
|
367
|
-
if (this.cartsBySponsor.size === 0) return;
|
|
368
|
-
this.cartsBySponsor.clear();
|
|
369
|
-
this.persist();
|
|
370
|
-
this.emit();
|
|
371
|
-
}
|
|
372
|
-
// MARK: - Read accessors
|
|
373
|
-
getAllCarts() {
|
|
374
|
-
return new Map(this.cartsBySponsor);
|
|
375
|
-
}
|
|
376
|
-
getCart(sponsorId) {
|
|
377
|
-
return this.cartsBySponsor.get(sponsorId);
|
|
378
|
-
}
|
|
379
|
-
get itemCount() {
|
|
380
|
-
let count = 0;
|
|
381
|
-
for (const cart of this.cartsBySponsor.values()) {
|
|
382
|
-
for (const item of cart.items) count += item.quantity;
|
|
383
|
-
}
|
|
384
|
-
return count;
|
|
385
|
-
}
|
|
386
|
-
subtotalForSponsor(sponsorId) {
|
|
387
|
-
const cart = this.cartsBySponsor.get(sponsorId);
|
|
388
|
-
if (!cart) return 0;
|
|
389
|
-
return cart.items.reduce((sum, i) => sum + i.unitPrice * i.quantity, 0);
|
|
390
|
-
}
|
|
391
|
-
get totalAcrossSponsors() {
|
|
392
|
-
let sum = 0;
|
|
393
|
-
for (const cart of this.cartsBySponsor.values()) {
|
|
394
|
-
for (const item of cart.items) sum += item.unitPrice * item.quantity;
|
|
395
|
-
}
|
|
396
|
-
return sum;
|
|
397
|
-
}
|
|
398
|
-
get primaryCurrency() {
|
|
399
|
-
for (const cart of this.cartsBySponsor.values()) return cart.currency;
|
|
400
|
-
return "NOK";
|
|
401
|
-
}
|
|
402
|
-
formatTotal() {
|
|
403
|
-
return formatPrice(this.totalAcrossSponsors, this.primaryCurrency);
|
|
404
|
-
}
|
|
405
|
-
// MARK: - Persistence
|
|
406
|
-
persist() {
|
|
407
|
-
if (typeof localStorage === "undefined") return;
|
|
408
|
-
try {
|
|
409
|
-
const obj = {};
|
|
410
|
-
for (const [k, v] of this.cartsBySponsor) obj[String(k)] = v;
|
|
411
|
-
localStorage.setItem(STORAGE_KEY, JSON.stringify(obj));
|
|
412
|
-
} catch {
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
loadFromStorage() {
|
|
416
|
-
if (typeof localStorage === "undefined") return /* @__PURE__ */ new Map();
|
|
417
|
-
try {
|
|
418
|
-
const raw = localStorage.getItem(STORAGE_KEY);
|
|
419
|
-
if (!raw) return /* @__PURE__ */ new Map();
|
|
420
|
-
const obj = JSON.parse(raw);
|
|
421
|
-
const map = /* @__PURE__ */ new Map();
|
|
422
|
-
for (const [k, v] of Object.entries(obj)) {
|
|
423
|
-
const id = parseInt(k, 10);
|
|
424
|
-
if (!Number.isNaN(id) && v && Array.isArray(v.items)) map.set(id, v);
|
|
425
|
-
}
|
|
426
|
-
return map;
|
|
427
|
-
} catch {
|
|
428
|
-
return /* @__PURE__ */ new Map();
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
emit() {
|
|
432
|
-
const detail = {
|
|
433
|
-
cartsBySponsor: this.getAllCarts(),
|
|
434
|
-
itemCount: this.itemCount
|
|
435
|
-
};
|
|
436
|
-
this.dispatchEvent(new CustomEvent("change", { detail }));
|
|
437
|
-
}
|
|
438
|
-
};
|
|
439
|
-
|
|
440
|
-
// src/core/checkout/payments/apple-pay.ts
|
|
441
|
-
var STRIPE_JS_URL = "https://js.stripe.com/v3";
|
|
442
|
-
var stripeJsPromise = null;
|
|
443
|
-
function loadStripeJs() {
|
|
444
|
-
if (stripeJsPromise) return stripeJsPromise;
|
|
445
|
-
if (typeof document === "undefined") {
|
|
446
|
-
return Promise.reject(new Error("[VioApplePay] Stripe.js requires a browser environment"));
|
|
447
|
-
}
|
|
448
|
-
stripeJsPromise = new Promise((resolve, reject) => {
|
|
449
|
-
const w = window;
|
|
450
|
-
if (w.Stripe) {
|
|
451
|
-
resolve(w.Stripe);
|
|
452
|
-
return;
|
|
453
|
-
}
|
|
454
|
-
const existing = document.querySelector(
|
|
455
|
-
`script[src^="${STRIPE_JS_URL}"]`
|
|
456
|
-
);
|
|
457
|
-
if (existing) {
|
|
458
|
-
existing.addEventListener("load", () => resolve(w.Stripe));
|
|
459
|
-
existing.addEventListener(
|
|
460
|
-
"error",
|
|
461
|
-
() => reject(new Error("[VioApplePay] Failed to load Stripe.js"))
|
|
462
|
-
);
|
|
463
|
-
return;
|
|
464
|
-
}
|
|
465
|
-
const script = document.createElement("script");
|
|
466
|
-
script.src = STRIPE_JS_URL;
|
|
467
|
-
script.async = true;
|
|
468
|
-
script.addEventListener("load", () => resolve(w.Stripe));
|
|
469
|
-
script.addEventListener(
|
|
470
|
-
"error",
|
|
471
|
-
() => reject(new Error("[VioApplePay] Failed to load Stripe.js"))
|
|
472
|
-
);
|
|
473
|
-
document.head.appendChild(script);
|
|
474
|
-
});
|
|
475
|
-
return stripeJsPromise;
|
|
476
|
-
}
|
|
477
|
-
async function getStripe(config) {
|
|
478
|
-
const Stripe = await loadStripeJs();
|
|
479
|
-
return Stripe(
|
|
480
|
-
config.publishableKey,
|
|
481
|
-
config.connectedAccount ? { stripeAccount: config.connectedAccount } : void 0
|
|
482
|
-
);
|
|
483
|
-
}
|
|
484
|
-
function buildRequestOptions(config) {
|
|
485
|
-
const ship = config.shippingOptions;
|
|
486
|
-
const preselectedShipping = ship && ship.length > 0 ? ship[0].amount : 0;
|
|
487
|
-
return {
|
|
488
|
-
country: config.country,
|
|
489
|
-
currency: config.currency.toLowerCase(),
|
|
490
|
-
total: { label: config.label, amount: config.amount + preselectedShipping },
|
|
491
|
-
requestPayerName: config.requestPayerName ?? true,
|
|
492
|
-
requestPayerEmail: config.requestPayerEmail ?? true,
|
|
493
|
-
requestShipping: ship && ship.length > 0 ? true : config.requestShipping ?? false,
|
|
494
|
-
...ship && ship.length > 0 ? { shippingOptions: ship } : {},
|
|
495
|
-
disableWallets: ["link", "googlePay"]
|
|
496
|
-
};
|
|
497
|
-
}
|
|
498
|
-
async function checkApplePayAvailability(config) {
|
|
499
|
-
if (typeof window === "undefined") return false;
|
|
500
|
-
if (typeof window.ApplePaySession === "undefined") {
|
|
501
|
-
return false;
|
|
502
|
-
}
|
|
503
|
-
if (!config.publishableKey) return false;
|
|
504
|
-
try {
|
|
505
|
-
const stripe = await getStripe(config);
|
|
506
|
-
const pr = stripe.paymentRequest(buildRequestOptions(config));
|
|
507
|
-
const result = await pr.canMakePayment();
|
|
508
|
-
return !!result?.applePay;
|
|
509
|
-
} catch (err) {
|
|
510
|
-
if (typeof console !== "undefined") {
|
|
511
|
-
console.warn("[VioApplePay] availability check failed:", err);
|
|
512
|
-
}
|
|
513
|
-
return false;
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
async function showApplePaySheet(config) {
|
|
517
|
-
if (!config.publishableKey) {
|
|
518
|
-
throw new Error("[VioApplePay] publishableKey is required");
|
|
519
|
-
}
|
|
520
|
-
const stripe = await getStripe(config);
|
|
521
|
-
const pr = stripe.paymentRequest(buildRequestOptions(config));
|
|
522
|
-
const canMake = await pr.canMakePayment();
|
|
523
|
-
if (!canMake?.applePay) {
|
|
524
|
-
throw new Error("[VioApplePay] Apple Pay is not available on this device");
|
|
525
|
-
}
|
|
526
|
-
return new Promise((resolve, reject) => {
|
|
527
|
-
let settled = false;
|
|
528
|
-
pr.on("paymentmethod", (raw) => {
|
|
529
|
-
const ev = raw;
|
|
530
|
-
settled = true;
|
|
531
|
-
ev.complete("success");
|
|
532
|
-
resolve({
|
|
533
|
-
paymentMethodId: ev.paymentMethod.id,
|
|
534
|
-
payerName: ev.payerName,
|
|
535
|
-
payerEmail: ev.payerEmail,
|
|
536
|
-
payerPhone: ev.payerPhone,
|
|
537
|
-
shippingAddress: ev.shippingAddress
|
|
538
|
-
});
|
|
539
|
-
});
|
|
540
|
-
pr.on("cancel", () => {
|
|
541
|
-
if (settled) return;
|
|
542
|
-
reject(new Error("[VioApplePay] User cancelled Apple Pay"));
|
|
543
|
-
});
|
|
544
|
-
pr.show();
|
|
545
|
-
});
|
|
546
|
-
}
|
|
547
|
-
async function prepareApplePay(config) {
|
|
548
|
-
const unavailable = (msg) => ({
|
|
549
|
-
available: false,
|
|
550
|
-
show: () => Promise.reject(new Error(msg))
|
|
551
|
-
});
|
|
552
|
-
if (typeof window === "undefined" || !config.publishableKey) {
|
|
553
|
-
return unavailable("[VioApplePay] unavailable");
|
|
554
|
-
}
|
|
555
|
-
if (typeof window.ApplePaySession === "undefined") {
|
|
556
|
-
return unavailable("[VioApplePay] ApplePaySession not available");
|
|
557
|
-
}
|
|
558
|
-
try {
|
|
559
|
-
const stripe = await getStripe(config);
|
|
560
|
-
const pr = stripe.paymentRequest(buildRequestOptions(config));
|
|
561
|
-
if (config.shippingOptions && config.shippingOptions.length > 0) {
|
|
562
|
-
pr.on("shippingoptionchange", (raw) => {
|
|
563
|
-
const ev = raw;
|
|
564
|
-
const opt = config.shippingOptions?.find((o) => o.id === ev.shippingOption?.id);
|
|
565
|
-
ev.updateWith({
|
|
566
|
-
status: "success",
|
|
567
|
-
total: { label: config.label, amount: config.amount + (opt?.amount ?? 0) }
|
|
568
|
-
});
|
|
569
|
-
});
|
|
570
|
-
}
|
|
571
|
-
const result = await pr.canMakePayment();
|
|
572
|
-
if (!result?.applePay) return unavailable("[VioApplePay] Apple Pay not available");
|
|
573
|
-
return {
|
|
574
|
-
available: true,
|
|
575
|
-
show() {
|
|
576
|
-
return new Promise((resolve, reject) => {
|
|
577
|
-
let settled = false;
|
|
578
|
-
pr.on("paymentmethod", (raw) => {
|
|
579
|
-
const ev = raw;
|
|
580
|
-
settled = true;
|
|
581
|
-
ev.complete("success");
|
|
582
|
-
resolve({
|
|
583
|
-
paymentMethodId: ev.paymentMethod.id,
|
|
584
|
-
payerName: ev.payerName,
|
|
585
|
-
payerEmail: ev.payerEmail,
|
|
586
|
-
payerPhone: ev.payerPhone,
|
|
587
|
-
shippingAddress: ev.shippingAddress
|
|
588
|
-
});
|
|
589
|
-
});
|
|
590
|
-
pr.on("cancel", () => {
|
|
591
|
-
if (!settled) reject(new Error("[VioApplePay] User cancelled Apple Pay"));
|
|
592
|
-
});
|
|
593
|
-
pr.show();
|
|
594
|
-
});
|
|
595
|
-
}
|
|
596
|
-
};
|
|
597
|
-
} catch (err) {
|
|
598
|
-
if (typeof console !== "undefined") {
|
|
599
|
-
console.warn("[VioApplePay] prepare failed:", err);
|
|
600
|
-
}
|
|
601
|
-
return unavailable("[VioApplePay] prepare failed");
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
// src/core/checkout/payments/klarna.ts
|
|
606
|
-
var KLARNA_SDK_URL = "https://js.klarna.com/web-sdk/v2/klarna.mjs";
|
|
607
|
-
var sdkCache = /* @__PURE__ */ new Map();
|
|
608
|
-
function loadKlarnaSdk(config) {
|
|
609
|
-
if (typeof window === "undefined") {
|
|
610
|
-
return Promise.reject(new Error("[VioKlarna] requires a browser environment"));
|
|
611
|
-
}
|
|
612
|
-
if (!config.clientId) {
|
|
613
|
-
return Promise.reject(new Error("[VioKlarna] clientId is required"));
|
|
614
|
-
}
|
|
615
|
-
const cacheKey = `${config.clientId}:${config.environment ?? "default"}`;
|
|
616
|
-
const cached = sdkCache.get(cacheKey);
|
|
617
|
-
if (cached) return cached;
|
|
618
|
-
const url = KLARNA_SDK_URL;
|
|
619
|
-
const promise = import(
|
|
620
|
-
/* @vite-ignore */
|
|
621
|
-
url
|
|
622
|
-
).then((mod) => {
|
|
623
|
-
const factory = mod.KlarnaSDK ?? mod.default;
|
|
624
|
-
if (typeof factory !== "function") {
|
|
625
|
-
throw new Error("[VioKlarna] KlarnaSDK factory not found in module");
|
|
626
|
-
}
|
|
627
|
-
return factory({
|
|
628
|
-
clientId: config.clientId,
|
|
629
|
-
products: ["PAYMENT"],
|
|
630
|
-
...config.locale ? { locale: config.locale } : {},
|
|
631
|
-
...config.environment ? { environment: config.environment } : {}
|
|
632
|
-
});
|
|
633
|
-
}).catch((err) => {
|
|
634
|
-
sdkCache.delete(cacheKey);
|
|
635
|
-
throw err;
|
|
636
|
-
});
|
|
637
|
-
sdkCache.set(cacheKey, promise);
|
|
638
|
-
return promise;
|
|
639
|
-
}
|
|
640
|
-
async function isKlarnaAvailable(config) {
|
|
641
|
-
if (typeof window === "undefined" || !config.clientId) return false;
|
|
642
|
-
try {
|
|
643
|
-
await loadKlarnaSdk(config);
|
|
644
|
-
return true;
|
|
645
|
-
} catch (err) {
|
|
646
|
-
if (typeof console !== "undefined") {
|
|
647
|
-
console.warn("[VioKlarna] availability check failed:", err);
|
|
648
|
-
}
|
|
649
|
-
return false;
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
function findAuthToken(value, depth = 0) {
|
|
653
|
-
if (!value || depth > 4 || typeof value !== "object") return void 0;
|
|
654
|
-
const o = value;
|
|
655
|
-
for (const key of ["authorizationToken", "authorization_token", "authToken"]) {
|
|
656
|
-
if (typeof o[key] === "string" && o[key].length > 0) return o[key];
|
|
657
|
-
}
|
|
658
|
-
for (const k of Object.keys(o)) {
|
|
659
|
-
const found = findAuthToken(o[k], depth + 1);
|
|
660
|
-
if (found) return found;
|
|
661
|
-
}
|
|
662
|
-
return void 0;
|
|
663
|
-
}
|
|
664
|
-
async function mountKlarnaExpressButton(opts) {
|
|
665
|
-
const sdk = await loadKlarnaSdk(opts.config);
|
|
666
|
-
const button = sdk.Payment.button({
|
|
667
|
-
id: "vio-klarna-express",
|
|
668
|
-
// `PAY` = one-time purchase (vs BUY/SUBSCRIBE). Must be an array.
|
|
669
|
-
intents: ["PAY"],
|
|
670
|
-
// initiationMode belongs on the BUTTON config (per Klarna Web SDK docs),
|
|
671
|
-
// NOT inside the initiate() data — when set only in initiate() the SDK
|
|
672
|
-
// falls back to its default (DEVICE_BEST → desktop popup), which opened
|
|
673
|
-
// and closed without completing. Forcing REDIRECT here makes the click do
|
|
674
|
-
// a full-page navigation to Klarna's hosted journey, returning via
|
|
675
|
-
// `returnUrl` (recovered by resumeKlarnaReturn()).
|
|
676
|
-
initiationMode: "REDIRECT"
|
|
677
|
-
});
|
|
678
|
-
const returnUrl = opts.returnUrl ?? (typeof window !== "undefined" ? window.location.origin + window.location.pathname : "");
|
|
679
|
-
let settled = false;
|
|
680
|
-
button.on("click", () => {
|
|
681
|
-
if (opts.pendingKey && opts.pendingContext && typeof sessionStorage !== "undefined") {
|
|
682
|
-
try {
|
|
683
|
-
sessionStorage.setItem(opts.pendingKey, JSON.stringify(opts.pendingContext));
|
|
684
|
-
} catch {
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
const promise = sdk.Payment.initiate(() => ({
|
|
688
|
-
currency: opts.paymentRequest.currency,
|
|
689
|
-
paymentAmount: opts.paymentRequest.amount,
|
|
690
|
-
intents: ["PAY"],
|
|
691
|
-
locale: "nb-NO",
|
|
692
|
-
// REDIRECT = full-page navigation to Klarna's hosted journey, then back
|
|
693
|
-
// to `returnUrl` with the result. We force this (vs DEVICE_BEST's desktop
|
|
694
|
-
// popup) because the popup proved unreliable here — it opened and closed
|
|
695
|
-
// without completing. The redirect flow has no popup-blocker / window-
|
|
696
|
-
// messaging failure modes; the return is recovered via resumeKlarnaReturn().
|
|
697
|
-
initiationMode: "REDIRECT",
|
|
698
|
-
returnUrl,
|
|
699
|
-
...opts.paymentRequest.supplementaryPurchaseData ? { supplementaryPurchaseData: opts.paymentRequest.supplementaryPurchaseData } : {}
|
|
700
|
-
}));
|
|
701
|
-
Promise.resolve(promise).then((result) => {
|
|
702
|
-
if (settled) return;
|
|
703
|
-
const token = findAuthToken(result);
|
|
704
|
-
if (token) {
|
|
705
|
-
settled = true;
|
|
706
|
-
opts.onAuthorize({ authorizationToken: token, raw: result });
|
|
707
|
-
} else if (typeof console !== "undefined") {
|
|
708
|
-
console.warn("[VioKlarna] authorize resolved without a token:", result);
|
|
709
|
-
}
|
|
710
|
-
}).catch((err) => {
|
|
711
|
-
opts.onError?.(err);
|
|
712
|
-
});
|
|
713
|
-
return promise;
|
|
714
|
-
});
|
|
715
|
-
button.mount(opts.container);
|
|
716
|
-
return {
|
|
717
|
-
unmount: () => {
|
|
718
|
-
try {
|
|
719
|
-
button.unmount();
|
|
720
|
-
} catch {
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
};
|
|
724
|
-
}
|
|
725
|
-
function readKlarnaTokenFromUrl() {
|
|
726
|
-
if (typeof window === "undefined") return null;
|
|
727
|
-
const params = new URLSearchParams(window.location.search);
|
|
728
|
-
for (const name of [
|
|
729
|
-
"authorization_token",
|
|
730
|
-
"authorizationToken",
|
|
731
|
-
"klarna_authorization_token",
|
|
732
|
-
"vio_klarna_token"
|
|
733
|
-
]) {
|
|
734
|
-
const v = params.get(name);
|
|
735
|
-
if (v && v.length > 0 && v !== "{klarna.authorization_token}") {
|
|
736
|
-
return { token: v, param: name };
|
|
737
|
-
}
|
|
738
|
-
}
|
|
739
|
-
return null;
|
|
740
|
-
}
|
|
741
|
-
function cleanUrlParams(params) {
|
|
742
|
-
if (typeof window === "undefined" || !window.history?.replaceState) return;
|
|
743
|
-
const url = new URL(window.location.href);
|
|
744
|
-
let changed = false;
|
|
745
|
-
for (const p of params) {
|
|
746
|
-
if (url.searchParams.has(p)) {
|
|
747
|
-
url.searchParams.delete(p);
|
|
748
|
-
changed = true;
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
if (changed) window.history.replaceState({}, "", url.toString());
|
|
752
|
-
}
|
|
753
|
-
async function listenForKlarnaCompletion(opts) {
|
|
754
|
-
let sdk;
|
|
755
|
-
try {
|
|
756
|
-
sdk = await loadKlarnaSdk(opts.config);
|
|
757
|
-
} catch (err) {
|
|
758
|
-
if (typeof console !== "undefined") {
|
|
759
|
-
console.warn("[VioKlarna] resume: SDK load failed:", err);
|
|
760
|
-
}
|
|
761
|
-
return;
|
|
762
|
-
}
|
|
763
|
-
let settled = false;
|
|
764
|
-
const onComplete = (...args) => {
|
|
765
|
-
if (settled) return;
|
|
766
|
-
for (const a of args) {
|
|
767
|
-
const token = findAuthToken(a);
|
|
768
|
-
if (token) {
|
|
769
|
-
settled = true;
|
|
770
|
-
opts.onComplete({ authorizationToken: token, raw: a });
|
|
771
|
-
return;
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
|
-
};
|
|
775
|
-
try {
|
|
776
|
-
sdk.Payment.on?.("complete", onComplete);
|
|
777
|
-
} catch {
|
|
778
|
-
}
|
|
779
|
-
try {
|
|
780
|
-
sdk.Payment.on?.("error", (...a) => opts.onError?.(a[0]));
|
|
781
|
-
} catch {
|
|
782
|
-
}
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
// src/core/checkout/payments/klarna-payments.ts
|
|
786
|
-
var KLARNA_KP_LIB_URL = "https://x.klarnacdn.net/kp/lib/v1/api.js";
|
|
787
|
-
var kpLoad = null;
|
|
788
|
-
function loadKpLib() {
|
|
789
|
-
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
790
|
-
return Promise.reject(new Error("[VioKlarnaKP] requires a browser environment"));
|
|
791
|
-
}
|
|
792
|
-
if (window.Klarna?.Payments) return Promise.resolve(window.Klarna);
|
|
793
|
-
if (kpLoad) return kpLoad;
|
|
794
|
-
kpLoad = new Promise((resolve, reject) => {
|
|
795
|
-
const timer = setTimeout(
|
|
796
|
-
() => reject(new Error("[VioKlarnaKP] kp/lib/v1/api.js load timed out")),
|
|
797
|
-
12e3
|
|
798
|
-
);
|
|
799
|
-
const ready = () => {
|
|
800
|
-
clearTimeout(timer);
|
|
801
|
-
if (window.Klarna?.Payments) resolve(window.Klarna);
|
|
802
|
-
else reject(new Error("[VioKlarnaKP] Klarna global missing after ready"));
|
|
803
|
-
};
|
|
804
|
-
const prev = window.klarnaAsyncCallback;
|
|
805
|
-
window.klarnaAsyncCallback = () => {
|
|
806
|
-
if (typeof prev === "function") {
|
|
807
|
-
try {
|
|
808
|
-
prev();
|
|
809
|
-
} catch {
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
ready();
|
|
813
|
-
};
|
|
814
|
-
const existing = document.querySelector(
|
|
815
|
-
`script[src="${KLARNA_KP_LIB_URL}"]`
|
|
816
|
-
);
|
|
817
|
-
if (existing) {
|
|
818
|
-
if (window.Klarna?.Payments) ready();
|
|
819
|
-
} else {
|
|
820
|
-
const s = document.createElement("script");
|
|
821
|
-
s.src = KLARNA_KP_LIB_URL;
|
|
822
|
-
s.async = true;
|
|
823
|
-
s.onerror = () => {
|
|
824
|
-
clearTimeout(timer);
|
|
825
|
-
kpLoad = null;
|
|
826
|
-
reject(new Error("[VioKlarnaKP] failed to load kp/lib/v1/api.js"));
|
|
827
|
-
};
|
|
828
|
-
document.head.appendChild(s);
|
|
829
|
-
}
|
|
830
|
-
}).catch((err) => {
|
|
831
|
-
kpLoad = null;
|
|
832
|
-
throw err;
|
|
833
|
-
});
|
|
834
|
-
return kpLoad;
|
|
835
|
-
}
|
|
836
|
-
async function isKlarnaPaymentsAvailable() {
|
|
837
|
-
if (typeof window === "undefined") return false;
|
|
838
|
-
try {
|
|
839
|
-
await loadKpLib();
|
|
840
|
-
return true;
|
|
841
|
-
} catch (err) {
|
|
842
|
-
if (typeof console !== "undefined") {
|
|
843
|
-
console.warn("[VioKlarnaKP] availability check failed:", err);
|
|
844
|
-
}
|
|
845
|
-
return false;
|
|
846
|
-
}
|
|
847
|
-
}
|
|
848
|
-
async function createKlarnaPaymentsWidget(input) {
|
|
849
|
-
const Klarna = await loadKpLib();
|
|
850
|
-
Klarna.Payments.init({ client_token: input.clientToken });
|
|
851
|
-
let selected = input.category ?? input.categories[0]?.identifier ?? "pay_later";
|
|
852
|
-
return {
|
|
853
|
-
categories: input.categories,
|
|
854
|
-
get selected() {
|
|
855
|
-
return selected;
|
|
856
|
-
},
|
|
857
|
-
load(category) {
|
|
858
|
-
if (category) selected = category;
|
|
859
|
-
return new Promise((resolve, reject) => {
|
|
860
|
-
try {
|
|
861
|
-
Klarna.Payments.load(
|
|
862
|
-
{ container: input.container, payment_method_category: selected },
|
|
863
|
-
{},
|
|
864
|
-
(res) => {
|
|
865
|
-
if (res?.error) reject(res.error);
|
|
866
|
-
else resolve({ showForm: res?.show_form !== false });
|
|
867
|
-
}
|
|
868
|
-
);
|
|
869
|
-
} catch (err) {
|
|
870
|
-
reject(err);
|
|
871
|
-
}
|
|
872
|
-
});
|
|
873
|
-
},
|
|
874
|
-
authorize(data) {
|
|
875
|
-
return new Promise((resolve, reject) => {
|
|
876
|
-
try {
|
|
877
|
-
Klarna.Payments.authorize(
|
|
878
|
-
{ payment_method_category: selected, auto_finalize: true },
|
|
879
|
-
data ?? {},
|
|
880
|
-
(res) => {
|
|
881
|
-
if (res?.approved && res.authorization_token) {
|
|
882
|
-
resolve(res.authorization_token);
|
|
883
|
-
} else if (res?.error) {
|
|
884
|
-
reject(res.error);
|
|
885
|
-
} else if (res?.finalize_required) {
|
|
886
|
-
reject(new Error("Klarna requires a finalize step (multistep)"));
|
|
887
|
-
} else {
|
|
888
|
-
reject(new Error("Klarna authorization was not approved"));
|
|
889
|
-
}
|
|
890
|
-
}
|
|
891
|
-
);
|
|
892
|
-
} catch (err) {
|
|
893
|
-
reject(err);
|
|
894
|
-
}
|
|
895
|
-
});
|
|
896
|
-
}
|
|
897
|
-
};
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
// src/core/checkout/checkout-manager.ts
|
|
901
|
-
function describeError(err) {
|
|
902
|
-
if (err instanceof Error) return err.message;
|
|
903
|
-
if (typeof err === "string") return err;
|
|
904
|
-
if (err && typeof err === "object") {
|
|
905
|
-
const o = err;
|
|
906
|
-
for (const k of ["message", "error", "reason", "description"]) {
|
|
907
|
-
if (typeof o[k] === "string" && o[k]) return o[k];
|
|
908
|
-
}
|
|
909
|
-
try {
|
|
910
|
-
return JSON.stringify(err);
|
|
911
|
-
} catch {
|
|
912
|
-
return String(err);
|
|
913
|
-
}
|
|
914
|
-
}
|
|
915
|
-
return String(err);
|
|
916
|
-
}
|
|
917
|
-
var KLARNA_PENDING_KEY = "vio.klarna.pending.v1";
|
|
918
|
-
var KLARNA_SHIPPING_OPTIONS = [
|
|
919
|
-
{
|
|
920
|
-
id: "standard",
|
|
921
|
-
method: "Standard",
|
|
922
|
-
description: "3\u20135 virkedager",
|
|
923
|
-
price: 4900,
|
|
924
|
-
tax_amount: 0,
|
|
925
|
-
tax_rate: 0,
|
|
926
|
-
preselected: true
|
|
927
|
-
},
|
|
928
|
-
{
|
|
929
|
-
id: "express",
|
|
930
|
-
method: "Express",
|
|
931
|
-
description: "1\u20132 virkedager",
|
|
932
|
-
price: 9900,
|
|
933
|
-
tax_amount: 0,
|
|
934
|
-
tax_rate: 0,
|
|
935
|
-
preselected: false
|
|
936
|
-
}
|
|
937
|
-
];
|
|
938
|
-
var CheckoutManager = class extends EventTarget {
|
|
939
|
-
constructor(cartManager) {
|
|
940
|
-
super();
|
|
941
|
-
this.cartManager = cartManager;
|
|
942
|
-
this.state = null;
|
|
943
|
-
this.applePayAvailableCache = null;
|
|
944
|
-
this.klarnaAvailableCache = null;
|
|
945
|
-
this.klarnaOrderInFlight = false;
|
|
946
|
-
}
|
|
947
|
-
open(sponsorId) {
|
|
948
|
-
const cart = this.cartManager.getCart(sponsorId);
|
|
949
|
-
if (!cart || cart.items.length === 0) {
|
|
950
|
-
throw new Error(`[VioCheckout] sponsor ${sponsorId} has no cart items`);
|
|
951
|
-
}
|
|
952
|
-
this.state = {
|
|
953
|
-
sponsorId,
|
|
954
|
-
subtotal: this.cartManager.subtotalForSponsor(sponsorId),
|
|
955
|
-
currency: cart.currency
|
|
956
|
-
};
|
|
957
|
-
this.emit();
|
|
958
|
-
return this.state;
|
|
959
|
-
}
|
|
960
|
-
close() {
|
|
961
|
-
this.state = null;
|
|
962
|
-
this.applePayAvailableCache = null;
|
|
963
|
-
this.klarnaAvailableCache = null;
|
|
964
|
-
this.emit();
|
|
965
|
-
}
|
|
966
|
-
setAddress(address) {
|
|
967
|
-
if (!this.state) return;
|
|
968
|
-
this.state = { ...this.state, address };
|
|
969
|
-
this.emit();
|
|
970
|
-
}
|
|
971
|
-
selectPaymentMethod(method) {
|
|
972
|
-
if (!this.state) return;
|
|
973
|
-
this.state = { ...this.state, paymentMethod: method };
|
|
974
|
-
this.emit();
|
|
975
|
-
this.dispatchEvent(
|
|
976
|
-
new CustomEvent("payment-select", {
|
|
977
|
-
detail: { method, state: this.state }
|
|
978
|
-
})
|
|
979
|
-
);
|
|
980
|
-
}
|
|
981
|
-
// MARK: - Apple Pay (Stripe Payment Request)
|
|
982
|
-
/**
|
|
983
|
-
* Returns true if Apple Pay can be shown on this device + Stripe config.
|
|
984
|
-
* Result is memoised — call `clearApplePayCache()` if config changes.
|
|
985
|
-
*/
|
|
986
|
-
async isApplePayAvailable(country = "NO") {
|
|
987
|
-
if (this.applePayAvailableCache !== null) return this.applePayAvailableCache;
|
|
988
|
-
const cfg = Configuration.isInitialized ? Configuration.get() : null;
|
|
989
|
-
const publishableKey = cfg?.stripePublishableKey ?? "";
|
|
990
|
-
if (!publishableKey || !this.state) {
|
|
991
|
-
this.applePayAvailableCache = false;
|
|
992
|
-
return false;
|
|
993
|
-
}
|
|
994
|
-
const amount = toSmallestUnit(this.state.subtotal, this.state.currency);
|
|
995
|
-
const available = await checkApplePayAvailability({
|
|
996
|
-
publishableKey,
|
|
997
|
-
connectedAccount: this.findStripeConnectAccount() ?? void 0,
|
|
998
|
-
amount,
|
|
999
|
-
currency: this.state.currency,
|
|
1000
|
-
country,
|
|
1001
|
-
label: "Vio"
|
|
1002
|
-
});
|
|
1003
|
-
this.applePayAvailableCache = available;
|
|
1004
|
-
return available;
|
|
1005
|
-
}
|
|
1006
|
-
clearApplePayCache() {
|
|
1007
|
-
this.applePayAvailableCache = null;
|
|
1008
|
-
}
|
|
1009
|
-
/**
|
|
1010
|
-
* Prepare Apple Pay for the product-page express button (no active checkout
|
|
1011
|
-
* needed). Pre-creates the Stripe PaymentRequest + resolves canMakePayment so
|
|
1012
|
-
* the returned `show()` can fire synchronously inside the tap (Apple blocks
|
|
1013
|
-
* the sheet if `show()` is reached after an await). `show()` resolves the
|
|
1014
|
-
* sheet, then dispatches `payment-complete` (→ confirmation) or
|
|
1015
|
-
* `payment-error`. `available` is true only when Apple Pay will actually work.
|
|
1016
|
-
*/
|
|
1017
|
-
async prepareApplePayFor(amount, currency, country = "NO") {
|
|
1018
|
-
const cfg = Configuration.isInitialized ? Configuration.get() : null;
|
|
1019
|
-
if (!cfg?.stripePublishableKey) return { available: false, show: async () => {
|
|
1020
|
-
} };
|
|
1021
|
-
const prepared = await prepareApplePay({
|
|
1022
|
-
publishableKey: cfg.stripePublishableKey,
|
|
1023
|
-
amount: toSmallestUnit(amount, currency),
|
|
1024
|
-
currency,
|
|
1025
|
-
country,
|
|
1026
|
-
label: "Vio",
|
|
1027
|
-
// Native Apple Pay shipping — same Standard/Express options as Klarna.
|
|
1028
|
-
shippingOptions: KLARNA_SHIPPING_OPTIONS.map((o) => ({
|
|
1029
|
-
id: o.id,
|
|
1030
|
-
label: o.method,
|
|
1031
|
-
detail: o.description,
|
|
1032
|
-
amount: o.price
|
|
1033
|
-
}))
|
|
1034
|
-
});
|
|
1035
|
-
return {
|
|
1036
|
-
available: prepared.available,
|
|
1037
|
-
show: async () => {
|
|
1038
|
-
try {
|
|
1039
|
-
const result = await prepared.show();
|
|
1040
|
-
this.selectPaymentMethod("apple-pay");
|
|
1041
|
-
const state = this.state ?? { sponsorId: 0, subtotal: amount, currency };
|
|
1042
|
-
this.dispatchEvent(
|
|
1043
|
-
new CustomEvent("payment-complete", {
|
|
1044
|
-
detail: { method: "apple-pay", state, result }
|
|
1045
|
-
})
|
|
1046
|
-
);
|
|
1047
|
-
} catch (err) {
|
|
1048
|
-
if (typeof console !== "undefined") {
|
|
1049
|
-
console.warn("[VioCheckout] Apple Pay express failed:", err);
|
|
1050
|
-
}
|
|
1051
|
-
this.dispatchEvent(
|
|
1052
|
-
new CustomEvent("payment-error", {
|
|
1053
|
-
detail: { method: "apple-pay", error: describeError(err) }
|
|
1054
|
-
})
|
|
1055
|
-
);
|
|
1056
|
-
}
|
|
1057
|
-
}
|
|
1058
|
-
};
|
|
1059
|
-
}
|
|
1060
|
-
/**
|
|
1061
|
-
* Prepare an Apple Pay express charge for an arbitrary amount (e.g. the whole
|
|
1062
|
-
* cart) with NO active checkout. Unlike `prepareApplePayFor`, the returned
|
|
1063
|
-
* `show()` RESOLVES with the result (or `null` on cancel/failure) and does
|
|
1064
|
-
* NOT dispatch the global `payment-complete` — so no checkout overlay appears.
|
|
1065
|
-
* The caller (the cart drawer) renders its own confirmation. `show()` must be
|
|
1066
|
-
* called synchronously inside the tap (Apple's user-gesture rule).
|
|
1067
|
-
*/
|
|
1068
|
-
async prepareApplePayExpress(amount, currency, country = "NO") {
|
|
1069
|
-
const cfg = Configuration.isInitialized ? Configuration.get() : null;
|
|
1070
|
-
if (!cfg?.stripePublishableKey) return { available: false, show: async () => null };
|
|
1071
|
-
const prepared = await prepareApplePay({
|
|
1072
|
-
publishableKey: cfg.stripePublishableKey,
|
|
1073
|
-
amount: toSmallestUnit(amount, currency),
|
|
1074
|
-
currency,
|
|
1075
|
-
country,
|
|
1076
|
-
label: "Vio",
|
|
1077
|
-
// Native Apple Pay shipping (same Standard/Express options as Klarna) —
|
|
1078
|
-
// selected inside the sheet, so it's not a separate checkout step.
|
|
1079
|
-
shippingOptions: KLARNA_SHIPPING_OPTIONS.map((o) => ({
|
|
1080
|
-
id: o.id,
|
|
1081
|
-
label: o.method,
|
|
1082
|
-
detail: o.description,
|
|
1083
|
-
amount: o.price
|
|
1084
|
-
}))
|
|
1085
|
-
});
|
|
1086
|
-
return {
|
|
1087
|
-
available: prepared.available,
|
|
1088
|
-
show: async () => {
|
|
1089
|
-
try {
|
|
1090
|
-
return await prepared.show();
|
|
1091
|
-
} catch (err) {
|
|
1092
|
-
if (typeof console !== "undefined") {
|
|
1093
|
-
console.warn("[VioCheckout] cart Apple Pay express failed:", err);
|
|
1094
|
-
}
|
|
1095
|
-
return null;
|
|
1096
|
-
}
|
|
1097
|
-
}
|
|
1098
|
-
};
|
|
1099
|
-
}
|
|
1100
|
-
/**
|
|
1101
|
-
* Launches the Apple Pay sheet. Resolves with the Stripe PaymentMethod
|
|
1102
|
-
* id once the user authorises — the backend then confirms the
|
|
1103
|
-
* PaymentIntent with that id to actually capture the funds.
|
|
1104
|
-
*/
|
|
1105
|
-
async startApplePay(options = {}) {
|
|
1106
|
-
if (!this.state) throw new Error("[VioCheckout] no active checkout \u2014 call open() first");
|
|
1107
|
-
const cfg = Configuration.get();
|
|
1108
|
-
if (!cfg.stripePublishableKey) {
|
|
1109
|
-
throw new Error(
|
|
1110
|
-
"[VioCheckout] No Stripe publishable key configured. Pass `stripePublishableKey` to Vio.init({ ... })."
|
|
1111
|
-
);
|
|
1112
|
-
}
|
|
1113
|
-
const amount = toSmallestUnit(this.state.subtotal, this.state.currency);
|
|
1114
|
-
const result = await showApplePaySheet({
|
|
1115
|
-
publishableKey: cfg.stripePublishableKey,
|
|
1116
|
-
connectedAccount: this.findStripeConnectAccount() ?? void 0,
|
|
1117
|
-
amount,
|
|
1118
|
-
currency: this.state.currency,
|
|
1119
|
-
country: options.country ?? "NO",
|
|
1120
|
-
label: options.label ?? "Vio"
|
|
1121
|
-
});
|
|
1122
|
-
this.selectPaymentMethod("apple-pay");
|
|
1123
|
-
this.dispatchEvent(
|
|
1124
|
-
new CustomEvent("payment-complete", {
|
|
1125
|
-
detail: { method: "apple-pay", state: this.state, result }
|
|
1126
|
-
})
|
|
1127
|
-
);
|
|
1128
|
-
return result;
|
|
1129
|
-
}
|
|
1130
|
-
// MARK: - Klarna Express Checkout (Klarna Web SDK v2)
|
|
1131
|
-
/**
|
|
1132
|
-
* True if the Klarna Express button can be offered (clientId configured +
|
|
1133
|
-
* SDK loads). Memoised per active checkout. Never throws.
|
|
1134
|
-
*/
|
|
1135
|
-
async isKlarnaExpressAvailable() {
|
|
1136
|
-
if (this.klarnaAvailableCache !== null) return this.klarnaAvailableCache;
|
|
1137
|
-
const cfg = Configuration.isInitialized ? Configuration.get() : null;
|
|
1138
|
-
const clientId = cfg?.klarnaClientId ?? "";
|
|
1139
|
-
if (!clientId || !this.state) {
|
|
1140
|
-
this.klarnaAvailableCache = false;
|
|
1141
|
-
return false;
|
|
1142
|
-
}
|
|
1143
|
-
const available = await isKlarnaAvailable({
|
|
1144
|
-
clientId,
|
|
1145
|
-
locale: "nb-NO",
|
|
1146
|
-
environment: cfg?.klarnaEnvironment
|
|
1147
|
-
});
|
|
1148
|
-
this.klarnaAvailableCache = available;
|
|
1149
|
-
return available;
|
|
1150
|
-
}
|
|
1151
|
-
clearKlarnaCache() {
|
|
1152
|
-
this.klarnaAvailableCache = null;
|
|
1153
|
-
}
|
|
1154
|
-
/**
|
|
1155
|
-
* Build the Klarna payment request from the active cart — amounts in the
|
|
1156
|
-
* currency's minor unit (øre/cents), one line item per cart line.
|
|
1157
|
-
*/
|
|
1158
|
-
buildKlarnaPaymentRequest() {
|
|
1159
|
-
if (!this.state) throw new Error("[VioCheckout] no active checkout \u2014 call open() first");
|
|
1160
|
-
const currency = this.state.currency;
|
|
1161
|
-
const lineItems = this.items.map((item) => {
|
|
1162
|
-
const unitPrice = toSmallestUnit(item.unitPrice, currency);
|
|
1163
|
-
return {
|
|
1164
|
-
name: item.name || item.brand || "Produkt",
|
|
1165
|
-
quantity: item.quantity,
|
|
1166
|
-
unitPrice,
|
|
1167
|
-
totalAmount: unitPrice * item.quantity,
|
|
1168
|
-
reference: String(item.productId),
|
|
1169
|
-
...item.imageUrl ? { imageUrl: item.imageUrl } : {}
|
|
1170
|
-
};
|
|
1171
|
-
});
|
|
1172
|
-
return {
|
|
1173
|
-
currency,
|
|
1174
|
-
amount: toSmallestUnit(this.state.subtotal, currency),
|
|
1175
|
-
supplementaryPurchaseData: { lineItems }
|
|
1176
|
-
};
|
|
1177
|
-
}
|
|
1178
|
-
/**
|
|
1179
|
-
* Mount the Klarna Express button into `container`. On a successful
|
|
1180
|
-
* authorization, fires `payment-complete` with the authorization token —
|
|
1181
|
-
* the host (or vio-backend) then creates the Klarna order server-side.
|
|
1182
|
-
*
|
|
1183
|
-
* Returns the button handle so the UI can unmount on teardown.
|
|
1184
|
-
*/
|
|
1185
|
-
async mountKlarnaExpress(container) {
|
|
1186
|
-
if (!this.state) throw new Error("[VioCheckout] no active checkout \u2014 call open() first");
|
|
1187
|
-
const cfg = Configuration.get();
|
|
1188
|
-
if (!cfg.klarnaClientId) {
|
|
1189
|
-
throw new Error(
|
|
1190
|
-
"[VioCheckout] No Klarna client id configured. Pass `klarnaClientId` to Vio.init({ ... })."
|
|
1191
|
-
);
|
|
1192
|
-
}
|
|
1193
|
-
const pendingContext = this.buildKlarnaPendingContext();
|
|
1194
|
-
return mountKlarnaExpressButton({
|
|
1195
|
-
config: {
|
|
1196
|
-
clientId: cfg.klarnaClientId,
|
|
1197
|
-
locale: "nb-NO",
|
|
1198
|
-
environment: cfg.klarnaEnvironment
|
|
1199
|
-
},
|
|
1200
|
-
container,
|
|
1201
|
-
paymentRequest: this.buildKlarnaPaymentRequest(),
|
|
1202
|
-
pendingKey: KLARNA_PENDING_KEY,
|
|
1203
|
-
pendingContext,
|
|
1204
|
-
// POPUP path: the initiate() promise resolves in-page with the token.
|
|
1205
|
-
onAuthorize: (result) => {
|
|
1206
|
-
void this.completeKlarnaOrder(result.authorizationToken, pendingContext, result);
|
|
1207
|
-
},
|
|
1208
|
-
onError: (err) => {
|
|
1209
|
-
if (typeof console !== "undefined") {
|
|
1210
|
-
console.warn("[VioCheckout] Klarna Express cancelled/error:", err);
|
|
1211
|
-
}
|
|
1212
|
-
this.dispatchEvent(
|
|
1213
|
-
new CustomEvent("payment-error", {
|
|
1214
|
-
detail: { method: "klarna", error: describeError(err) }
|
|
1215
|
-
})
|
|
1216
|
-
);
|
|
1217
|
-
}
|
|
1218
|
-
});
|
|
1219
|
-
}
|
|
1220
|
-
/**
|
|
1221
|
-
* Klarna Payments — classic widget flow (server client_token). Creates a
|
|
1222
|
-
* session on vio-backend, then mounts Klarna's inline widget into
|
|
1223
|
-
* `container`. Returns a handle to switch category and to authorize → which
|
|
1224
|
-
* on approval creates the order via the same backend /orders route as KEC.
|
|
1225
|
-
*
|
|
1226
|
-
* Unlike `mountKlarnaExpress`, this needs no public `klarnaClientId` and no
|
|
1227
|
-
* origin handshake — auth is the server-side API key.
|
|
1228
|
-
*/
|
|
1229
|
-
async mountKlarnaPayments(container, opts) {
|
|
1230
|
-
if (!this.state) throw new Error("[VioCheckout] no active checkout \u2014 call open() first");
|
|
1231
|
-
const cfg = Configuration.get();
|
|
1232
|
-
const { ctx, shippingOptions } = opts?.withShipping ? this.buildKlarnaInstantContext(opts.shippingId) : { ctx: this.buildKlarnaPendingContext(), shippingOptions: void 0 };
|
|
1233
|
-
const res = await fetch(`${cfg.apiBase}/v2/commerce/klarna/sessions`, {
|
|
1234
|
-
method: "POST",
|
|
1235
|
-
headers: { "Content-Type": "application/json", "X-API-Key": cfg.apiKey },
|
|
1236
|
-
body: JSON.stringify({
|
|
1237
|
-
currency: ctx.currency,
|
|
1238
|
-
purchaseCountry: "NO",
|
|
1239
|
-
locale: "nb-NO",
|
|
1240
|
-
orderAmount: ctx.orderAmount,
|
|
1241
|
-
orderLines: ctx.orderLines,
|
|
1242
|
-
...shippingOptions ? { shippingOptions } : {}
|
|
1243
|
-
})
|
|
1244
|
-
});
|
|
1245
|
-
const json = await res.json().catch(() => ({}));
|
|
1246
|
-
if (!res.ok || !json.clientToken) {
|
|
1247
|
-
throw new Error(json?.error ?? `Klarna session failed (HTTP ${res.status})`);
|
|
1248
|
-
}
|
|
1249
|
-
const widget = await createKlarnaPaymentsWidget({
|
|
1250
|
-
clientToken: json.clientToken,
|
|
1251
|
-
categories: json.paymentMethodCategories ?? [],
|
|
1252
|
-
container
|
|
1253
|
-
});
|
|
1254
|
-
await widget.load();
|
|
1255
|
-
return {
|
|
1256
|
-
categories: widget.categories,
|
|
1257
|
-
get selected() {
|
|
1258
|
-
return widget.selected;
|
|
1259
|
-
},
|
|
1260
|
-
load: (category) => widget.load(category),
|
|
1261
|
-
authorize: async () => {
|
|
1262
|
-
try {
|
|
1263
|
-
const token = await widget.authorize();
|
|
1264
|
-
await this.completeKlarnaOrder(token, ctx, { authorizationToken: token });
|
|
1265
|
-
} catch (err) {
|
|
1266
|
-
if (typeof console !== "undefined") {
|
|
1267
|
-
console.warn("[VioCheckout] Klarna Payments authorize failed:", err);
|
|
1268
|
-
}
|
|
1269
|
-
this.dispatchEvent(
|
|
1270
|
-
new CustomEvent("payment-error", {
|
|
1271
|
-
detail: { method: "klarna", error: describeError(err) }
|
|
1272
|
-
})
|
|
1273
|
-
);
|
|
1274
|
-
}
|
|
1275
|
-
},
|
|
1276
|
-
unmount: () => {
|
|
1277
|
-
try {
|
|
1278
|
-
container.innerHTML = "";
|
|
1279
|
-
} catch {
|
|
1280
|
-
}
|
|
1281
|
-
}
|
|
1282
|
-
};
|
|
1283
|
-
}
|
|
1284
|
-
/** True if the Klarna Payments widget lib can load (UI gate). */
|
|
1285
|
-
async klarnaPaymentsAvailable() {
|
|
1286
|
-
return isKlarnaPaymentsAvailable();
|
|
1287
|
-
}
|
|
1288
|
-
/** Cart snapshot + chosen shipping line/amount, for express purchases. */
|
|
1289
|
-
buildKlarnaInstantContext(shippingId) {
|
|
1290
|
-
const base = this.buildKlarnaPendingContext();
|
|
1291
|
-
const shipping = (shippingId ? KLARNA_SHIPPING_OPTIONS.find((o) => o.id === shippingId) : void 0) ?? KLARNA_SHIPPING_OPTIONS.find((o) => o.preselected) ?? KLARNA_SHIPPING_OPTIONS[0];
|
|
1292
|
-
const orderLines = [...base.orderLines];
|
|
1293
|
-
if (shipping) {
|
|
1294
|
-
orderLines.push({
|
|
1295
|
-
name: `Frakt \u2013 ${shipping.method}`,
|
|
1296
|
-
quantity: 1,
|
|
1297
|
-
unit_price: shipping.price,
|
|
1298
|
-
total_amount: shipping.price,
|
|
1299
|
-
reference: `shipping:${shipping.id}`,
|
|
1300
|
-
type: "shipping_fee"
|
|
1301
|
-
});
|
|
1302
|
-
}
|
|
1303
|
-
return {
|
|
1304
|
-
ctx: {
|
|
1305
|
-
...base,
|
|
1306
|
-
orderLines,
|
|
1307
|
-
orderAmount: base.orderAmount + (shipping?.price ?? 0)
|
|
1308
|
-
},
|
|
1309
|
-
shippingOptions: KLARNA_SHIPPING_OPTIONS
|
|
1310
|
-
};
|
|
1311
|
-
}
|
|
1312
|
-
/**
|
|
1313
|
-
* Express instant purchase — go straight to Klarna's own modal, no checkout
|
|
1314
|
-
* overlay. Creates a session (with shipping options), loads the widget into a
|
|
1315
|
-
* hidden container, then authorizes (which opens Klarna's confirm modal). On
|
|
1316
|
-
* approval creates the order → `payment-complete` (host shows confirmation);
|
|
1317
|
-
* on failure → `payment-error`. Call `open(sponsorId)` first.
|
|
1318
|
-
*/
|
|
1319
|
-
async startKlarnaInstant() {
|
|
1320
|
-
if (!this.state) throw new Error("[VioCheckout] no active checkout \u2014 call open() first");
|
|
1321
|
-
const cfg = Configuration.get();
|
|
1322
|
-
const { ctx, shippingOptions } = this.buildKlarnaInstantContext();
|
|
1323
|
-
let clientToken;
|
|
1324
|
-
let categories;
|
|
1325
|
-
try {
|
|
1326
|
-
const res = await fetch(`${cfg.apiBase}/v2/commerce/klarna/sessions`, {
|
|
1327
|
-
method: "POST",
|
|
1328
|
-
headers: { "Content-Type": "application/json", "X-API-Key": cfg.apiKey },
|
|
1329
|
-
body: JSON.stringify({
|
|
1330
|
-
currency: ctx.currency,
|
|
1331
|
-
purchaseCountry: "NO",
|
|
1332
|
-
locale: "nb-NO",
|
|
1333
|
-
orderAmount: ctx.orderAmount,
|
|
1334
|
-
orderLines: ctx.orderLines,
|
|
1335
|
-
shippingOptions
|
|
1336
|
-
})
|
|
1337
|
-
});
|
|
1338
|
-
const json = await res.json().catch(() => ({}));
|
|
1339
|
-
if (!res.ok || !json.clientToken) {
|
|
1340
|
-
throw new Error(json?.error ?? `Klarna session failed (HTTP ${res.status})`);
|
|
1341
|
-
}
|
|
1342
|
-
clientToken = json.clientToken;
|
|
1343
|
-
categories = json.paymentMethodCategories ?? [];
|
|
1344
|
-
} catch (err) {
|
|
1345
|
-
this.dispatchEvent(
|
|
1346
|
-
new CustomEvent("payment-error", {
|
|
1347
|
-
detail: { method: "klarna", error: describeError(err) }
|
|
1348
|
-
})
|
|
1349
|
-
);
|
|
1350
|
-
return;
|
|
1351
|
-
}
|
|
1352
|
-
const container = document.createElement("div");
|
|
1353
|
-
container.setAttribute("aria-hidden", "true");
|
|
1354
|
-
container.style.cssText = "position:fixed;left:-9999px;top:0;width:1px;height:1px;overflow:hidden;";
|
|
1355
|
-
document.body.appendChild(container);
|
|
1356
|
-
try {
|
|
1357
|
-
const widget = await createKlarnaPaymentsWidget({ clientToken, categories, container });
|
|
1358
|
-
await widget.load();
|
|
1359
|
-
const token = await widget.authorize();
|
|
1360
|
-
await this.completeKlarnaOrder(token, ctx, { authorizationToken: token });
|
|
1361
|
-
} catch (err) {
|
|
1362
|
-
if (typeof console !== "undefined") {
|
|
1363
|
-
console.warn("[VioCheckout] Klarna instant failed:", err);
|
|
1364
|
-
}
|
|
1365
|
-
this.dispatchEvent(
|
|
1366
|
-
new CustomEvent("payment-error", {
|
|
1367
|
-
detail: { method: "klarna", error: describeError(err) }
|
|
1368
|
-
})
|
|
1369
|
-
);
|
|
1370
|
-
} finally {
|
|
1371
|
-
try {
|
|
1372
|
-
container.remove();
|
|
1373
|
-
} catch {
|
|
1374
|
-
}
|
|
1375
|
-
}
|
|
1376
|
-
}
|
|
1377
|
-
/**
|
|
1378
|
-
* REDIRECT path: call once on app load. If a Klarna purchase was launched
|
|
1379
|
-
* (pending context persisted) and we're back with a token — from the URL or
|
|
1380
|
-
* the SDK's resumed `complete` event — finish creating the order + confirm.
|
|
1381
|
-
*/
|
|
1382
|
-
async resumeKlarnaReturn() {
|
|
1383
|
-
if (typeof sessionStorage === "undefined") return;
|
|
1384
|
-
const raw = sessionStorage.getItem(KLARNA_PENDING_KEY);
|
|
1385
|
-
if (!raw) return;
|
|
1386
|
-
let pending;
|
|
1387
|
-
try {
|
|
1388
|
-
pending = JSON.parse(raw);
|
|
1389
|
-
} catch {
|
|
1390
|
-
sessionStorage.removeItem(KLARNA_PENDING_KEY);
|
|
1391
|
-
return;
|
|
1392
|
-
}
|
|
1393
|
-
const fromUrl = readKlarnaTokenFromUrl();
|
|
1394
|
-
if (fromUrl) {
|
|
1395
|
-
cleanUrlParams([fromUrl.param]);
|
|
1396
|
-
await this.completeKlarnaOrder(fromUrl.token, pending, { authorizationToken: fromUrl.token });
|
|
1397
|
-
return;
|
|
1398
|
-
}
|
|
1399
|
-
if (typeof window !== "undefined" && typeof console !== "undefined" && window.location.search) {
|
|
1400
|
-
console.warn(
|
|
1401
|
-
"[VioCheckout] Klarna return: no known token param. URL was:",
|
|
1402
|
-
window.location.search
|
|
1403
|
-
);
|
|
1404
|
-
}
|
|
1405
|
-
const cfg = Configuration.isInitialized ? Configuration.get() : null;
|
|
1406
|
-
if (!cfg?.klarnaClientId) return;
|
|
1407
|
-
await listenForKlarnaCompletion({
|
|
1408
|
-
config: { clientId: cfg.klarnaClientId, locale: "nb-NO", environment: cfg.klarnaEnvironment },
|
|
1409
|
-
onComplete: (result) => {
|
|
1410
|
-
void this.completeKlarnaOrder(result.authorizationToken, pending, result);
|
|
1411
|
-
},
|
|
1412
|
-
onError: (err) => {
|
|
1413
|
-
if (typeof console !== "undefined") {
|
|
1414
|
-
console.warn("[VioCheckout] Klarna resume error:", err);
|
|
1415
|
-
}
|
|
1416
|
-
}
|
|
1417
|
-
});
|
|
1418
|
-
}
|
|
1419
|
-
/** Snapshot of the active cart for redirect recovery (minor units). */
|
|
1420
|
-
buildKlarnaPendingContext() {
|
|
1421
|
-
if (!this.state) throw new Error("[VioCheckout] no active checkout");
|
|
1422
|
-
const currency = this.state.currency;
|
|
1423
|
-
return {
|
|
1424
|
-
sponsorId: this.state.sponsorId,
|
|
1425
|
-
currency,
|
|
1426
|
-
orderAmount: toSmallestUnit(this.state.subtotal, currency),
|
|
1427
|
-
orderLines: this.items.map((item) => {
|
|
1428
|
-
const unitPrice = toSmallestUnit(item.unitPrice, currency);
|
|
1429
|
-
return {
|
|
1430
|
-
name: item.name || item.brand || "Produkt",
|
|
1431
|
-
quantity: item.quantity,
|
|
1432
|
-
unit_price: unitPrice,
|
|
1433
|
-
total_amount: unitPrice * item.quantity,
|
|
1434
|
-
reference: String(item.productId)
|
|
1435
|
-
};
|
|
1436
|
-
})
|
|
1437
|
-
};
|
|
1438
|
-
}
|
|
1439
|
-
/**
|
|
1440
|
-
* POST the token + order lines to vio-backend (it holds the secret API key
|
|
1441
|
-
* and creates the real Klarna order). On success → `payment-complete`; on
|
|
1442
|
-
* failure → `payment-error`. Idempotent: clears the pending key so the
|
|
1443
|
-
* popup and redirect paths can't double-fire.
|
|
1444
|
-
*/
|
|
1445
|
-
async completeKlarnaOrder(authorizationToken, ctx, result) {
|
|
1446
|
-
if (typeof sessionStorage !== "undefined") {
|
|
1447
|
-
if (this.klarnaOrderInFlight) return;
|
|
1448
|
-
this.klarnaOrderInFlight = true;
|
|
1449
|
-
sessionStorage.removeItem(KLARNA_PENDING_KEY);
|
|
1450
|
-
}
|
|
1451
|
-
const cfg = Configuration.get();
|
|
1452
|
-
try {
|
|
1453
|
-
const res = await fetch(`${cfg.apiBase}/v2/commerce/klarna/orders`, {
|
|
1454
|
-
method: "POST",
|
|
1455
|
-
headers: { "Content-Type": "application/json", "X-API-Key": cfg.apiKey },
|
|
1456
|
-
body: JSON.stringify({
|
|
1457
|
-
authorizationToken,
|
|
1458
|
-
purchaseCountry: "NO",
|
|
1459
|
-
currency: ctx.currency,
|
|
1460
|
-
locale: "nb-NO",
|
|
1461
|
-
orderAmount: ctx.orderAmount,
|
|
1462
|
-
orderLines: ctx.orderLines
|
|
1463
|
-
})
|
|
1464
|
-
});
|
|
1465
|
-
const json = await res.json().catch(() => ({}));
|
|
1466
|
-
if (!res.ok) throw new Error(json?.error ?? `Klarna order failed (HTTP ${res.status})`);
|
|
1467
|
-
const state = this.state ?? { sponsorId: ctx.sponsorId, subtotal: ctx.orderAmount, currency: ctx.currency };
|
|
1468
|
-
this.dispatchEvent(
|
|
1469
|
-
new CustomEvent("payment-complete", {
|
|
1470
|
-
detail: {
|
|
1471
|
-
method: "klarna",
|
|
1472
|
-
state,
|
|
1473
|
-
// chargedTotal in MAJOR units (incl. shipping for express) for the
|
|
1474
|
-
// confirmation screen. ctx.orderAmount is minor (øre) → /100.
|
|
1475
|
-
result: { ...result, order: json, chargedTotal: ctx.orderAmount / 100 }
|
|
1476
|
-
}
|
|
1477
|
-
})
|
|
1478
|
-
);
|
|
1479
|
-
} catch (err) {
|
|
1480
|
-
if (typeof console !== "undefined") {
|
|
1481
|
-
console.error("[VioCheckout] Klarna order creation failed:", err);
|
|
1482
|
-
}
|
|
1483
|
-
this.dispatchEvent(
|
|
1484
|
-
new CustomEvent("payment-error", {
|
|
1485
|
-
detail: { method: "klarna", error: err instanceof Error ? err.message : String(err) }
|
|
1486
|
-
})
|
|
1487
|
-
);
|
|
1488
|
-
} finally {
|
|
1489
|
-
this.klarnaOrderInFlight = false;
|
|
1490
|
-
}
|
|
1491
|
-
}
|
|
1492
|
-
// MARK: - Accessors
|
|
1493
|
-
get currentState() {
|
|
1494
|
-
return this.state;
|
|
1495
|
-
}
|
|
1496
|
-
get items() {
|
|
1497
|
-
if (!this.state) return [];
|
|
1498
|
-
return this.cartManager.getCart(this.state.sponsorId)?.items ?? [];
|
|
1499
|
-
}
|
|
1500
|
-
// MARK: - Internals
|
|
1501
|
-
/**
|
|
1502
|
-
* Resolve the Stripe Connect account for the active sponsor — for now
|
|
1503
|
-
* pulled from bootstrap cache if present. Returns undefined if not set
|
|
1504
|
-
* (Stripe will route to the platform account in that case).
|
|
1505
|
-
*/
|
|
1506
|
-
findStripeConnectAccount() {
|
|
1507
|
-
return void 0;
|
|
1508
|
-
}
|
|
1509
|
-
emit() {
|
|
1510
|
-
this.dispatchEvent(
|
|
1511
|
-
new CustomEvent("change", { detail: { state: this.state } })
|
|
1512
|
-
);
|
|
1513
|
-
}
|
|
1514
|
-
};
|
|
1515
|
-
function toSmallestUnit(amount, currency) {
|
|
1516
|
-
const zeroDecimal = /* @__PURE__ */ new Set(["JPY", "KRW", "VND"]);
|
|
1517
|
-
if (zeroDecimal.has(currency.toUpperCase())) return Math.round(amount);
|
|
1518
|
-
return Math.round(amount * 100);
|
|
1519
|
-
}
|
|
1520
|
-
|
|
1521
|
-
// src/core/client.ts
|
|
1522
|
-
var VioFacade = class {
|
|
1523
|
-
constructor() {
|
|
1524
|
-
this.apiInstance = null;
|
|
1525
|
-
this.commerceClients = /* @__PURE__ */ new Map();
|
|
1526
|
-
this.bootstrapPromise = null;
|
|
1527
|
-
this.cachedBootstrap = null;
|
|
1528
|
-
// Client-side managers — created lazily on first access. They don't
|
|
1529
|
-
// need Vio.init() to work (state is purely local).
|
|
1530
|
-
this.cartManager = null;
|
|
1531
|
-
this.checkoutManager = null;
|
|
1532
|
-
}
|
|
1533
|
-
init(config) {
|
|
1534
|
-
Configuration.init(config);
|
|
1535
|
-
this.apiInstance = null;
|
|
1536
|
-
this.commerceClients.clear();
|
|
1537
|
-
this.bootstrapPromise = null;
|
|
1538
|
-
this.cachedBootstrap = null;
|
|
1539
|
-
}
|
|
1540
|
-
get isInitialized() {
|
|
1541
|
-
return Configuration.isInitialized;
|
|
1542
|
-
}
|
|
1543
|
-
get config() {
|
|
1544
|
-
return Configuration.get();
|
|
1545
|
-
}
|
|
1546
|
-
// MARK: - Client-side managers (no init required)
|
|
1547
|
-
/** Multi-sponsor cart manager. Reactive — listen via addEventListener('change', ...). */
|
|
1548
|
-
get cart() {
|
|
1549
|
-
if (!this.cartManager) this.cartManager = new CartManager();
|
|
1550
|
-
return this.cartManager;
|
|
1551
|
-
}
|
|
1552
|
-
/** Checkout flow manager. Subscribes to cart state via the CartManager. */
|
|
1553
|
-
get checkout() {
|
|
1554
|
-
if (!this.checkoutManager) this.checkoutManager = new CheckoutManager(this.cart);
|
|
1555
|
-
return this.checkoutManager;
|
|
1556
|
-
}
|
|
1557
|
-
// MARK: - Backend-bound
|
|
1558
|
-
/** Lazy VioApi for REST endpoints on `api-dev.vio.live`. */
|
|
1559
|
-
get api() {
|
|
1560
|
-
if (!this.apiInstance) {
|
|
1561
|
-
const cfg = Configuration.get();
|
|
1562
|
-
this.apiInstance = createVioApi({ apiBase: cfg.apiBase, apiKey: cfg.apiKey });
|
|
1563
|
-
}
|
|
1564
|
-
return this.apiInstance;
|
|
1565
|
-
}
|
|
1566
|
-
/**
|
|
1567
|
-
* Bootstrap — fetches client config and caches it. Dedupes in-flight
|
|
1568
|
-
* concurrent requests.
|
|
1569
|
-
*/
|
|
1570
|
-
async bootstrap() {
|
|
1571
|
-
if (this.cachedBootstrap) return this.cachedBootstrap;
|
|
1572
|
-
if (this.bootstrapPromise) return this.bootstrapPromise;
|
|
1573
|
-
this.bootstrapPromise = this.api.bootstrap().then((res) => {
|
|
1574
|
-
this.cachedBootstrap = res;
|
|
1575
|
-
return res;
|
|
1576
|
-
}).finally(() => {
|
|
1577
|
-
this.bootstrapPromise = null;
|
|
1578
|
-
});
|
|
1579
|
-
return this.bootstrapPromise;
|
|
1580
|
-
}
|
|
1581
|
-
get bootstrapCache() {
|
|
1582
|
-
return this.cachedBootstrap;
|
|
1583
|
-
}
|
|
1584
|
-
/**
|
|
1585
|
-
* Get a CommerceClient scoped to a specific sponsor (via bootstrap).
|
|
1586
|
-
* Throws if bootstrap hasn't been called or the sponsor is unknown.
|
|
1587
|
-
*/
|
|
1588
|
-
commerceFor(sponsorId) {
|
|
1589
|
-
const existing = this.commerceClients.get(sponsorId);
|
|
1590
|
-
if (existing) return existing;
|
|
1591
|
-
const sponsor = this.findSponsor(sponsorId);
|
|
1592
|
-
if (!sponsor) {
|
|
1593
|
-
throw new Error(
|
|
1594
|
-
`[Vio] Sponsor ${sponsorId} not found in bootstrap. Call Vio.bootstrap() first or pass commerce-key directly.`
|
|
1595
|
-
);
|
|
1596
|
-
}
|
|
1597
|
-
const cfg = Configuration.get();
|
|
1598
|
-
const client = createCommerceClient({ endpoint: cfg.graphQLBase, apiKey: sponsor.commerce.apiKey });
|
|
1599
|
-
this.commerceClients.set(sponsorId, client);
|
|
1600
|
-
return client;
|
|
1601
|
-
}
|
|
1602
|
-
/** Get a CommerceClient using an explicit commerce apiKey (skip bootstrap). */
|
|
1603
|
-
commerceWithKey(commerceApiKey) {
|
|
1604
|
-
const cfg = Configuration.get();
|
|
1605
|
-
return createCommerceClient({ endpoint: cfg.graphQLBase, apiKey: commerceApiKey });
|
|
1606
|
-
}
|
|
1607
|
-
findSponsor(sponsorId) {
|
|
1608
|
-
const boot = this.cachedBootstrap;
|
|
1609
|
-
if (!boot) return void 0;
|
|
1610
|
-
if (boot.primarySponsor?.id === sponsorId) return boot.primarySponsor;
|
|
1611
|
-
return boot.secondarySponsors?.find((s) => s.id === sponsorId);
|
|
1612
|
-
}
|
|
1613
|
-
};
|
|
1614
|
-
var Vio = new VioFacade();
|
|
1615
|
-
|
|
1616
|
-
export { CartManager, CheckoutManager, CommerceClient, Configuration, KLARNA_SHIPPING_OPTIONS, Vio, VioApi, VioApiError, __decorateClass, checkApplePayAvailability, createCommerceClient, createVioApi, displayPrice, formatPrice, primaryImageUrl, showApplePaySheet };
|
|
1617
|
-
//# sourceMappingURL=chunk-5SPY7TH6.js.map
|
|
1618
|
-
//# sourceMappingURL=chunk-5SPY7TH6.js.map
|