@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.
@@ -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